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;
35 use Wikimedia\Services\SalvageableService;
36 
44 class HookContainer implements SalvageableService {
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 ) ) {
141  "Returning a string from a hook handler is deprecated since MediaWiki 1.35 ' .
142  '(done by $functionName for $hook)",
143  '1.35', false, false
144  );
145  throw new UnexpectedValueException( $return );
146  }
147  }
148  }
149 
150  $handlers = $this->getHandlers( $hook );
151  $funcName = 'on' . str_replace( ':', '_', ucfirst( $hook ) );
152 
153  foreach ( $handlers as $handler ) {
154  $return = $handler->$funcName( ...$args );
155  if ( $notAbortable && $return !== null && $return !== true ) {
156  throw new UnexpectedValueException(
157  "Invalid return from " . $funcName . " for unabortable $hook."
158  );
159  }
160  if ( $return === false ) {
161  return false;
162  }
163  if ( $return !== null && !is_bool( $return ) ) {
164  throw new UnexpectedValueException( "Invalid return from " . $funcName . " for $hook." );
165  }
166  }
167  return true;
168  }
169 
181  public function clear( string $hook ) : void {
182  if ( !defined( 'MW_PHPUNIT_TEST' ) && !defined( 'MW_PARSER_TEST' ) ) {
183  throw new MWException( 'Cannot reset hooks in operation.' );
184  }
185  unset( $this->legacyRegisteredHandlers[$hook] ); // dynamically registered legacy handlers
186  }
187 
198  public function scopedRegister( string $hook, $callback, bool $replace = false ) : ScopedCallback {
199  if ( $replace ) {
200  // Stash any previously registered hooks
201  if ( !isset( $this->originalHooks[$hook] ) &&
202  isset( $this->legacyRegisteredHandlers[$hook] )
203  ) {
204  $this->originalHooks[$hook] = $this->legacyRegisteredHandlers[$hook];
205  }
206  $this->legacyRegisteredHandlers[$hook] = [ $callback ];
207  return new ScopedCallback( function () use ( $hook ) {
208  unset( $this->legacyRegisteredHandlers[$hook] );
209  } );
210  }
211  $id = 'TemporaryHook_' . $this->nextScopedRegisterId++;
212  $this->legacyRegisteredHandlers[$hook][$id] = $callback;
213  return new ScopedCallback( function () use ( $hook, $id ) {
214  unset( $this->legacyRegisteredHandlers[$hook][$id] );
215  } );
216  }
217 
225  public function getOriginalHooksForTest() {
226  if ( !defined( 'MW_PHPUNIT_TEST' ) && !defined( 'MW_PARSER_TEST' ) ) {
227  throw new MWException( 'Cannot get original hooks outside when not in test mode' );
228  }
229  return $this->originalHooks ?? [];
230  }
231 
242  private function normalizeHandler( $handler, string $hook ) {
243  $normalizedHandler = $handler;
244  if ( !is_array( $handler ) ) {
245  $normalizedHandler = [ $normalizedHandler ];
246  }
247 
248  // Empty array or array filled with null/false/empty.
249  if ( !array_filter( $normalizedHandler ) ) {
250  return false;
251  }
252 
253  if ( is_array( $normalizedHandler[0] ) ) {
254  // First element is an array, meaning the developer intended
255  // the first element to be a callback. Merge it in so that
256  // processing can be uniform.
257  $normalizedHandler = array_merge( $normalizedHandler[0], array_slice( $normalizedHandler, 1 ) );
258  }
259 
260  $firstArg = $normalizedHandler[0];
261 
262  // Extract function name, handler object, and any arguments for handler object
263  if ( $firstArg instanceof Closure ) {
264  $functionName = "hook-$hook-closure";
265  $callback = array_shift( $normalizedHandler );
266  } elseif ( is_object( $firstArg ) ) {
267  $object = array_shift( $normalizedHandler );
268  $functionName = array_shift( $normalizedHandler );
269 
270  // If no method was specified, default to on$event
271  if ( $functionName === null ) {
272  $functionName = "on$hook";
273  } else {
274  $colonPos = strpos( $functionName, '::' );
275  if ( $colonPos !== false ) {
276  // Some extensions use [ $object, 'Class::func' ] which
277  // worked with call_user_func_array() but doesn't work now
278  // that we use a plain varadic call
279  $functionName = substr( $functionName, $colonPos + 2 );
280  }
281  }
282 
283  $callback = [ $object, $functionName ];
284  } elseif ( is_string( $firstArg ) ) {
285  $functionName = $callback = array_shift( $normalizedHandler );
286  } else {
287  throw new UnexpectedValueException( 'Unknown datatype in hooks for ' . $hook );
288  }
289  return [
290  'callback' => $callback,
291  'args' => $normalizedHandler,
292  'functionName' => $functionName,
293  ];
294  }
295 
309  private function callLegacyHook( string $hook, $handler, array $args, array $options ) {
310  $callback = $handler['callback'];
311  $hookArgs = array_merge( $handler['args'], $args );
312  if ( isset( $options['deprecatedVersion'] ) && empty( $options['silent'] ) ) {
313  wfDeprecated(
314  "$hook hook (used in " . $handler['functionName'] . ")",
315  $options['deprecatedVersion'] ?? false,
316  $options['component'] ?? false
317  );
318  }
319  // Call the hooks
320  return $callback( ...$hookArgs );
321  }
322 
330  public function isRegistered( string $hook ) : bool {
331  $legacyRegisteredHook = !empty( $this->registry->getGlobalHooks()[$hook] ) ||
332  !empty( $this->legacyRegisteredHandlers[$hook] );
333  $registeredHooks = $this->registry->getExtensionHooks();
334  return !empty( $registeredHooks[$hook] ) || $legacyRegisteredHook;
335  }
336 
343  public function register( string $hook, $callback ) {
344  $deprecatedHooks = $this->registry->getDeprecatedHooks();
345  $deprecated = $deprecatedHooks->isHookDeprecated( $hook );
346  if ( $deprecated ) {
347  $info = $deprecatedHooks->getDeprecationInfo( $hook );
348  if ( empty( $info['silent'] ) ) {
349  $deprecatedVersion = $info['deprecatedVersion'] ?? false;
350  $component = $info['component'] ?? false;
351  wfDeprecated(
352  "$hook hook", $deprecatedVersion, $component
353  );
354  }
355  }
356  $this->legacyRegisteredHandlers[$hook][] = $callback;
357  }
358 
366  public function getLegacyHandlers( string $hook ) : array {
367  $handlers = array_merge(
368  $this->legacyRegisteredHandlers[$hook] ?? [],
369  $this->registry->getGlobalHooks()[$hook] ?? []
370  );
371  return $handlers;
372  }
373 
380  public function getHandlers( string $hook ) : array {
381  $handlers = [];
382  $deprecatedHooks = $this->registry->getDeprecatedHooks();
383  $registeredHooks = $this->registry->getExtensionHooks();
384  if ( isset( $registeredHooks[$hook] ) ) {
385  foreach ( $registeredHooks[$hook] as $hookReference ) {
386  // Non-legacy hooks have handler attributes
387  $handlerObject = $hookReference['handler'];
388  // Skip hooks that both acknowledge deprecation and are deprecated in core
389  $flaggedDeprecated = !empty( $hookReference['deprecated'] );
390  $deprecated = $deprecatedHooks->isHookDeprecated( $hook );
391  if ( $deprecated && $flaggedDeprecated ) {
392  continue;
393  }
394  $handlerName = $handlerObject['name'];
395  if ( !isset( $this->handlersByName[$handlerName] ) ) {
396  $this->handlersByName[$handlerName] =
397  $this->objectFactory->createObject( $handlerObject );
398  }
399  $handlers[] = $this->handlersByName[$handlerName];
400  }
401  }
402  return $handlers;
403  }
404 
411  public function emitDeprecationWarnings() {
412  $deprecatedHooks = $this->registry->getDeprecatedHooks();
413  $registeredHooks = $this->registry->getExtensionHooks();
414  foreach ( $registeredHooks as $name => $handlers ) {
415  if ( $deprecatedHooks->isHookDeprecated( $name ) ) {
416  $deprecationInfo = $deprecatedHooks->getDeprecationInfo( $name );
417  if ( !empty( $deprecationInfo['silent'] ) ) {
418  continue;
419  }
420  $version = $deprecationInfo['deprecatedVersion'] ?? '';
421  $component = $deprecationInfo['component'] ?? 'MediaWiki';
422  foreach ( $handlers as $handler ) {
423  if ( !isset( $handler['deprecated'] ) || !$handler['deprecated'] ) {
425  "Hook $name was deprecated in $component $version " .
426  "but is registered in " . $handler['extensionPath']
427  );
428  }
429  }
430  }
431  }
432  }
433 }
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:309
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:411
MediaWiki\HookContainer\HookContainer\getHandlers
getHandlers(string $hook)
Return array of handler objects registered with given hook in the new system.
Definition: HookContainer.php:380
MediaWiki\HookContainer\HookContainer\isRegistered
isRegistered(string $hook)
Return whether hook has any handlers registered to it.
Definition: HookContainer.php:330
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:242
MediaWiki\HookContainer\HookContainer\getLegacyHandlers
getLegacyHandlers(string $hook)
Get all handlers for legacy hooks system.
Definition: HookContainer.php:366
wfDeprecatedMsg
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
Definition: GlobalFunctions.php:1058
MWException
MediaWiki exception.
Definition: MWException.php:29
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1026
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:198
$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:225
MediaWiki\HookContainer\HookContainer\$registry
HookRegistry $registry
Definition: HookContainer.php:53
MediaWiki\HookContainer
Definition: DeprecatedHooks.php:23
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:181
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:321
MediaWiki\HookContainer\HookContainer\$originalHooks
array $originalHooks
existing hook names and their handlers to restore between tests
Definition: HookContainer.php:62