Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
80.85% covered (warning)
80.85%
190 / 235
50.00% covered (danger)
50.00%
9 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
HookContainer
80.85% covered (warning)
80.85%
190 / 235
50.00% covered (danger)
50.00%
9 / 18
160.71
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 salvage
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 run
75.00% covered (warning)
75.00%
15 / 20
0.00% covered (danger)
0.00%
0 / 1
9.00
 clear
n/a
0 / 0
n/a
0 / 0
3
 scopedRegister
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
2.00
 makeExtensionHandlerCallback
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
5
 normalizeHandler
78.57% covered (warning)
78.57%
55 / 70
0.00% covered (danger)
0.00%
0 / 1
40.46
 isRegistered
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 register
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 getHandlerCallbacks
66.67% covered (warning)
66.67%
8 / 12
0.00% covered (danger)
0.00%
0 / 1
3.33
 getHookNames
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 getHandlers
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
4.00
 getHandlerDescriptions
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
4
 describeHandler
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
 emitDeprecationWarnings
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
7
 checkDeprecation
40.00% covered (danger)
40.00%
4 / 10
0.00% covered (danger)
0.00%
0 / 1
7.46
 callableToString
81.82% covered (warning)
81.82%
9 / 11
0.00% covered (danger)
0.00%
0 / 1
5.15
 getHookMethodName
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 mayBeCallable
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
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
26namespace MediaWiki\HookContainer;
27
28use Closure;
29use Error;
30use InvalidArgumentException;
31use LogicException;
32use MediaWiki\Debug\MWDebug;
33use UnexpectedValueException;
34use Wikimedia\Assert\Assert;
35use Wikimedia\NonSerializable\NonSerializableTrait;
36use Wikimedia\ObjectFactory\ObjectFactory;
37use Wikimedia\ScopedCallback;
38use Wikimedia\Services\SalvageableService;
39use function array_filter;
40use function array_keys;
41use function array_merge;
42use function array_shift;
43use function array_unique;
44use function is_array;
45use function is_object;
46use function is_string;
47use function strpos;
48use function strtr;
49
50/**
51 * HookContainer class.
52 *
53 * Main class for managing hooks
54 *
55 * @since 1.35
56 */
57class 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}