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