bool map of explicit on/off choices. * * A module absent from this map has never been chosen for, and falls back to * its own is_enabled_by_default(). */ const STATE_OPTION = 'pagecore_enabled_modules'; /** * Discovered modules, keyed by id. * * @var Module[] */ private $modules = array(); /** * Ids of modules whose boot() has run, so it cannot run twice. * * @var string[] */ private $booted = array(); /** * Whether discovery has already run. * * @var bool */ private $discovered = false; /** * Scan modules/ and instantiate what is found. * * A module directory named foo-bar must contain class-foo-bar-module.php * defining PageCore\Foo_Bar_Module. A directory without one is a group, * and the modules inside it are found the same way, one level down, so * modules/editor-options/tablet-navigation/ is the tablet-navigation * module. Anything that does not match is skipped in silence, so a * half-written module cannot break the admin. * * @return void */ public function discover() { if ( $this->discovered ) { return; } $this->discovered = true; $base = PAGECORE_DIR . 'modules'; $dirs = is_dir( $base ) ? glob( $base . '/*', GLOB_ONLYDIR ) : array(); foreach ( (array) $dirs as $dir ) { $module = $this->load_from_directory( $dir ); if ( $module ) { $this->register( $module ); continue; } // A folder with no module of its own is a group, such as // editor-options/, holding related modules one level down. if ( ! is_readable( $dir . '/class-' . basename( $dir ) . '-module.php' ) ) { foreach ( (array) glob( $dir . '/*', GLOB_ONLYDIR ) as $child ) { $module = $this->load_from_directory( $child ); if ( $module ) { $this->register( $module ); } } } } /** * Register modules that do not live in modules/. * * Handlers receive and return an array of Module instances, which is * how a separate plugin can add a PageCore module. * * @param Module[] $modules Modules discovered so far. */ $external = apply_filters( 'pagecore_register_modules', array() ); foreach ( (array) $external as $module ) { if ( $module instanceof Module ) { $this->register( $module ); } } uasort( $this->modules, static function ( $a, $b ) { return $a->get_menu_position() <=> $b->get_menu_position(); } ); } /** * Instantiate the module living in one directory. * * @param string $dir Absolute path to a module directory. * @return Module|null */ private function load_from_directory( $dir ) { $slug = basename( $dir ); if ( ! preg_match( '/^[a-z0-9]+(?:-[a-z0-9]+)*$/', $slug ) ) { return null; } $file = $dir . '/class-' . $slug . '-module.php'; if ( ! is_readable( $file ) ) { return null; } // The module owns everything beneath its directory, so its whole tree is // autoloadable once we know it exists. Autoloader::add_path( $dir, true ); require_once $file; $class_name = Autoloader::NAMESPACE_PREFIX . str_replace( '-', '_', ucwords( $slug, '-' ) ) . '_Module'; if ( ! class_exists( $class_name ) ) { return null; } $module = new $class_name(); if ( ! $module instanceof Module || $module->get_id() !== $slug ) { return null; } $module->set_location( $dir ); return $module; } /** * Add a module to the registry. * * @param Module $module Module instance. * @return void */ public function register( Module $module ) { $this->modules[ $module->get_id() ] = $module; } /** * Every discovered module. * * @return Module[] */ public function all() { $this->discover(); return $this->modules; } /** * One module by id. * * @param string $id Module id. * @return Module|null */ public function get( $id ) { $this->discover(); return isset( $this->modules[ $id ] ) ? $this->modules[ $id ] : null; } /** * Modules that are both enabled and available. * * @return Module[] */ public function enabled() { return array_filter( $this->all(), function ( Module $module ) { return $this->is_enabled( $module->get_id() ); } ); } /** * Whether the site owner has a module switched on, whether or not it can * run right now. * * A module waiting on something - the Style Library on the PageCore * theme - keeps its switch. This reads that switch, for anything that * needs to know what will run once the wait is over. * * @param string $id Module id. * @return bool */ public function is_switched_on( $id ) { $module = $this->get( $id ); if ( ! $module ) { return false; } $state = $this->get_state(); return array_key_exists( $id, $state ) ? (bool) $state[ $id ] : $module->is_enabled_by_default(); } /** * Whether a module should run. * * @param string $id Module id. * @return bool */ public function is_enabled( $id ) { $module = $this->get( $id ); if ( ! $module || ! $module->is_available() ) { return false; } return (bool) $this->is_switched_on( $id ); } /** * Switch a module on or off. * * Switching off leaves every trace of the module's data in place. * * @param string $id Module id. * @param bool $enabled Desired state. * @return bool|\WP_Error True on success. */ public function set_enabled( $id, $enabled ) { $module = $this->get( $id ); if ( ! $module ) { return new \WP_Error( 'pagecore_unknown_module', __( 'That module does not exist.', 'pagecore' ), array( 'status' => 404 ) ); } if ( $enabled && ! $module->is_available() ) { return new \WP_Error( 'pagecore_module_unavailable', $module->get_unavailable_reason(), array( 'status' => 400 ) ); } $state = $this->get_state(); $state[ $id ] = (bool) $enabled; update_option( self::STATE_OPTION, $state, true ); return true; } /** * The stored on/off map. * * @return array */ private function get_state() { $state = get_option( self::STATE_OPTION, array() ); return is_array( $state ) ? $state : array(); } /** * Boot every enabled module. * * One module throwing must not take the rest of the plugin down, so each * boot is guarded and failures are logged rather than fatal. * * @return void */ public function boot_enabled() { foreach ( $this->enabled() as $id => $module ) { if ( in_array( $id, $this->booted, true ) ) { continue; } $this->booted[] = $id; try { $module->boot(); } catch ( \Exception $e ) { Logger::error( sprintf( 'Module %s failed to boot: %s', $id, $e->getMessage() ) ); } catch ( \Error $e ) { Logger::error( sprintf( 'Module %s failed to boot: %s', $id, $e->getMessage() ) ); } } } /** * Every listed module as a plain array, for the Settings screen. * * @return array[] */ public function to_array() { $listed = array_filter( $this->all(), static function ( Module $module ) { return $module->is_listed(); } ); return array_values( array_map( static function ( Module $module ) { return $module->to_array(); }, $listed ) ); } }