aboutsummaryrefslogtreecommitdiffstats
path: root/includes
diff options
context:
space:
mode:
authorNikki Nikkhoui <nnikkhoui@wikimedia.org>2020-02-10 11:47:46 -0300
committerTim Starling <tstarling@wikimedia.org>2020-04-17 15:48:38 +1000
commit0adc5f342888740e1b611f95f1474be329eae817 (patch)
tree7d1ccedc5a0016b24592feb69454f67ea3cd2a54 /includes
parent99a84628f422b28739cf2ff1d42ef43f0f38fe5b (diff)
downloadmediawikicore-0adc5f342888740e1b611f95f1474be329eae817.tar.gz
mediawikicore-0adc5f342888740e1b611f95f1474be329eae817.zip
Hook Container
New classes and modificatons to existing classes to support the new Hooks system. All changes are documented in RFC https://phabricator.wikimedia.org/T240307. - HookContainer.php: Class for doing much of what Hooks.php has historically done, but enabling new-style hooks to be processed and registered. Changes include new ways of defining hook handler functions as an object with defined dependencies in extension.json, removing runWithoutAbort() and addit it to an $options parameter to be passed to HookContainer::run(), being able to decipher whether a hook handler is legacy or non-legacy style and run them in the appropriate way, etc. - DeprecatedHooks.php: For marking hooks deprecated and verifying if one is deprecated - DeprecatedHooksTest.php: Unit tests for DeprecatedHooks.php - Hooks.php: register() will now additionally register hooks with handlers in new HooksContainer.php. getHandlers() will be a legacy wrapper for calling the newer HookContainer::getHandlers() - MediaWikiServices.php: Added getHookContainer() for retrieving HookContainer singleton - ExtensionProcessor.php: modified extractHooks() to be able to extract new style handler objects being registered in extension.json - ServiceWiring.php: Added HookContainer to list of services to return - HookContainerTest.php: Unit tests for HookContainer.php - ExtensionProcessorTest.php: Moved file out of /unit folder and now extends MediaWikiTestCase instead of MediaWikiUnitTestCase (as the tests are not truly unit tests). Modified existing tests for ExtensionProcessor::extractHooks() to include a test case for new style handler Bug: T240307 Change-Id: I432861d8995cfd7180e77e115251d8055b7eceec
Diffstat (limited to 'includes')
-rw-r--r--includes/HookRunner/DeprecatedHooks.php96
-rw-r--r--includes/HookRunner/HookContainer.php365
-rw-r--r--includes/Hooks.php160
-rw-r--r--includes/MediaWikiServices.php20
-rw-r--r--includes/ServiceWiring.php13
-rw-r--r--includes/registration/ExtensionProcessor.php129
6 files changed, 645 insertions, 138 deletions
diff --git a/includes/HookRunner/DeprecatedHooks.php b/includes/HookRunner/DeprecatedHooks.php
new file mode 100644
index 000000000000..c3db19e0e9a8
--- /dev/null
+++ b/includes/HookRunner/DeprecatedHooks.php
@@ -0,0 +1,96 @@
+<?php
+/**
+ * Holds list of deprecated hooks and methods for retrieval
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\HookRunner;
+
+use InvalidArgumentException;
+
+class DeprecatedHooks {
+
+ /**
+ * @param array[] $deprecatedHooks List of hooks to mark as deprecated.
+ * Value arrays for each hook contain:
+ * - deprecatedVersion: (string) Version in which the hook was deprecated,
+ * to pass to wfDeprecated().
+ * - component: (string, optional) $component to pass to wfDeprecated().
+ */
+ public function __construct( array $deprecatedHooks = [] ) {
+ foreach ( $deprecatedHooks as $hook => $info ) {
+ $this->markDeprecated( $hook, $info['deprecatedVersion'], $info['component'] ?? false );
+ }
+ }
+
+ /**
+ * @var array[] List of deprecated hooks. Value arrays for each hook contain:
+ * - deprecatedVersion: (string) Version in which the hook was deprecated,
+ * to pass to wfDeprecated().
+ * - component: (string, optional) $component to pass to wfDeprecated().
+ */
+ private $deprecatedHooks = [];
+
+ /**
+ * For use by extensions, to add to list of deprecated hooks.
+ * Core-defined hooks should instead be added to $this->$deprecatedHooks directly.
+ * However, the preferred method of marking a hook deprecated is by adding it to
+ * the DeprecatedHooks attribute in extension.json
+ *
+ * @param string $hook
+ * @param string $version Version in which the hook was deprecated, to pass to wfDeprecated()
+ * @param string|null $component (optional) component to pass to wfDeprecated().
+ * @throws InvalidArgumentException Hook has already been marked deprecated
+ */
+ public function markDeprecated( string $hook, string $version, ?string $component = null ) : void {
+ if ( isset( $this->deprecatedHooks[$hook] ) ) {
+ throw new InvalidArgumentException(
+ "Cannot mark hook '$hook' deprecated with version $version. " .
+ "It is already marked deprecated with version " .
+ $this->deprecatedHooks[$hook]['deprecatedVersion']
+ );
+ }
+ $hookInfo = [ 'deprecatedVersion' => $version ];
+ if ( $component ) {
+ $hookInfo['component'] = $component;
+ }
+ $this->deprecatedHooks[$hook] = $hookInfo;
+ }
+
+ /**
+ * Checks whether hook is marked deprecated
+ * @param string $hook Hook name
+ * @return bool
+ */
+ public function isHookDeprecated( string $hook ) : bool {
+ return isset( $this->deprecatedHooks[$hook] );
+ }
+
+ /**
+ * Gets deprecation info for a specific hook or all hooks if hook not specified
+ * @param string|null $hook (optional) Hook name
+ * @return array|null Value array from $this->deprecatedHooks for a specific hook or all hooks
+ */
+ public function getDeprecationInfo( ?string $hook = null ) : ?array {
+ if ( !$hook ) {
+ return $this->deprecatedHooks;
+ }
+ return $this->deprecatedHooks[$hook] ?? null;
+ }
+}
diff --git a/includes/HookRunner/HookContainer.php b/includes/HookRunner/HookContainer.php
new file mode 100644
index 000000000000..17945e30fee8
--- /dev/null
+++ b/includes/HookRunner/HookContainer.php
@@ -0,0 +1,365 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\HookRunner;
+
+use Closure;
+use ExtensionRegistry;
+use MWException;
+use UnexpectedValueException;
+use Wikimedia\Assert\Assert;
+use Wikimedia\ObjectFactory;
+use Wikimedia\ScopedCallback;
+use Wikimedia\Services\SalvageableService;
+
+/**
+ * HookContainer class.
+ *
+ * Main class for managing hooks
+ *
+ * @since 1.35
+ */
+class HookContainer implements SalvageableService {
+
+ /** @var array Hooks and their callbacks registered through $this->register() */
+ private $legacyRegisteredHandlers;
+
+ /** @var array handler name and their handler objects */
+ private $handlersByName;
+
+ /** @var ExtensionRegistry */
+ private $extensionRegistry;
+
+ /** @var ObjectFactory */
+ private $objectFactory;
+
+ /** @var DeprecatedHooks */
+ private $deprecatedHooks;
+
+ /**
+ * @param ExtensionRegistry $extensionRegistry
+ * @param ObjectFactory $objectFactory
+ * @param DeprecatedHooks $deprecatedHooks
+ */
+ public function __construct(
+ ExtensionRegistry $extensionRegistry,
+ ObjectFactory $objectFactory,
+ DeprecatedHooks $deprecatedHooks
+ ) {
+ $this->extensionRegistry = $extensionRegistry;
+ $this->objectFactory = $objectFactory;
+ $this->deprecatedHooks = $deprecatedHooks;
+ }
+
+ /**
+ * Salvage the state of HookContainer by retaining existing handler objects
+ * and hooks registered via HookContainer::register(). Necessary in the event
+ * that MediaWikiServices::resetGlobalInstance() is called after hooks have already
+ * been registered.
+ *
+ * @param HookContainer|SalvageableService $other The object to salvage state from. $other be
+ * of type HookContainer
+ * @throws MWException
+ */
+ public function salvage( SalvageableService $other ) {
+ Assert::parameterType( self::class, $other, '$other' );
+ if ( $this->legacyRegisteredHandlers ) {
+ throw new MWException( 'salvage() must be called immediately after construction' );
+ }
+ $this->handlersByName = $other->handlersByName ?? [];
+ $this->legacyRegisteredHandlers = $other->legacyRegisteredHandlers ?? [];
+ }
+
+ /**
+ * Call registered hook functions through either the legacy $wgHooks or extension.json
+ *
+ * For the given hook, fetch the array of handler objects and
+ * process them. Determine the proper callback for each hook and
+ * then call the actual hook using the appropriate arguments.
+ * Finally, process the return value and return/throw accordingly.
+ *
+ * For hooks that are not abortable through a handler's return value,
+ * use runWithoutAbort() instead.
+ *
+ * @param string $hook Name of the hook
+ * @param array $args Arguments to pass to hook handler
+ * @param array $options options map:
+ * - abortable: (bool) If false, handlers will not be allowed to abort the call sequenece.
+ * An exception will be raised if a handler returns anything other than true or null.
+ * - deprecatedVersion: (string) Version of MediaWiki this hook was deprecated in. For supporting
+ * Hooks::run() legacy $deprecatedVersion parameter
+ * @return bool True if no handler aborted the hook
+ * @throws UnexpectedValueException if handlers return an invalid value
+ */
+ public function run( string $hook, array $args = [], array $options = [] ) : bool {
+ $legacyHandlers = $this->getLegacyHandlers( $hook );
+ $options = array_merge(
+ $this->deprecatedHooks->getDeprecationInfo( $hook ) ?? [],
+ $options ?? []
+ );
+ // Equivalent of legacy Hooks::runWithoutAbort()
+ $notAbortable = ( isset( $options['abortable'] ) && $options['abortable'] === false );
+ foreach ( $legacyHandlers as $handler ) {
+ $normalizedHandler = $this->normalizeHandler( $handler, $hook );
+ if ( $normalizedHandler ) {
+ $functionName = $normalizedHandler['functionName'];
+ $return = $this->callLegacyHook( $hook, $normalizedHandler, $args, $options );
+ if ( $notAbortable && $return !== null && $return !== true ) {
+ throw new UnexpectedValueException( "Invalid return from $functionName" .
+ " for unabortable $hook." );
+ }
+ if ( $return === false ) {
+ return false;
+ }
+ if ( is_string( $return ) ) {
+ wfDeprecated(
+ "returning a string from a hook handler (done by $functionName for $hook)",
+ '1.35'
+ );
+ throw new UnexpectedValueException( $return );
+ }
+ }
+ }
+
+ $handlers = $this->getHandlers( $hook );
+
+ foreach ( $handlers as $handler ) {
+ $return = $this->callHook( $hook, $handler, $args );
+ $funcName = 'on' . str_replace( ':', '_', ucfirst( $hook ) );
+ if ( $notAbortable && $return !== null && $return !== true ) {
+ throw new UnexpectedValueException(
+ "Invalid return from " . $funcName . " for unabortable $hook."
+ );
+ }
+ if ( $return !== null && !is_bool( $return ) ) {
+ throw new UnexpectedValueException( "Invalid return from " . $funcName . " for $hook." );
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Clear hooks registered via Hooks::register().
+ * This is intended for use while testing and will fail if MW_PHPUNIT_TEST
+ * and MW_PARSER_TEST are not defined.
+ *
+ * @param string $hook Name of hook to clear
+ *
+ * @internal For use by Hooks.php
+ * @throws MWException If not in testing mode.
+ * @codeCoverageIgnore
+ */
+ public function clear( string $hook ) : void {
+ if ( !defined( 'MW_PHPUNIT_TEST' ) && !defined( 'MW_PARSER_TEST' ) ) {
+ throw new MWException( 'Cannot reset hooks in operation.' );
+ }
+ unset( $this->legacyRegisteredHandlers[$hook] ); // dynamically registered legacy handlers
+ }
+
+ /**
+ * Register hook and handler, allowing for easy removal.
+ * Intended for use in temporary registration e.g. testing
+ *
+ * @param string $hook Name of hook
+ * @param callable|string|array $callback handler object to attach
+ * @return ScopedCallback
+ */
+ public function scopedRegister( string $hook, $callback ) : ScopedCallback {
+ $this->legacyRegisteredHandlers[$hook][] = $callback;
+ return new ScopedCallback( function () use ( $hook ) {
+ unset( $this->legacyRegisteredHandlers[$hook] );
+ } );
+ }
+
+ /**
+ * Normalize/clean up format of argument passed as hook handler
+ *
+ * @param array|callable $handler Executable handler function
+ * @param string $hook Hook name
+ * @return array|false
+ * - handler: (callable) Executable handler function
+ * - functionName: (string) Handler name for passing to wfDeprecated() or Exceptions thrown
+ * - args: (array) handler function arguments
+ */
+ private function normalizeHandler( $handler, string $hook ) {
+ $normalizedHandler = $handler;
+ if ( !is_array( $handler ) ) {
+ $normalizedHandler = [ $normalizedHandler ];
+ }
+
+ // Empty array or array filled with null/false/empty.
+ if ( !array_filter( $normalizedHandler ) ) {
+ return false;
+ }
+
+ if ( is_array( $normalizedHandler[0] ) ) {
+ // First element is an array, meaning the developer intended
+ // the first element to be a callback. Merge it in so that
+ // processing can be uniform.
+ $normalizedHandler = array_merge( $normalizedHandler[0], array_slice( $normalizedHandler, 1 ) );
+ }
+
+ $firstArg = $normalizedHandler[0];
+
+ // Extract function name, handler object, and any arguments for handler object
+ if ( $firstArg instanceof Closure ) {
+ $functionName = "hook-$hook-closure";
+ $callback = array_shift( $normalizedHandler );
+ } elseif ( is_object( $firstArg ) ) {
+ $object = array_shift( $normalizedHandler );
+ $functionName = array_shift( $normalizedHandler );
+
+ // If no method was specified, default to on$event
+ if ( $functionName === null ) {
+ $functionName = "on$hook";
+ }
+
+ $callback = [ $object, $functionName ];
+ } elseif ( is_string( $firstArg ) ) {
+ $functionName = $callback = array_shift( $normalizedHandler );
+ } else {
+ throw new UnexpectedValueException( 'Unknown datatype in hooks for ' . $hook );
+ }
+ return [
+ 'callback' => $callback,
+ 'args' => $normalizedHandler,
+ 'functionName' => $functionName,
+ ];
+ }
+
+ /**
+ * Run legacy hooks
+ * Hook can be: a function, an object, an array of $function and
+ * $data, an array of just a function, an array of object and
+ * method, or an array of object, method, and data
+ * (See hooks.txt for more details)
+ *
+ * @param string $hook
+ * @param array|callable $handler The name of the hooks handler function
+ * @param array $args Arguments for hook handler function
+ * @param array $options
+ * @return null|string|bool
+ */
+ private function callLegacyHook( string $hook, $handler, array $args, array $options ) {
+ $callback = $handler['callback'];
+ $hookArgs = array_merge( $handler['args'], $args );
+ if ( isset( $options['deprecatedVersion'] ) ) {
+ wfDeprecated(
+ "$hook hook (used in " . $handler['functionName'] . ")",
+ $options['deprecatedVersion'] ?? false,
+ $options['component'] ?? false
+ );
+ }
+ // Call the hooks
+ return $callback( ...$hookArgs );
+ }
+
+ /**
+ * Run handler function for non-legacy hooks
+ *
+ * @param string $hook
+ * @param object $handler handler object
+ * @param array $args args to provide to handler function
+ * @return null|string|bool handler function return
+ */
+ private function callHook( string $hook, $handler, array $args = [] ) {
+ $funcName = 'on' . str_replace( ':', '_', ucfirst( $hook ) );
+ return $handler->$funcName( ...$args );
+ }
+
+ /**
+ * Return whether hook has any handlers registered to it.
+ * The function may have been registered via Hooks::register or in extension.json
+ *
+ * @param string $hook Name of hook
+ * @return bool Whether the hook has a handler registered to it
+ */
+ public function isRegistered( string $hook ) : bool {
+ global $wgHooks;
+ $legacyRegisteredHook = isset( $wgHooks[$hook] ) ||
+ isset( $this->legacyRegisteredHandlers[$hook] );
+ $registeredHooks = $this->extensionRegistry->getAttribute( 'Hooks' );
+ return isset( $registeredHooks[$hook] ) || $legacyRegisteredHook;
+ }
+
+ /**
+ * Attach an event handler to a given hook.
+ *
+ * @param string $hook Name of hook
+ * @param callable|string|array $callback handler object to attach
+ */
+ public function register( string $hook, $callback ) {
+ $deprecated = $this->deprecatedHooks->isHookDeprecated( $hook );
+ if ( $deprecated ) {
+ $deprecatedVersion = $this->deprecatedHooks->getDeprecationInfo( $hook )['deprecatedVersion']
+ ?? false;
+ $component = $this->deprecatedHooks->getDeprecationInfo( $hook )['component'] ?? false;
+ wfDeprecated(
+ "$hook hook", $deprecatedVersion, $component
+ );
+ }
+ $this->legacyRegisteredHandlers[$hook][] = $callback;
+ }
+
+ /**
+ * Get all handlers for legacy hooks system
+ *
+ * @internal For use by Hooks.php
+ * @param string $hook Name of hook
+ * @return array function names
+ */
+ public function getLegacyHandlers( string $hook ) : array {
+ global $wgHooks;
+ $handlers = array_merge(
+ $this->legacyRegisteredHandlers[$hook] ?? [],
+ $wgHooks[$hook] ?? []
+ );
+ return $handlers;
+ }
+
+ /**
+ * Return array of handler objects registered with given hook in the new system
+ * @internal For use by Hooks.php
+ * @param string $hook Name of the hook
+ * @return array non-deprecated handler objects
+ */
+ public function getHandlers( string $hook ) : array {
+ $handlers = [];
+ $registeredHooks = $this->extensionRegistry->getAttribute( 'Hooks' );
+ if ( isset( $registeredHooks[$hook] ) ) {
+ foreach ( $registeredHooks[$hook] as $hookReference ) {
+ // Non-legacy hooks have handler attributes
+ $handlerObject = $hookReference['handler'];
+ // Skip hooks that both acknowledge deprecation and are deprecated in core
+ $flaggedDeprecated = !empty( $hookReference['deprecated'] );
+ $deprecated = $this->deprecatedHooks->isHookDeprecated( $hook );
+ if ( $deprecated && $flaggedDeprecated ) {
+ continue;
+ }
+ $handlerName = $handlerObject['name'];
+ if ( !isset( $this->handlersByName[$handlerName] ) ) {
+ $this->handlersByName[$handlerName] = $this->objectFactory->createObject( $handlerObject );
+ }
+ $handlers[] = $this->handlersByName[$handlerName];
+ }
+ }
+ return $handlers;
+ }
+}
diff --git a/includes/Hooks.php b/includes/Hooks.php
index 9a2b0342477a..099940e65d45 100644
--- a/includes/Hooks.php
+++ b/includes/Hooks.php
@@ -24,30 +24,33 @@
* @file
*/
+use MediaWiki\MediaWikiServices;
+
/**
* Hooks class.
*
- * Used to supersede $wgHooks, because globals are EVIL.
+ * Legacy wrapper for HookContainer
+ * Please use HookContainer instead.
*
* @since 1.18
*/
class Hooks {
- /**
- * Array of events mapped to an array of callbacks to be run
- * when that event is triggered.
- */
- protected static $handlers = [];
/**
- * Attach an event handler to a given hook.
+ * Attach an event handler to a given hook in both legacy and non-legacy hook systems
*
* @param string $name Name of hook
* @param callable $callback Callback function to attach
- *
+ * @deprecated since 1.35. use HookContainer::register() instead
* @since 1.18
*/
public static function register( $name, $callback ) {
- self::$handlers[$name][] = $callback;
+ if ( !defined( 'MW_SERVICE_BOOTSTRAP_COMPLETE' ) ) {
+ wfDeprecated( 'Registering handler for ' . $name .
+ ' before MediaWiki bootstrap complete', '1.35' );
+ }
+ $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
+ $hookContainer->register( $name, $callback );
}
/**
@@ -57,15 +60,18 @@ class Hooks {
* @param string $name The name of the hook to clear.
*
* @since 1.21
+ * @deprecated since 1.35. Instead of using Hooks::register() and Hooks::clear(),
+ * use HookContainer::scopedRegister() instead to register a temporary hook
* @throws MWException If not in testing mode.
* @codeCoverageIgnore
*/
public static function clear( $name ) {
+ wfDeprecated( __METHOD__, '1.35' );
if ( !defined( 'MW_PHPUNIT_TEST' ) && !defined( 'MW_PARSER_TEST' ) ) {
throw new MWException( 'Cannot reset hooks in operation.' );
}
-
- unset( self::$handlers[$name] );
+ $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
+ $hookContainer->clear( $name );
}
/**
@@ -73,13 +79,13 @@ class Hooks {
* The function may have been registered either via Hooks::register or in $wgHooks.
*
* @since 1.18
- *
+ * @deprecated since 1.35. use HookContainer::isRegistered() instead
* @param string $name Name of hook
* @return bool True if the hook has a function registered to it
*/
public static function isRegistered( $name ) {
- global $wgHooks;
- return !empty( $wgHooks[$name] ) || !empty( self::$handlers[$name] );
+ $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
+ return $hookContainer->isRegistered( $name );
}
/**
@@ -87,91 +93,18 @@ class Hooks {
* This combines functions registered via Hooks::register and with $wgHooks.
*
* @since 1.18
- *
+ * @deprecated since 1.35
* @param string $name Name of the hook
* @return array
*/
public static function getHandlers( $name ) {
- global $wgHooks;
-
- if ( !self::isRegistered( $name ) ) {
- return [];
- } elseif ( !isset( self::$handlers[$name] ) ) {
- return $wgHooks[$name];
- } elseif ( !isset( $wgHooks[$name] ) ) {
- return self::$handlers[$name];
- } else {
- return array_merge( self::$handlers[$name], $wgHooks[$name] );
+ $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
+ $handlers = $hookContainer->getLegacyHandlers( $name );
+ $funcName = 'on' . str_replace( ':', '_', ucfirst( $name ) );
+ foreach ( $hookContainer->getHandlers( $name ) as $obj ) {
+ $handlers[] = [ $obj, $funcName ];
}
- }
-
- /**
- * @param string $event Event name
- * @param array|callable $hook
- * @param array $args Array of parameters passed to hook functions
- * @param string|null $deprecatedVersion [optional]
- * @param string|null &$fname [optional] Readable name of hook [returned]
- * @return null|string|bool
- */
- private static function callHook( $event, $hook, array $args, $deprecatedVersion = null,
- &$fname = null
- ) {
- // Turn non-array values into an array. (Can't use casting because of objects.)
- if ( !is_array( $hook ) ) {
- $hook = [ $hook ];
- }
-
- if ( !array_filter( $hook ) ) {
- // Either array is empty or it's an array filled with null/false/empty.
- return null;
- }
-
- if ( is_array( $hook[0] ) ) {
- // First element is an array, meaning the developer intended
- // the first element to be a callback. Merge it in so that
- // processing can be uniform.
- $hook = array_merge( $hook[0], array_slice( $hook, 1 ) );
- }
-
- /**
- * $hook can be: a function, an object, an array of $function and
- * $data, an array of just a function, an array of object and
- * method, or an array of object, method, and data.
- */
- if ( $hook[0] instanceof Closure ) {
- $fname = "hook-$event-closure";
- $callback = array_shift( $hook );
- } elseif ( is_object( $hook[0] ) ) {
- $object = array_shift( $hook );
- $method = array_shift( $hook );
-
- // If no method was specified, default to on$event.
- if ( $method === null ) {
- $method = "on$event";
- }
-
- $fname = get_class( $object ) . '::' . $method;
- $callback = [ $object, $method ];
- } elseif ( is_string( $hook[0] ) ) {
- $fname = $callback = array_shift( $hook );
- } else {
- throw new MWException( 'Unknown datatype in hooks for ' . $event . "\n" );
- }
-
- // Run autoloader (workaround for call_user_func_array bug)
- // and throw error if not callable.
- if ( !is_callable( $callback ) ) {
- throw new MWException( 'Invalid callback ' . $fname . ' in hooks for ' . $event . "\n" );
- }
-
- // mark hook as deprecated, if deprecation version is specified
- if ( $deprecatedVersion !== null ) {
- wfDeprecated( "$event hook (used in $fname)", $deprecatedVersion );
- }
-
- // Call the hook.
- $hook_args = array_merge( $hook, $args );
- return call_user_func_array( $callback, $hook_args );
+ return $handlers;
}
/**
@@ -191,30 +124,15 @@ class Hooks {
* @return bool True if no handler aborted the hook
*
* @throws Exception
- * @throws FatalError
- * @throws MWException
* @since 1.22 A hook function is not required to return a value for
* processing to continue. Not returning a value (or explicitly
* returning null) is equivalent to returning true.
+ * @deprecated since 1.35 Use HookContainer::run() instead
*/
public static function run( $event, array $args = [], $deprecatedVersion = null ) {
- foreach ( self::getHandlers( $event ) as $hook ) {
- $retval = self::callHook( $event, $hook, $args, $deprecatedVersion );
- if ( $retval === null ) {
- continue;
- }
-
- // Process the return value.
- if ( is_string( $retval ) ) {
- // String returned means error.
- throw new FatalError( $retval );
- } elseif ( $retval === false ) {
- // False was returned. Stop processing, but no error.
- return false;
- }
- }
-
- return true;
+ $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
+ $options = $deprecatedVersion ? [ 'deprecatedVersion' => $deprecatedVersion ] : [];
+ return $hookContainer->run( $event, $args, $options );
}
/**
@@ -224,18 +142,14 @@ class Hooks {
* @param array $args Array of parameters passed to hook functions
* @param string|null $deprecatedVersion [optional] Mark hook as deprecated with version number
* @return bool Always true
- * @throws MWException If a callback is invalid, unknown
- * @throws UnexpectedValueException If a callback returns an abort value.
+ * @throws UnexpectedValueException callback returns an invalid value
* @since 1.30
+ * @deprecated since 1.35 Use HookContainer::run() with 'abortable' option instead
*/
public static function runWithoutAbort( $event, array $args = [], $deprecatedVersion = null ) {
- foreach ( self::getHandlers( $event ) as $hook ) {
- $fname = null;
- $retval = self::callHook( $event, $hook, $args, $deprecatedVersion, $fname );
- if ( $retval !== null && $retval !== true ) {
- throw new UnexpectedValueException( "Invalid return from $fname for unabortable $event." );
- }
- }
- return true;
+ $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
+ $options = $deprecatedVersion ? [ 'deprecatedVersion' => $deprecatedVersion ] : [];
+ $options[ 'abortable' ] = false;
+ return $hookContainer->run( $event, $args, $options );
}
}
diff --git a/includes/MediaWikiServices.php b/includes/MediaWikiServices.php
index 4ea81b1fab1d..0138192ca4d2 100644
--- a/includes/MediaWikiServices.php
+++ b/includes/MediaWikiServices.php
@@ -35,6 +35,7 @@ use MediaWiki\Config\ConfigRepository;
use MediaWiki\Content\IContentHandlerFactory;
use MediaWiki\FileBackend\FSFile\TempFSFileFactory;
use MediaWiki\FileBackend\LockManager\LockManagerGroupFactory;
+use MediaWiki\HookRunner\HookContainer;
use MediaWiki\Http\HttpRequestFactory;
use MediaWiki\Interwiki\InterwikiLookup;
use MediaWiki\Languages\LanguageConverterFactory;
@@ -174,8 +175,11 @@ class MediaWikiServices extends ServiceContainer {
// configuration from.
$bootstrapConfig = new GlobalVarConfig();
self::$instance = self::newInstance( $bootstrapConfig, 'load' );
- }
+ // Provides a traditional hook point to allow extensions to configure services.
+ // NOTE: Ideally this would be in newInstance() but it causes an infinite run loop
+ Hooks::run( 'MediaWikiServices', [ self::$instance ] );
+ }
return self::$instance;
}
@@ -257,6 +261,10 @@ class MediaWikiServices extends ServiceContainer {
$oldInstance = self::$instance;
self::$instance = self::newInstance( $bootstrapConfig, 'load' );
+
+ // Provides a traditional hook point to allow extensions to configure services.
+ Hooks::run( 'MediaWikiServices', [ self::$instance ] );
+
self::$instance->importWiring( $oldInstance, [ 'BootstrapConfig' ] );
if ( $quick === 'quick' ) {
@@ -320,9 +328,6 @@ class MediaWikiServices extends ServiceContainer {
$instance->loadWiringFiles( $wiringFiles );
}
- // Provide a traditional hook point to allow extensions to configure services.
- Hooks::run( 'MediaWikiServices', [ $instance ] );
-
return $instance;
}
@@ -692,6 +697,13 @@ class MediaWikiServices extends ServiceContainer {
}
/**
+ * @return HookContainer
+ */
+ public function getHookContainer() : HookContainer {
+ return $this->getService( 'HookContainer' );
+ }
+
+ /**
* @since 1.35
* @return HtmlCacheUpdater
*/
diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php
index a954eb903a93..be44e852608e 100644
--- a/includes/ServiceWiring.php
+++ b/includes/ServiceWiring.php
@@ -57,6 +57,8 @@ use MediaWiki\Content\ContentHandlerFactory;
use MediaWiki\Content\IContentHandlerFactory;
use MediaWiki\FileBackend\FSFile\TempFSFileFactory;
use MediaWiki\FileBackend\LockManager\LockManagerGroupFactory;
+use MediaWiki\HookRunner\DeprecatedHooks;
+use MediaWiki\HookRunner\HookContainer;
use MediaWiki\Http\HttpRequestFactory;
use MediaWiki\Interwiki\ClassicInterwikiLookup;
use MediaWiki\Interwiki\InterwikiLookup;
@@ -340,6 +342,17 @@ return [
);
},
+ 'HookContainer' => function ( MediaWikiServices $services ) : HookContainer {
+ $extRegistry = ExtensionRegistry::getInstance();
+ $extDeprecatedHooks = $extRegistry->getAttribute( 'DeprecatedHooks' );
+ $deprecatedHooks = new DeprecatedHooks( $extDeprecatedHooks );
+ return new HookContainer(
+ $extRegistry,
+ $services->getObjectFactory(),
+ $deprecatedHooks
+ );
+ },
+
'HtmlCacheUpdater' => function ( MediaWikiServices $services ) : HtmlCacheUpdater {
$config = $services->getMainConfig();
diff --git a/includes/registration/ExtensionProcessor.php b/includes/registration/ExtensionProcessor.php
index 7a085ba8c11f..ea8ea216fef2 100644
--- a/includes/registration/ExtensionProcessor.php
+++ b/includes/registration/ExtensionProcessor.php
@@ -1,5 +1,7 @@
<?php
+use MediaWiki\HookRunner\DeprecatedHooks;
+
class ExtensionProcessor implements Processor {
/**
@@ -196,7 +198,7 @@ class ExtensionProcessor implements Processor {
*/
public function extractInfo( $path, array $info, $version ) {
$dir = dirname( $path );
- $this->extractHooks( $info );
+ $this->extractHooks( $info, $path );
$this->extractExtensionMessagesFiles( $dir, $info );
$this->extractMessagesDirs( $dir, $info );
$this->extractNamespaces( $info );
@@ -258,7 +260,6 @@ class ExtensionProcessor implements Processor {
$this->storeToArray( $path, $key, $val, $this->attributes );
}
}
-
}
}
@@ -274,7 +275,39 @@ class ExtensionProcessor implements Processor {
}
}
+ /**
+ * Will throw wfDeprecated() warning if:
+ * 1. an extension does not acknowledge deprecation and
+ * 2. is marked deprecated
+ */
+ private function emitDeprecatedHookWarnings() {
+ if ( !isset( $this->attributes['Hooks'] ) ) {
+ return;
+ }
+ $extDeprecatedHooks = $this->attributes['DeprecatedHooks'] ?? false;
+ if ( !$extDeprecatedHooks ) {
+ return;
+ }
+ $deprecatedHooks = new DeprecatedHooks( $extDeprecatedHooks );
+ foreach ( $this->attributes['Hooks'] as $name => $handlers ) {
+ if ( $deprecatedHooks->isHookDeprecated( $name ) ) {
+ $deprecationInfo = $deprecatedHooks->getDeprecationInfo( $name );
+ foreach ( $handlers as $handler ) {
+ if ( !isset( $handler['deprecated'] ) || !$handler['deprecated'] ) {
+ wfDeprecated(
+ "$name hook",
+ $deprecationInfo['deprecatedVersion'] ?? false,
+ $deprecationInfo['component'] ?? false
+ );
+ }
+ }
+ }
+ }
+ }
+
public function getExtractedInfo() {
+ $this->emitDeprecatedHookWarnings();
+
// Make sure the merge strategies are set
foreach ( $this->globals as $key => $val ) {
if ( isset( self::MERGE_STRATEGIES[$key] ) ) {
@@ -376,25 +409,99 @@ class ExtensionProcessor implements Processor {
);
}
}
-
return $merged;
}
- protected function extractHooks( array $info ) {
- if ( isset( $info['Hooks'] ) ) {
- foreach ( $info['Hooks'] as $name => $value ) {
- if ( is_array( $value ) ) {
- foreach ( $value as $callback ) {
- $this->globals['wgHooks'][$name][] = $callback;
+ /**
+ * When handler value is an array, set $wgHooks or Hooks attribute
+ * Could be legacy hook e.g. 'GlobalFunctionName' or non-legacy hook
+ * referencing a handler definition from 'HookHandler' attribute
+ *
+ * @param array $callback Handler
+ * @param array $hookHandlersAttr handler definitions from 'HookHandler' attribute
+ * @param string $name
+ * @param string $path extension.json file path
+ * @throws UnexpectedValueException
+ */
+ private function setArrayHookHandler(
+ array $callback,
+ array $hookHandlersAttr,
+ string $name,
+ string $path
+ ) {
+ if ( isset( $callback['handler'] ) ) {
+ $handlerName = $callback['handler'];
+ $handlerDefinition = $hookHandlersAttr[$handlerName] ?? false;
+ if ( !$handlerDefinition ) {
+ throw new UnexpectedValueException(
+ "Missing handler definition for $name in HookHandlers attribute in $path"
+ );
+ }
+ $callback['handler'] = $handlerDefinition;
+ $this->attributes['Hooks'][$name][] = $callback;
+ } else {
+ foreach ( $callback as $callable ) {
+ if ( is_array( $callable ) ) {
+ if ( isset( $callable['handler'] ) ) { // Non-legacy style handler
+ $this->setArrayHookHandler( $callable, $hookHandlersAttr, $name, $path );
+ } else { // Legacy style handler array
+ $this->globals['wgHooks'][$name][] = $callable;
}
- } else {
- $this->globals['wgHooks'][$name][] = $value;
+ } elseif ( is_string( $callable ) ) {
+ $this->setStringHookHandler( $callable, $hookHandlersAttr, $name );
}
}
}
}
/**
+ * When handler value is a string, set $wgHooks or Hooks attribute.
+ * Could be legacy hook e.g. 'GlobalFunctionName' or non-legacy hook
+ * referencing a handler definition from 'HookHandler' attribute
+ *
+ * @param string $callback Handler
+ * @param array $hookHandlersAttr handler definitions from 'HookHandler' attribute
+ * @param string $name
+ */
+ private function setStringHookHandler(
+ string $callback,
+ array $hookHandlersAttr,
+ string $name
+ ) {
+ if ( isset( $hookHandlersAttr[$callback] ) ) {
+ $handler = [ 'handler' => $hookHandlersAttr[$callback] ];
+ $this->attributes['Hooks'][$name][] = $handler;
+ } else { // legacy style handler
+ $this->globals['wgHooks'][$name][] = $callback;
+ }
+ }
+
+ /**
+ * Extract hook information from Hooks and HookHandler attributes.
+ * Store hook in $wgHooks if a legacy style handler or the 'Hooks' attribute if
+ * a non-legacy handler
+ *
+ * @param array $info attributes and associated values from extension.json
+ * @param string $path path to extension.json
+ */
+ protected function extractHooks( array $info, string $path ) {
+ if ( !isset( $info['Hooks'] ) ) {
+ return;
+ }
+ $hookHandlersAttr = [];
+ foreach ( $info['HookHandlers'] ?? [] as $name => $def ) {
+ $hookHandlersAttr[$name] = [ 'name' => "$path-$name" ] + $def;
+ }
+ foreach ( $info['Hooks'] as $name => $callback ) {
+ if ( is_string( $callback ) ) {
+ $this->setStringHookHandler( $callback, $hookHandlersAttr, $name );
+ } elseif ( is_array( $callback ) ) {
+ $this->setArrayHookHandler( $callback, $hookHandlersAttr, $name, $path );
+ }
+ }
+ }
+
+ /**
* Register namespaces with the appropriate global settings
*
* @param array $info