MediaWiki  master
HookContainer.php
Go to the documentation of this file.
1 <?php
26 namespace MediaWiki\HookContainer;
27 
28 use Closure;
29 use MWDebug;
30 use MWException;
31 use UnexpectedValueException;
32 use Wikimedia\Assert\Assert;
33 use Wikimedia\ObjectFactory;
34 use Wikimedia\ScopedCallback;
36 
45 
48 
50  private $handlersByName = [];
51 
53  private $registry;
54 
56  private $objectFactory;
57 
59  private $nextScopedRegisterId = 0;
60 
62  private $originalHooks;
63 
68  public function __construct(
69  HookRegistry $hookRegistry,
70  ObjectFactory $objectFactory
71  ) {
72  $this->registry = $hookRegistry;
73  $this->objectFactory = $objectFactory;
74  }
75 
86  public function salvage( SalvageableService $other ) {
87  Assert::parameterType( self::class, $other, '$other' );
88  if ( $this->legacyRegisteredHandlers || $this->handlersByName ) {
89  throw new MWException( 'salvage() must be called immediately after construction' );
90  }
91  $this->handlersByName = $other->handlersByName;
92  $this->legacyRegisteredHandlers = $other->legacyRegisteredHandlers;
93  }
94 
119  public function run( string $hook, array $args = [], array $options = [] ) : bool {
120  $legacyHandlers = $this->getLegacyHandlers( $hook );
121  $options = array_merge(
122  $this->registry->getDeprecatedHooks()->getDeprecationInfo( $hook ) ?? [],
123  $options
124  );
125  // Equivalent of legacy Hooks::runWithoutAbort()
126  $notAbortable = ( isset( $options['abortable'] ) && $options['abortable'] === false );
127  foreach ( $legacyHandlers as $handler ) {
128  $normalizedHandler = $this->normalizeHandler( $handler, $hook );
129  if ( $normalizedHandler ) {
130  $functionName = $normalizedHandler['functionName'];
131  $return = $this->callLegacyHook( $hook, $normalizedHandler, $args, $options );
132  if ( $notAbortable && $return !== null && $return !== true ) {
133  throw new UnexpectedValueException( "Invalid return from $functionName" .
134  " for unabortable $hook." );
135  }
136  if ( $return === false ) {
137  return false;
138  }
139  if ( is_string( $return ) ) {
140  wfDeprecated(
141  "returning a string from a hook handler (done by $functionName for $hook)",
142  '1.35'
143  );
144  throw new UnexpectedValueException( $return );
145  }
146  }
147  }
148 
149  $handlers = $this->getHandlers( $hook );
150  $funcName = 'on' . str_replace( ':', '_', ucfirst( $hook ) );
151 
152  foreach ( $handlers as $handler ) {
153  $return = $handler->$funcName( ...$args );
154  if ( $notAbortable && $return !== null && $return !== true ) {
155  throw new UnexpectedValueException(
156  "Invalid return from " . $funcName . " for unabortable $hook."
157  );
158  }
159  if ( $return === false ) {
160  return false;
161  }
162  if ( $return !== null && !is_bool( $return ) ) {
163  throw new UnexpectedValueException( "Invalid return from " . $funcName . " for $hook." );
164  }
165  }
166  return true;
167  }
168 
180  public function clear( string $hook ) : void {
181  if ( !defined( 'MW_PHPUNIT_TEST' ) && !defined( 'MW_PARSER_TEST' ) ) {
182  throw new MWException( 'Cannot reset hooks in operation.' );
183  }
184  unset( $this->legacyRegisteredHandlers[$hook] ); // dynamically registered legacy handlers
185  }
186 
197  public function scopedRegister( string $hook, $callback, bool $replace = false ) : ScopedCallback {
198  if ( $replace ) {
199  // Stash any previously registered hooks
200  if ( !isset( $this->originalHooks[$hook] ) &&
201  isset( $this->legacyRegisteredHandlers[$hook] )
202  ) {
203  $this->originalHooks[$hook] = $this->legacyRegisteredHandlers[$hook];
204  }
205  $this->legacyRegisteredHandlers[$hook] = [ $callback ];
206  return new ScopedCallback( function () use ( $hook ) {
207  unset( $this->legacyRegisteredHandlers[$hook] );
208  } );
209  }
210  $id = $this->nextScopedRegisterId++;
211  $this->legacyRegisteredHandlers[$hook][$id] = $callback;
212  return new ScopedCallback( function () use ( $hook, $id ) {
213  unset( $this->legacyRegisteredHandlers[$hook][$id] );
214  } );
215  }
216 
224  public function getOriginalHooksForTest() {
225  if ( !defined( 'MW_PHPUNIT_TEST' ) && !defined( 'MW_PARSER_TEST' ) ) {
226  throw new MWException( 'Cannot get original hooks outside when not in test mode' );
227  }
228  return $this->originalHooks ?? [];
229  }
230 
241  private function normalizeHandler( $handler, string $hook ) {
242  $normalizedHandler = $handler;
243  if ( !is_array( $handler ) ) {
244  $normalizedHandler = [ $normalizedHandler ];
245  }
246 
247  // Empty array or array filled with null/false/empty.
248  if ( !array_filter( $normalizedHandler ) ) {
249  return false;
250  }
251 
252  if ( is_array( $normalizedHandler[0] ) ) {
253  // First element is an array, meaning the developer intended
254  // the first element to be a callback. Merge it in so that
255  // processing can be uniform.
256  $normalizedHandler = array_merge( $normalizedHandler[0], array_slice( $normalizedHandler, 1 ) );
257  }
258 
259  $firstArg = $normalizedHandler[0];
260 
261  // Extract function name, handler object, and any arguments for handler object
262  if ( $firstArg instanceof Closure ) {
263  $functionName = "hook-$hook-closure";
264  $callback = array_shift( $normalizedHandler );
265  } elseif ( is_object( $firstArg ) ) {
266  $object = array_shift( $normalizedHandler );
267  $functionName = array_shift( $normalizedHandler );
268 
269  // If no method was specified, default to on$event
270  if ( $functionName === null ) {
271  $functionName = "on$hook";
272  } else {
273  $colonPos = strpos( $functionName, '::' );
274  if ( $colonPos !== false ) {
275  // Some extensions use [ $object, 'Class::func' ] which
276  // worked with call_user_func_array() but doesn't work now
277  // that we use a plain varadic call
278  $functionName = substr( $functionName, $colonPos + 2 );
279  }
280  }
281 
282  $callback = [ $object, $functionName ];
283  } elseif ( is_string( $firstArg ) ) {
284  $functionName = $callback = array_shift( $normalizedHandler );
285  } else {
286  throw new UnexpectedValueException( 'Unknown datatype in hooks for ' . $hook );
287  }
288  return [
289  'callback' => $callback,
290  'args' => $normalizedHandler,
291  'functionName' => $functionName,
292  ];
293  }
294 
308  private function callLegacyHook( string $hook, $handler, array $args, array $options ) {
309  $callback = $handler['callback'];
310  $hookArgs = array_merge( $handler['args'], $args );
311  if ( isset( $options['deprecatedVersion'] ) && empty( $options['silent'] ) ) {
312  wfDeprecated(
313  "$hook hook (used in " . $handler['functionName'] . ")",
314  $options['deprecatedVersion'] ?? false,
315  $options['component'] ?? false
316  );
317  }
318  // Call the hooks
319  return $callback( ...$hookArgs );
320  }
321 
329  public function isRegistered( string $hook ) : bool {
330  $legacyRegisteredHook = !empty( $this->registry->getGlobalHooks()[$hook] ) ||
331  !empty( $this->legacyRegisteredHandlers[$hook] );
332  $registeredHooks = $this->registry->getExtensionHooks();
333  return !empty( $registeredHooks[$hook] ) || $legacyRegisteredHook;
334  }
335 
342  public function register( string $hook, $callback ) {
343  $deprecatedHooks = $this->registry->getDeprecatedHooks();
344  $deprecated = $deprecatedHooks->isHookDeprecated( $hook );
345  if ( $deprecated ) {
346  $info = $deprecatedHooks->getDeprecationInfo( $hook );
347  if ( empty( $info['silent'] ) ) {
348  $deprecatedVersion = $info['deprecatedVersion'] ?? false;
349  $component = $info['component'] ?? false;
350  wfDeprecated(
351  "$hook hook", $deprecatedVersion, $component
352  );
353  }
354  }
355  $this->legacyRegisteredHandlers[$hook][] = $callback;
356  }
357 
365  public function getLegacyHandlers( string $hook ) : array {
366  $handlers = array_merge(
367  $this->legacyRegisteredHandlers[$hook] ?? [],
368  $this->registry->getGlobalHooks()[$hook] ?? []
369  );
370  return $handlers;
371  }
372 
379  public function getHandlers( string $hook ) : array {
380  $handlers = [];
381  $deprecatedHooks = $this->registry->getDeprecatedHooks();
382  $registeredHooks = $this->registry->getExtensionHooks();
383  if ( isset( $registeredHooks[$hook] ) ) {
384  foreach ( $registeredHooks[$hook] as $hookReference ) {
385  // Non-legacy hooks have handler attributes
386  $handlerObject = $hookReference['handler'];
387  // Skip hooks that both acknowledge deprecation and are deprecated in core
388  $flaggedDeprecated = !empty( $hookReference['deprecated'] );
389  $deprecated = $deprecatedHooks->isHookDeprecated( $hook );
390  if ( $deprecated && $flaggedDeprecated ) {
391  continue;
392  }
393  $handlerName = $handlerObject['name'];
394  if ( !isset( $this->handlersByName[$handlerName] ) ) {
395  $this->handlersByName[$handlerName] =
396  $this->objectFactory->createObject( $handlerObject );
397  }
398  $handlers[] = $this->handlersByName[$handlerName];
399  }
400  }
401  return $handlers;
402  }
403 
410  public function emitDeprecationWarnings() {
411  $deprecatedHooks = $this->registry->getDeprecatedHooks();
412  $registeredHooks = $this->registry->getExtensionHooks();
413  foreach ( $registeredHooks as $name => $handlers ) {
414  if ( $deprecatedHooks->isHookDeprecated( $name ) ) {
415  $deprecationInfo = $deprecatedHooks->getDeprecationInfo( $name );
416  if ( !empty( $deprecationInfo['silent'] ) ) {
417  continue;
418  }
419  $version = $deprecationInfo['deprecatedVersion'] ?? '';
420  $component = $deprecationInfo['component'] ?? 'MediaWiki';
421  foreach ( $handlers as $handler ) {
422  if ( !isset( $handler['deprecated'] ) || !$handler['deprecated'] ) {
424  "Hook $name was deprecated in $component $version " .
425  "but is registered in " . $handler['extensionPath']
426  );
427  }
428  }
429  }
430  }
431  }
432 }
MediaWiki\HookContainer\HookContainer\callLegacyHook
callLegacyHook(string $hook, $handler, array $args, array $options)
Run legacy hooks Hook can be: a function, an object, an array of $function and $data,...
Definition: HookContainer.php:308
MediaWiki\HookContainer\HookContainer\$handlersByName
array $handlersByName
handler name and their handler objects
Definition: HookContainer.php:50
MediaWiki\HookContainer\HookContainer\emitDeprecationWarnings
emitDeprecationWarnings()
Will log a deprecation warning if:
Definition: HookContainer.php:410
MediaWiki\HookContainer\HookContainer\getHandlers
getHandlers(string $hook)
Return array of handler objects registered with given hook in the new system.
Definition: HookContainer.php:379
MediaWiki\HookContainer\HookContainer\isRegistered
isRegistered(string $hook)
Return whether hook has any handlers registered to it.
Definition: HookContainer.php:329
MWDebug
New debugger system that outputs a toolbar on page view.
Definition: MWDebug.php:33
MediaWiki\HookContainer\HookContainer\$objectFactory
ObjectFactory $objectFactory
Definition: HookContainer.php:56
MediaWiki\HookContainer\HookRegistry
Definition: HookRegistry.php:5
MediaWiki\HookContainer\HookContainer\normalizeHandler
normalizeHandler( $handler, string $hook)
Normalize/clean up format of argument passed as hook handler.
Definition: HookContainer.php:241
MediaWiki\HookContainer\HookContainer\getLegacyHandlers
getLegacyHandlers(string $hook)
Get all handlers for legacy hooks system.
Definition: HookContainer.php:365
MWException
MediaWiki exception.
Definition: MWException.php:26
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1030
MediaWiki\HookContainer\HookContainer\__construct
__construct(HookRegistry $hookRegistry, ObjectFactory $objectFactory)
Definition: HookContainer.php:68
MediaWiki\HookContainer\HookContainer\scopedRegister
scopedRegister(string $hook, $callback, bool $replace=false)
Register hook and handler, allowing for easy removal.
Definition: HookContainer.php:197
$args
if( $line===false) $args
Definition: mcc.php:124
MediaWiki\HookContainer\HookContainer\getOriginalHooksForTest
getOriginalHooksForTest()
Return hooks that were set before being potentially overridden by scopedRegister().
Definition: HookContainer.php:224
MediaWiki\HookContainer\HookContainer\$registry
HookRegistry $registry
Definition: HookContainer.php:53
MediaWiki\HookContainer
Definition: DeprecatedHooks.php:23
Wikimedia\Services\SalvageableService
SalvageableService defines an interface for services that are able to salvage state from a previous i...
Definition: SalvageableService.php:36
MediaWiki\HookContainer\HookContainer\$nextScopedRegisterId
int $nextScopedRegisterId
The next ID to be used by scopedRegister()
Definition: HookContainer.php:59
MediaWiki\HookContainer\HookContainer\$legacyRegisteredHandlers
array $legacyRegisteredHandlers
Hooks and their callbacks registered through $this->register()
Definition: HookContainer.php:47
MediaWiki\HookContainer\HookContainer\clear
clear(string $hook)
Clear hooks registered via Hooks::register().
Definition: HookContainer.php:180
MediaWiki\HookContainer\HookContainer\run
run(string $hook, array $args=[], array $options=[])
Call registered hook functions through either the legacy $wgHooks or extension.json.
Definition: HookContainer.php:119
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:44
MediaWiki\HookContainer\HookContainer\salvage
salvage(SalvageableService $other)
Salvage the state of HookContainer by retaining existing handler objects and hooks registered via Hoo...
Definition: HookContainer.php:86
MWDebug\sendRawDeprecated
static sendRawDeprecated( $msg, $sendToLog=true, $callerFunc='')
Send a raw deprecation message to the log and the debug toolbar, without filtering of duplicate messa...
Definition: MWDebug.php:285
MediaWiki\HookContainer\HookContainer\$originalHooks
array $originalHooks
existing hook names and their handlers to restore between tests
Definition: HookContainer.php:62