register( array( * 'id' => 'woocommerce', * 'name' => 'AI House WooCommerce', * 'version' => '1.0.0', * 'requires_core' => '1.9.8', * 'features' => array( 'woo.stock', 'woo.warranty' ), * 'boot' => array( 'MYBOT_Woo_Bootstrap', 'boot' ), * ) ); * } ); * * WHY FEATURES HAVE NAMES * * A boolean flag scattered through the core ("is Woo active?") would mean the core knows the name of every * add-on, which breaks the first invariant of the architecture (docs/13 muc 1: the core never contains add-on * code). Named features invert it: the add-on declares `woo.stock`, the core asks `has( 'woo.stock' )`, and * neither needs to know the other's name. The same names are what a license server returns, so the registry * is also the join between "what is installed" and "what is paid for". * * WHAT THIS FILE DELIBERATELY DOES NOT DO * * It does not check licenses, phone home, or refuse to register an add-on whose key is missing. Licensing * lives in the add-on (docs/13 muc 4), because a core that could disable an add-on is a core that contains * a paid gate — and because a broken license check in the core would take down a site whose owner paid for * nothing. * * @package AI House */ defined( 'ABSPATH' ) || exit; /** * Registry of the add-ons that are installed and active. */ class MYBOT_Addons { /** * Registered add-ons, keyed by id. * * @var array */ protected $addons = array(); /** * Feature name to add-on id, built from what add-ons declare. * * @var array */ protected $features = array(); /** * Problems found while registering, kept for the admin screen. * * A rejected registration is not an error the site owner sees at the wrong moment: it is recorded here and * shown on the AI House > Add-ons screen, because "my add-on does nothing" is otherwise impossible to * diagnose — the same silent-failure shape this project has recorded six times. * * @var array */ protected $problems = array(); /** * Register one add-on. * * @param array $addon Add-on definition. * @return bool True when the add-on was accepted. */ public function register( array $addon ) { $id = isset( $addon['id'] ) ? sanitize_key( (string) $addon['id'] ) : ''; if ( '' === $id ) { $this->reject( '', __( 'The add-on did not declare an id.', 'aihouse' ) ); return false; } if ( isset( $this->addons[ $id ] ) ) { $this->reject( $id, __( 'Another add-on already registered this id.', 'aihouse' ) ); return false; } $requires = isset( $addon['requires_core'] ) ? (string) $addon['requires_core'] : ''; if ( '' !== $requires && version_compare( MYBOT_VERSION, $requires, '<' ) ) { $this->reject( $id, sprintf( /* translators: 1: required core version, 2: installed core version */ __( 'This add-on needs AI House %1$s or newer, and %2$s is installed. Update AI House, then activate the add-on again.', 'aihouse' ), $requires, MYBOT_VERSION ) ); return false; } $features = isset( $addon['features'] ) && is_array( $addon['features'] ) ? $addon['features'] : array(); $clean = array(); foreach ( $features as $feature ) { $feature = is_string( $feature ) ? trim( $feature ) : ''; if ( '' === $feature ) { continue; } if ( isset( $this->features[ $feature ] ) ) { $this->reject( $id, sprintf( /* translators: 1: feature name, 2: add-on id that already declared it */ __( 'The feature "%1$s" is already declared by "%2$s". Feature names have to be unique, or a paid feature could be switched on by a free add-on that claims the same name.', 'aihouse' ), $feature, $this->features[ $feature ] ) ); continue; } $this->features[ $feature ] = $id; $clean[] = $feature; } $this->addons[ $id ] = array( 'id' => $id, 'name' => isset( $addon['name'] ) ? sanitize_text_field( (string) $addon['name'] ) : $id, 'version' => isset( $addon['version'] ) ? sanitize_text_field( (string) $addon['version'] ) : '', 'requires_core' => $requires, 'features' => $clean, 'boot' => isset( $addon['boot'] ) ? $addon['boot'] : null, 'file' => isset( $addon['file'] ) ? (string) $addon['file'] : '', ); /** * Fires after an add-on has been accepted into the registry. * * The core uses this to boot the add-on's own code: registering is a declaration, booting is the * add-on's decision about when to run. Keeping the two apart is what lets the admin screen list an * add-on whose key is missing without that add-on having hooked anything yet. * * @param array $addon The accepted definition. */ do_action( 'mybot_addon_registered', $this->addons[ $id ] ); $boot = $this->addons[ $id ]['boot']; if ( is_callable( $boot ) ) { call_user_func( $boot, $this->addons[ $id ] ); } return true; } /** * Record a rejected registration. * * @param string $id Add-on id, empty when it did not declare one. * @param string $message Why it was rejected. * @return void */ protected function reject( $id, $message ) { $this->problems[] = array( 'id' => $id, 'message' => $message, ); } /** * Whether an add-on is registered. * * @param string $id Add-on id. * @return bool */ public function is_active( $id ) { return isset( $this->addons[ sanitize_key( (string) $id ) ] ); } /** * Whether a feature is provided by a registered add-on. * * This is the question the rest of the core asks, and it is a filter so that a site can answer it without * an add-on: a developer who wants to try a feature by hand can return true from * `mybot_feature_enabled`, and a site that wants to switch a paid feature off can return false without * deactivating the add-on. The filter receives the feature name and the add-on id that declared it, so a * decision can be made per feature rather than per add-on. * * @param string $feature Feature name, e.g. `woo.stock`. * @return bool */ public function has( $feature ) { $feature = is_string( $feature ) ? trim( $feature ) : ''; $owner = isset( $this->features[ $feature ] ) ? $this->features[ $feature ] : ''; $enabled = '' !== $owner; /** * Filters whether a named feature is enabled. * * @param bool $enabled Whether an add-on declared it. * @param string $feature Feature name. * @param string $owner Add-on id that declared it, empty when nothing did. */ return (bool) apply_filters( 'mybot_feature_enabled', $enabled, $feature, $owner ); } /** * Every registered add-on. * * @return array */ public function all() { return $this->addons; } /** * Registrations that were rejected, with the reason. * * @return array */ public function problems() { return $this->problems; } /** * Every feature name that any add-on declared. * * @return array */ public function feature_names() { return array_keys( $this->features ); } }