Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
80.85% |
190 / 235 |
|
50.00% |
9 / 18 |
CRAP | |
0.00% |
0 / 1 |
HookContainer | |
80.85% |
190 / 235 |
|
50.00% |
9 / 18 |
160.71 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
salvage | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
run | |
75.00% |
15 / 20 |
|
0.00% |
0 / 1 |
9.00 | |||
clear | n/a |
0 / 0 |
n/a |
0 / 0 |
3 | |||||
scopedRegister | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
2.00 | |||
makeExtensionHandlerCallback | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
5 | |||
normalizeHandler | |
78.57% |
55 / 70 |
|
0.00% |
0 / 1 |
40.46 | |||
isRegistered | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
register | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
getHandlerCallbacks | |
66.67% |
8 / 12 |
|
0.00% |
0 / 1 |
3.33 | |||
getHookNames | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
getHandlers | |
93.75% |
15 / 16 |
|
0.00% |
0 / 1 |
4.00 | |||
getHandlerDescriptions | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
4 | |||
describeHandler | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
42 | |||
emitDeprecationWarnings | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
7 | |||
checkDeprecation | |
40.00% |
4 / 10 |
|
0.00% |
0 / 1 |
7.46 | |||
callableToString | |
81.82% |
9 / 11 |
|
0.00% |
0 / 1 |
5.15 | |||
getHookMethodName | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
mayBeCallable | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | * @ingroup Hooks |
20 | * @defgroup Hooks Hooks |
21 | * Hooks allow custom code to be executed when an event occurs; this module |
22 | * includes all hooks provided by MediaWiki Core; for more information, see |
23 | * https://www.mediawiki.org/wiki/Manual:Hooks. |
24 | */ |
25 | |
26 | namespace MediaWiki\HookContainer; |
27 | |
28 | use Closure; |
29 | use Error; |
30 | use InvalidArgumentException; |
31 | use LogicException; |
32 | use MediaWiki\Debug\MWDebug; |
33 | use UnexpectedValueException; |
34 | use Wikimedia\Assert\Assert; |
35 | use Wikimedia\NonSerializable\NonSerializableTrait; |
36 | use Wikimedia\ObjectFactory\ObjectFactory; |
37 | use Wikimedia\ScopedCallback; |
38 | use Wikimedia\Services\SalvageableService; |
39 | use function array_filter; |
40 | use function array_keys; |
41 | use function array_merge; |
42 | use function array_shift; |
43 | use function array_unique; |
44 | use function is_array; |
45 | use function is_object; |
46 | use function is_string; |
47 | use function strpos; |
48 | use function strtr; |
49 | |
50 | /** |
51 | * HookContainer class. |
52 | * |
53 | * Main class for managing hooks |
54 | * |
55 | * @since 1.35 |
56 | */ |
57 | class HookContainer implements SalvageableService { |
58 | use NonSerializableTrait; |
59 | |
60 | public const NOOP = '*no-op*'; |
61 | |
62 | /** |
63 | * Normalized hook handlers, as a 3D array: |
64 | * - the first level maps hook names to lists of handlers |
65 | * - the second is a list of handlers |
66 | * - each handler is an associative array with some well known keys, as returned by normalizeHandler() |
67 | * @var array<array> |
68 | */ |
69 | private $handlers = []; |
70 | |
71 | /** @var array<object> handler name and their handler objects */ |
72 | private $handlerObjects = []; |
73 | |
74 | /** @var HookRegistry */ |
75 | private $registry; |
76 | |
77 | /** |
78 | * Handlers registered by calling register(). |
79 | * @var array |
80 | */ |
81 | private $extraHandlers = []; |
82 | |
83 | /** @var ObjectFactory */ |
84 | private $objectFactory; |
85 | |
86 | /** @var int The next ID to be used by scopedRegister() */ |
87 | private $nextScopedRegisterId = 0; |
88 | |
89 | public function __construct( |
90 | HookRegistry $hookRegistry, |
91 | ObjectFactory $objectFactory |
92 | ) { |
93 | $this->registry = $hookRegistry; |
94 | $this->objectFactory = $objectFactory; |
95 | } |
96 | |
97 | /** |
98 | * Salvage the state of HookContainer by retaining existing handler objects |
99 | * and hooks registered via HookContainer::register(). Necessary in the event |
100 | * that MediaWikiServices::resetGlobalInstance() is called after hooks have already |
101 | * been registered. |
102 | * |
103 | * @param HookContainer|SalvageableService $other The object to salvage state from. $other be |
104 | * of type HookContainer |
105 | */ |
106 | public function salvage( SalvageableService $other ) { |
107 | Assert::parameterType( self::class, $other, '$other' ); |
108 | if ( $this->handlers || $this->handlerObjects || $this->extraHandlers ) { |
109 | throw new LogicException( 'salvage() must be called immediately after construction' ); |
110 | } |
111 | $this->handlerObjects = $other->handlerObjects; |
112 | $this->handlers = $other->handlers; |
113 | $this->extraHandlers = $other->extraHandlers; |
114 | } |
115 | |
116 | /** |
117 | * Call registered hook functions through either the legacy $wgHooks or extension.json |
118 | * |
119 | * For the given hook, fetch the array of handler objects and |
120 | * process them. Determine the proper callback for each hook and |
121 | * then call the actual hook using the appropriate arguments. |
122 | * Finally, process the return value and return/throw accordingly. |
123 | * |
124 | * @param string $hook Name of the hook |
125 | * @param array $args Arguments to pass to hook handler |
126 | * @param array $options options map: |
127 | * - abortable: (bool) If false, handlers will not be allowed to abort the call sequence. |
128 | * An exception will be raised if a handler returns anything other than true or null. |
129 | * - deprecatedVersion: (string) Version of MediaWiki this hook was deprecated in. For supporting |
130 | * Hooks::run() legacy $deprecatedVersion parameter. New core code should add deprecated |
131 | * hooks to the DeprecatedHooks::$deprecatedHooks array literal. New extension code should |
132 | * use the DeprecatedHooks attribute. |
133 | * - silent: (bool) If true, do not raise a deprecation warning |
134 | * - noServices: (bool) If true, do not allow hook handlers with service dependencies |
135 | * @return bool True if no handler aborted the hook |
136 | * @throws UnexpectedValueException if handlers return an invalid value |
137 | */ |
138 | public function run( string $hook, array $args = [], array $options = [] ): bool { |
139 | $checkDeprecation = isset( $options['deprecatedVersion'] ); |
140 | |
141 | $abortable = $options['abortable'] ?? true; |
142 | foreach ( $this->getHandlers( $hook, $options ) as $handler ) { |
143 | if ( $checkDeprecation ) { |
144 | $this->checkDeprecation( $hook, $handler, $options ); |
145 | } |
146 | |
147 | // Compose callback arguments. |
148 | if ( !empty( $handler['args'] ) ) { |
149 | $callbackArgs = array_merge( $handler['args'], $args ); |
150 | } else { |
151 | $callbackArgs = $args; |
152 | } |
153 | // Call the handler. |
154 | $callback = $handler['callback']; |
155 | $return = $callback( ...$callbackArgs ); |
156 | |
157 | // Handler returned false, signal abort to caller |
158 | if ( $return === false ) { |
159 | if ( !$abortable ) { |
160 | throw new UnexpectedValueException( "Handler {$handler['functionName']}" . |
161 | " return false for unabortable $hook." ); |
162 | } |
163 | |
164 | return false; |
165 | } elseif ( $return !== null && $return !== true ) { |
166 | throw new UnexpectedValueException( |
167 | "Hook handlers can only return null or a boolean. Got an unexpected value from " . |
168 | "handler {$handler['functionName']} for $hook" ); |
169 | } |
170 | } |
171 | |
172 | return true; |
173 | } |
174 | |
175 | /** |
176 | * Clear handlers of the given hook. |
177 | * This is intended for use while testing and will fail if MW_PHPUNIT_TEST |
178 | * and MW_PARSER_TEST are not defined. |
179 | * |
180 | * @param string $hook Name of hook to clear |
181 | * |
182 | * @internal For testing only |
183 | * @codeCoverageIgnore |
184 | */ |
185 | public function clear( string $hook ): void { |
186 | if ( !defined( 'MW_PHPUNIT_TEST' ) && !defined( 'MW_PARSER_TEST' ) ) { |
187 | throw new LogicException( 'Cannot reset hooks in operation.' ); |
188 | } |
189 | |
190 | $this->handlers[$hook] = []; |
191 | } |
192 | |
193 | /** |
194 | * Register hook and handler, allowing for easy removal. |
195 | * Intended for use in temporary registration e.g. testing |
196 | * |
197 | * @param string $hook Name of hook |
198 | * @param callable|string|array $handler Handler to attach |
199 | * @return ScopedCallback |
200 | */ |
201 | public function scopedRegister( string $hook, $handler ): ScopedCallback { |
202 | $handler = $this->normalizeHandler( $hook, $handler ); |
203 | if ( !$handler ) { |
204 | throw new InvalidArgumentException( 'Bad hook handler!' ); |
205 | } |
206 | |
207 | $this->checkDeprecation( $hook, $handler ); |
208 | |
209 | $id = 'TemporaryHook_' . $this->nextScopedRegisterId++; |
210 | |
211 | $this->getHandlers( $hook ); |
212 | |
213 | $this->handlers[$hook][$id] = $handler; |
214 | |
215 | return new ScopedCallback( function () use ( $hook, $id ) { |
216 | unset( $this->handlers[$hook][$id] ); |
217 | } ); |
218 | } |
219 | |
220 | /** |
221 | * Returns a callable array based on the handler specification provided. |
222 | * This will find the appropriate handler object to call a method on, |
223 | * This will find the appropriate handler object to call a method on, |
224 | * instantiating it if it doesn't exist yet. |
225 | * |
226 | * @param string $hook The name of the hook the handler was registered for |
227 | * @param array $handler A hook handler specification as given in an extension.json file. |
228 | * @param array $options Options to apply. If the 'noServices' option is set and the |
229 | * handler requires service injection, this method will throw an |
230 | * UnexpectedValueException. |
231 | * |
232 | * @return array |
233 | */ |
234 | private function makeExtensionHandlerCallback( string $hook, array $handler, array $options = [] ): array { |
235 | $spec = $handler['handler']; |
236 | $name = $spec['name']; |
237 | |
238 | if ( |
239 | !empty( $options['noServices'] ) && ( |
240 | !empty( $spec['services'] ) || |
241 | !empty( $spec['optional_services'] ) |
242 | ) |
243 | ) { |
244 | throw new UnexpectedValueException( |
245 | "The handler for the hook $hook registered in " . |
246 | "{$handler['extensionPath']} has a service dependency, " . |
247 | "but this hook does not allow it." ); |
248 | } |
249 | |
250 | if ( !isset( $this->handlerObjects[$name] ) ) { |
251 | // @phan-suppress-next-line PhanTypeInvalidCallableArraySize |
252 | $this->handlerObjects[$name] = $this->objectFactory->createObject( $spec ); |
253 | } |
254 | |
255 | $obj = $this->handlerObjects[$name]; |
256 | $method = $this->getHookMethodName( $hook ); |
257 | |
258 | return [ $obj, $method ]; |
259 | } |
260 | |
261 | /** |
262 | * Normalize/clean up format of argument passed as hook handler |
263 | * |
264 | * @param string $hook Hook name |
265 | * @param string|array|callable $handler Executable handler function. See register() for supported structures. |
266 | * @param array $options see makeExtensionHandlerCallback() |
267 | * |
268 | * @return array|false |
269 | * - callback: (callable) Executable handler function |
270 | * - functionName: (string) Handler name for passing to wfDeprecated() or Exceptions thrown |
271 | * - args: (array) Extra handler function arguments (omitted when not needed) |
272 | */ |
273 | private function normalizeHandler( string $hook, $handler, array $options = [] ) { |
274 | if ( is_object( $handler ) && !$handler instanceof Closure ) { |
275 | $handler = [ $handler, $this->getHookMethodName( $hook ) ]; |
276 | } |
277 | |
278 | // Backwards compatibility with old-style callable that uses a qualified method name. |
279 | if ( is_array( $handler ) && is_object( $handler[0] ?? false ) && is_string( $handler[1] ?? false ) ) { |
280 | $ofs = strpos( $handler[1], '::' ); |
281 | |
282 | if ( $ofs !== false ) { |
283 | $msg = self::callableToString( $handler ); |
284 | wfDeprecatedMsg( "Deprecated handler style for hook '$hook': " . |
285 | "callable using qualified method name ($msg)" ); |
286 | $handler[1] = substr( $handler[1], $ofs + 2 ); |
287 | } |
288 | } |
289 | |
290 | // Backwards compatibility: support objects wrapped in an array but no method name. |
291 | if ( is_array( $handler ) && is_object( $handler[0] ?? false ) && !isset( $handler[1] ) ) { |
292 | if ( !$handler[0] instanceof Closure ) { |
293 | $handler[1] = $this->getHookMethodName( $hook ); |
294 | $msg = self::callableToString( $handler ); |
295 | wfDeprecatedMsg( "Deprecated handler style for hook '$hook': object wrapped in array ($msg)" ); |
296 | } |
297 | } |
298 | |
299 | // The empty callback is used to represent a no-op handler in some test cases. |
300 | if ( $handler === [] || $handler === null || $handler === false || $handler === self::NOOP ) { |
301 | if ( $handler !== self::NOOP ) { |
302 | wfDeprecatedMsg( |
303 | "Deprecated handler style for hook '$hook': falsy value, use HookContainer::NOOP instead." |
304 | ); |
305 | } |
306 | |
307 | return [ |
308 | 'callback' => static function () { |
309 | // no-op |
310 | }, |
311 | 'functionName' => self::NOOP, |
312 | ]; |
313 | } |
314 | |
315 | // Plain callback |
316 | if ( self::mayBeCallable( $handler ) ) { |
317 | return [ |
318 | 'callback' => $handler, |
319 | 'functionName' => self::callableToString( $handler ), |
320 | ]; |
321 | } |
322 | |
323 | // Not callable and not an array. Something is wrong. |
324 | if ( !is_array( $handler ) ) { |
325 | return false; |
326 | } |
327 | |
328 | // Empty array or array filled with null/false/empty. |
329 | if ( !array_filter( $handler ) ) { |
330 | return false; |
331 | } |
332 | |
333 | // ExtensionRegistry style handler |
334 | if ( isset( $handler['handler'] ) ) { |
335 | // Skip hooks that both acknowledge deprecation and are deprecated in core |
336 | if ( $handler['deprecated'] ?? false ) { |
337 | $deprecatedHooks = $this->registry->getDeprecatedHooks(); |
338 | $deprecated = $deprecatedHooks->isHookDeprecated( $hook ); |
339 | if ( $deprecated ) { |
340 | return false; |
341 | } |
342 | } |
343 | |
344 | $callback = $this->makeExtensionHandlerCallback( $hook, $handler, $options ); |
345 | return [ |
346 | 'callback' => $callback, |
347 | 'functionName' => self::callableToString( $callback ), |
348 | ]; |
349 | } |
350 | |
351 | // Not an indexed array, something is wrong. |
352 | if ( !isset( $handler[0] ) ) { |
353 | return false; |
354 | } |
355 | |
356 | // Backwards compatibility: support for arrays of the form [ $object, $method, $data... ] |
357 | if ( is_object( $handler[0] ) && is_string( $handler[1] ?? false ) && array_key_exists( 2, $handler ) ) { |
358 | $obj = $handler[0]; |
359 | if ( !$obj instanceof Closure ) { |
360 | // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset |
361 | $method = $handler[1]; |
362 | $handler = array_merge( |
363 | [ [ $obj, $method ] ], |
364 | array_slice( $handler, 2 ) |
365 | ); |
366 | $msg = self::callableToString( $handler[1] ); |
367 | wfDeprecatedMsg( "Deprecated handler style for hook '$hook': callable array with extra data ($msg)" ); |
368 | } |
369 | } |
370 | |
371 | // The only option left is an array of the form [ $callable, $data... ] |
372 | $callback = array_shift( $handler ); |
373 | |
374 | // Backwards-compatibility for callbacks in the form [ [ $function ], $data ] |
375 | if ( is_array( $callback ) && count( $callback ) === 1 && is_string( $callback[0] ?? null ) ) { |
376 | $callback = $callback[0]; |
377 | $msg = self::callableToString( $callback ); |
378 | wfDeprecatedMsg( "Deprecated handler style for hook '$hook': function wrapped in array ($msg)" ); |
379 | } |
380 | |
381 | if ( !self::mayBeCallable( $callback ) ) { |
382 | return false; |
383 | } |
384 | |
385 | $msg = self::callableToString( $callback ); |
386 | wfDeprecatedMsg( "Deprecated handler style for hook '$hook': callable wrapped in array ($msg)" ); |
387 | return [ |
388 | 'callback' => $callback, |
389 | 'functionName' => self::callableToString( $callback ), |
390 | 'args' => $handler, |
391 | ]; |
392 | } |
393 | |
394 | /** |
395 | * Return whether hook has any handlers registered to it. |
396 | * The function may have been registered via Hooks::register or in extension.json |
397 | * |
398 | * @param string $hook Name of hook |
399 | * @return bool Whether the hook has a handler registered to it |
400 | */ |
401 | public function isRegistered( string $hook ): bool { |
402 | return (bool)$this->getHandlers( $hook ); |
403 | } |
404 | |
405 | /** |
406 | * Attach an event handler to a given hook. |
407 | * |
408 | * The handler should be given in one of the following forms: |
409 | * |
410 | * 1) A callable (string, array, or closure) |
411 | * 2) An extension hook handler spec in the form returned by |
412 | * HookRegistry::getExtensionHooks |
413 | * |
414 | * Several other forms are supported for backwards compatibility, but |
415 | * should not be used when calling this method directly. |
416 | * |
417 | * @note This method accepts "broken callables", that is, callable |
418 | * structures that reference classes that could not be found or could |
419 | * not be loaded, e.g. because they implement an interface that cannot |
420 | * be loaded. This situation may legitimately arise when implementing |
421 | * hooks defined by extensions that are not present. |
422 | * In that case, the hook will never fire and registering the "broken" |
423 | * handlers is harmless. If a broken hook handler is registered for a |
424 | * hook that is indeed called, it will cause an error. This is |
425 | * intentional: we don't want to silently ignore mistakes like mistyped |
426 | * class names in a hook handler registration. |
427 | * |
428 | * @param string $hook Name of hook |
429 | * @param string|array|callable $handler handler |
430 | */ |
431 | public function register( string $hook, $handler ) { |
432 | $this->checkDeprecation( $hook, $handler ); |
433 | |
434 | if ( !isset( $this->handlers[$hook] ) ) { |
435 | // Just remember the handler for later. |
436 | // NOTE: It would be nice to normalize immediately. But since some extensions make extensive |
437 | // use of this method for registering hooks on every call, that could be a performance |
438 | // issue. This is particularly true if the hook is declared in a way that would require |
439 | // service objects to be instantiated. |
440 | $this->extraHandlers[$hook][] = $handler; |
441 | return; |
442 | } |
443 | |
444 | $normalized = $this->normalizeHandler( $hook, $handler ); |
445 | if ( !$normalized ) { |
446 | throw new InvalidArgumentException( 'Bad hook handler!' ); |
447 | } |
448 | |
449 | $this->getHandlers( $hook ); |
450 | $this->handlers[$hook][] = $normalized; |
451 | } |
452 | |
453 | /** |
454 | * Get handler callbacks. |
455 | * |
456 | * @deprecated since 1.41. |
457 | * @internal For use by FauxHookHandlerArray. Delete when no longer needed. |
458 | * @param string $hook Name of hook |
459 | * @return callable[] |
460 | */ |
461 | public function getHandlerCallbacks( string $hook ): array { |
462 | wfDeprecated( __METHOD__, '1.41' ); |
463 | $handlers = $this->getHandlers( $hook ); |
464 | |
465 | $callbacks = []; |
466 | foreach ( $handlers as $h ) { |
467 | $callback = $h['callback']; |
468 | |
469 | if ( isset( $h['args'] ) ) { |
470 | // Needs curry in order to pass extra arguments. |
471 | // NOTE: This does not support reference parameters! |
472 | $extraArgs = $h['args']; |
473 | $callbacks[] = static function ( ...$hookArgs ) use ( $callback, $extraArgs ) { |
474 | return $callback( ...$extraArgs, ...$hookArgs ); |
475 | }; |
476 | } else { |
477 | $callbacks[] = $callback; |
478 | } |
479 | } |
480 | |
481 | return $callbacks; |
482 | } |
483 | |
484 | /** |
485 | * Returns the names of all hooks that have at least one handler registered. |
486 | * @return string[] |
487 | */ |
488 | public function getHookNames(): array { |
489 | $names = array_merge( |
490 | array_keys( array_filter( $this->handlers ) ), |
491 | array_keys( array_filter( $this->extraHandlers ) ), |
492 | array_keys( array_filter( $this->registry->getGlobalHooks() ) ), |
493 | array_keys( array_filter( $this->registry->getExtensionHooks() ) ) |
494 | ); |
495 | |
496 | return array_unique( $names ); |
497 | } |
498 | |
499 | /** |
500 | * Return the array of handlers for the given hook. |
501 | * |
502 | * @param string $hook Name of the hook |
503 | * @param array $options Handler options, which may include: |
504 | * - noServices: Do not allow hook handlers with service dependencies |
505 | * @return array[] A list of handler entries |
506 | */ |
507 | private function getHandlers( string $hook, array $options = [] ): array { |
508 | if ( !isset( $this->handlers[$hook] ) ) { |
509 | $handlers = []; |
510 | $registeredHooks = $this->registry->getExtensionHooks(); |
511 | $configuredHooks = $this->registry->getGlobalHooks(); |
512 | |
513 | $rawHandlers = array_merge( |
514 | $configuredHooks[ $hook ] ?? [], |
515 | $registeredHooks[ $hook ] ?? [], |
516 | $this->extraHandlers[ $hook ] ?? [], |
517 | ); |
518 | |
519 | foreach ( $rawHandlers as $raw ) { |
520 | $handler = $this->normalizeHandler( $hook, $raw, $options ); |
521 | if ( !$handler ) { |
522 | // XXX: log this?! |
523 | // NOTE: also happens for deprecated hooks, which is fine! |
524 | continue; |
525 | } |
526 | |
527 | $handlers[] = $handler; |
528 | } |
529 | |
530 | $this->handlers[ $hook ] = $handlers; |
531 | } |
532 | |
533 | return $this->handlers[ $hook ]; |
534 | } |
535 | |
536 | /** |
537 | * Return the array of strings that describe the handler registered with the given hook. |
538 | * |
539 | * @internal Only public for use by ApiQuerySiteInfo.php and SpecialVersion.php |
540 | * @param string $hook Name of the hook |
541 | * @return string[] A list of handler descriptions |
542 | */ |
543 | public function getHandlerDescriptions( string $hook ): array { |
544 | $descriptions = []; |
545 | |
546 | if ( isset( $this->handlers[ $hook ] ) ) { |
547 | $rawHandlers = $this->handlers[ $hook ]; |
548 | } else { |
549 | $registeredHooks = $this->registry->getExtensionHooks(); |
550 | $configuredHooks = $this->registry->getGlobalHooks(); |
551 | |
552 | $rawHandlers = array_merge( |
553 | $configuredHooks[ $hook ] ?? [], |
554 | $registeredHooks[ $hook ] ?? [], |
555 | $this->extraHandlers[ $hook ] ?? [], |
556 | ); |
557 | } |
558 | |
559 | foreach ( $rawHandlers as $raw ) { |
560 | $descr = $this->describeHandler( $hook, $raw ); |
561 | |
562 | if ( $descr ) { |
563 | $descriptions[] = $descr; |
564 | } |
565 | } |
566 | |
567 | return $descriptions; |
568 | } |
569 | |
570 | /** |
571 | * Returns a human-readable description of the given handler. |
572 | * |
573 | * @param string $hook |
574 | * @param string|array|callable $handler |
575 | * |
576 | * @return ?string |
577 | */ |
578 | private function describeHandler( string $hook, $handler ): ?string { |
579 | if ( is_array( $handler ) ) { |
580 | // already normalized |
581 | if ( isset( $handler['functionName'] ) ) { |
582 | return $handler['functionName']; |
583 | } |
584 | |
585 | if ( isset( $handler['callback'] ) ) { |
586 | return self::callableToString( $handler['callback'] ); |
587 | } |
588 | |
589 | if ( isset( $handler['handler']['class'] ) ) { |
590 | // New style hook. Avoid instantiating the handler object |
591 | $method = $this->getHookMethodName( $hook ); |
592 | return $handler['handler']['class'] . '::' . $method; |
593 | } |
594 | } |
595 | |
596 | $handler = $this->normalizeHandler( $hook, $handler ); |
597 | return $handler ? $handler['functionName'] : null; |
598 | } |
599 | |
600 | /** |
601 | * For each hook handler of each hook, this will log a deprecation if: |
602 | * 1. the hook is marked deprecated and |
603 | * 2. the "silent" flag is absent or false, and |
604 | * 3. an extension registers a handler in the new way but does not acknowledge deprecation |
605 | */ |
606 | public function emitDeprecationWarnings() { |
607 | $deprecatedHooks = $this->registry->getDeprecatedHooks(); |
608 | $extensionHooks = $this->registry->getExtensionHooks(); |
609 | |
610 | foreach ( $extensionHooks as $name => $handlers ) { |
611 | if ( $deprecatedHooks->isHookDeprecated( $name ) ) { |
612 | $deprecationInfo = $deprecatedHooks->getDeprecationInfo( $name ); |
613 | if ( !empty( $deprecationInfo['silent'] ) ) { |
614 | continue; |
615 | } |
616 | $version = $deprecationInfo['deprecatedVersion'] ?? ''; |
617 | $component = $deprecationInfo['component'] ?? 'MediaWiki'; |
618 | foreach ( $handlers as $handler ) { |
619 | if ( !isset( $handler['deprecated'] ) || !$handler['deprecated'] ) { |
620 | MWDebug::sendRawDeprecated( |
621 | "Hook $name was deprecated in $component $version " . |
622 | "but is registered in " . $handler['extensionPath'] |
623 | ); |
624 | } |
625 | } |
626 | } |
627 | } |
628 | } |
629 | |
630 | /** |
631 | * Will trigger a deprecation warning if the given hook is deprecated and the deprecation |
632 | * is not marked as silent. |
633 | * |
634 | * @param string $hook The name of the hook. |
635 | * @param array|callable|string $handler A handler spec |
636 | * @param array|null $deprecationInfo Deprecation info if the caller already knows it. |
637 | * If not given, it will be looked up from the hook registry. |
638 | * |
639 | * @return void |
640 | */ |
641 | private function checkDeprecation( string $hook, $handler, ?array $deprecationInfo = null ): void { |
642 | if ( !$deprecationInfo ) { |
643 | $deprecatedHooks = $this->registry->getDeprecatedHooks(); |
644 | $deprecationInfo = $deprecatedHooks->getDeprecationInfo( $hook ); |
645 | } |
646 | |
647 | if ( $deprecationInfo && empty( $deprecationInfo['silent'] ) ) { |
648 | $description = $this->describeHandler( $hook, $handler ); |
649 | wfDeprecated( |
650 | "$hook hook (used in $description)", |
651 | $deprecationInfo['deprecatedVersion'] ?? false, |
652 | $deprecationInfo['component'] ?? false |
653 | ); |
654 | } |
655 | } |
656 | |
657 | /** |
658 | * Returns a human-readable representation of the given callable. |
659 | * |
660 | * @param callable $callable |
661 | * |
662 | * @return string |
663 | */ |
664 | private static function callableToString( $callable ): string { |
665 | if ( is_string( $callable ) ) { |
666 | return $callable; |
667 | } |
668 | |
669 | if ( $callable instanceof Closure ) { |
670 | $hash = spl_object_hash( $callable ); |
671 | return "*closure#$hash*"; |
672 | } |
673 | |
674 | if ( is_array( $callable ) ) { |
675 | [ $on, $func ] = $callable; |
676 | |
677 | if ( is_object( $on ) ) { |
678 | $on = get_class( $on ); |
679 | } |
680 | |
681 | return "$on::$func"; |
682 | } |
683 | |
684 | throw new InvalidArgumentException( 'Unexpected kind of callable' ); |
685 | } |
686 | |
687 | /** |
688 | * Returns the default handler method name for the given hook. |
689 | * |
690 | * @param string $hook |
691 | * |
692 | * @return string |
693 | */ |
694 | private function getHookMethodName( string $hook ): string { |
695 | $hook = strtr( $hook, ':\\-', '___' ); |
696 | return "on$hook"; |
697 | } |
698 | |
699 | /** |
700 | * Replacement for is_callable that will also return true when the callable uses a class |
701 | * that cannot be loaded. |
702 | * |
703 | * This may legitimately happen when a hook handler uses a hook interfaces that is defined |
704 | * in another extension. In that case, the hook itself is also defined in the other extension, |
705 | * so the hook will never be called and no problem arises. |
706 | * |
707 | * However, it is entirely possible to register broken handlers for hooks that will indeed |
708 | * be called, causing an error. This is intentional: we don't want to silently ignore |
709 | * mistakes like mistyped class names in a hook handler registration. |
710 | * |
711 | * @param mixed $v |
712 | * |
713 | * @return bool |
714 | */ |
715 | private static function mayBeCallable( $v ): bool { |
716 | try { |
717 | return is_callable( $v ); |
718 | } catch ( Error $error ) { |
719 | // If the callable uses a class that can't be loaded because it extends an unknown base class. |
720 | // Continue as if is_callable had returned true, to allow the handler to be registered. |
721 | if ( preg_match( '/Class.*not found/', $error->getMessage() ) ) { |
722 | return true; |
723 | } |
724 | |
725 | throw $error; |
726 | } |
727 | } |
728 | } |