31use UnexpectedValueException;
32use Wikimedia\Assert\Assert;
33use Wikimedia\NonSerializable\NonSerializableTrait;
34use Wikimedia\ObjectFactory\ObjectFactory;
35use Wikimedia\ScopedCallback;
36use Wikimedia\Services\SalvageableService;
46 use NonSerializableTrait;
49 private $dynamicHandlers = [];
54 private $tombstones = [];
57 private const TOMBSTONE =
'TOMBSTONE';
60 private $handlersByName = [];
66 private $objectFactory;
69 private $nextScopedRegisterId = 0;
77 ObjectFactory $objectFactory
79 $this->registry = $hookRegistry;
80 $this->objectFactory = $objectFactory;
93 public function salvage( SalvageableService $other ) {
94 Assert::parameterType( self::class, $other,
'$other' );
95 if ( $this->dynamicHandlers || $this->handlersByName ) {
96 throw new MWException(
'salvage() must be called immediately after construction' );
98 $this->handlersByName = $other->handlersByName;
99 $this->dynamicHandlers = $other->dynamicHandlers;
100 $this->tombstones = $other->tombstones;
125 public function run(
string $hook, array $args = [], array $options = [] ): bool {
127 $options = array_merge(
128 $this->registry->getDeprecatedHooks()->getDeprecationInfo( $hook ) ?? [],
132 $notAbortable = ( isset( $options[
'abortable'] ) && $options[
'abortable'] === false );
133 foreach ( $legacyHandlers as $handler ) {
134 $normalizedHandler = $this->normalizeHandler( $handler, $hook );
135 if ( $normalizedHandler ) {
136 $functionName = $normalizedHandler[
'functionName'];
137 $return = $this->callLegacyHook( $hook, $normalizedHandler, $args, $options );
138 if ( $notAbortable && $return !==
null && $return !==
true ) {
139 throw new UnexpectedValueException(
"Invalid return from $functionName" .
140 " for unabortable $hook." );
142 if ( $return ===
false ) {
145 if ( is_string( $return ) ) {
147 "Returning a string from a hook handler is deprecated since MediaWiki 1.35 ' .
148 '(done by $functionName for $hook)",
151 throw new UnexpectedValueException( $return );
157 $funcName =
'on' . strtr( ucfirst( $hook ),
':-',
'__' );
159 foreach ( $handlers as $handler ) {
160 $return = $handler->$funcName( ...$args );
161 if ( $notAbortable && $return !==
null && $return !==
true ) {
162 throw new UnexpectedValueException(
163 "Invalid return from " . $funcName .
" for unabortable $hook."
166 if ( $return ===
false ) {
169 if ( $return !==
null && !is_bool( $return ) ) {
170 throw new UnexpectedValueException(
"Invalid return from " . $funcName .
" for $hook." );
187 public function clear(
string $hook ): void {
188 if ( !defined(
'MW_PHPUNIT_TEST' ) && !defined(
'MW_PARSER_TEST' ) ) {
189 throw new MWException(
'Cannot reset hooks in operation.' );
198 $this->dynamicHandlers[$hook][] = self::TOMBSTONE;
199 $this->tombstones[$hook] = ( $this->tombstones[$hook] ?? 0 ) + 1;
212 public function scopedRegister(
string $hook, $callback,
bool $replace =
false ): ScopedCallback {
215 $id =
'TemporaryHook_' . $this->nextScopedRegisterId++;
220 $ts =
"{$id}_TOMBSTONE";
223 $this->dynamicHandlers[$hook][$ts] = self::TOMBSTONE;
224 $this->dynamicHandlers[$hook][$id] = $callback;
225 $this->tombstones[$hook] = ( $this->tombstones[$hook] ?? 0 ) + 1;
227 return new ScopedCallback(
228 function () use ( $hook, $id, $ts ) {
229 unset( $this->dynamicHandlers[$hook][$ts] );
230 unset( $this->dynamicHandlers[$hook][$id] );
231 $this->tombstones[$hook]--;
235 $this->dynamicHandlers[$hook][$id] = $callback;
236 return new ScopedCallback(
function () use ( $hook, $id ) {
237 unset( $this->dynamicHandlers[$hook][$id] );
252 private function normalizeHandler( $handler,
string $hook ) {
253 $normalizedHandler = $handler;
254 if ( !is_array( $handler ) ) {
255 $normalizedHandler = [ $normalizedHandler ];
259 if ( !array_filter( $normalizedHandler ) ) {
263 if ( is_array( $normalizedHandler[0] ) ) {
267 $normalizedHandler = array_merge( $normalizedHandler[0], array_slice( $normalizedHandler, 1 ) );
270 $firstArg = $normalizedHandler[0];
273 if ( $firstArg instanceof Closure ) {
274 $functionName =
"hook-$hook-closure";
275 $callback = array_shift( $normalizedHandler );
276 } elseif ( is_object( $firstArg ) ) {
277 $object = array_shift( $normalizedHandler );
278 $functionName = array_shift( $normalizedHandler );
281 if ( $functionName ===
null ) {
282 $functionName =
"on$hook";
284 $colonPos = strpos( $functionName,
'::' );
285 if ( $colonPos !==
false ) {
289 $functionName = substr( $functionName, $colonPos + 2 );
293 $callback = [ $object, $functionName ];
294 } elseif ( is_string( $firstArg ) ) {
295 if ( is_callable( $normalizedHandler,
true, $functionName )
296 && class_exists( $firstArg )
298 $callback = $normalizedHandler;
299 $normalizedHandler = [];
301 $functionName = $callback = array_shift( $normalizedHandler );
304 throw new UnexpectedValueException(
'Unknown datatype in hooks for ' . $hook );
308 'callback' => $callback,
309 'args' => $normalizedHandler,
310 'functionName' => $functionName,
327 private function callLegacyHook(
string $hook, $handler, array $args, array $options ) {
328 $callback = $handler[
'callback'];
329 $hookArgs = array_merge( $handler[
'args'], $args );
330 if ( isset( $options[
'deprecatedVersion'] ) && empty( $options[
'silent'] ) ) {
332 "$hook hook (used in " . $handler[
'functionName'] .
")",
333 $options[
'deprecatedVersion'] ??
false,
334 $options[
'component'] ??
false
338 return $callback( ...$hookArgs );
349 if ( $this->tombstones[$hook] ?? false ) {
352 return !empty( $this->getLegacyHandlers( $hook ) );
356 if ( !empty( $this->registry->getGlobalHooks()[$hook] ) ||
357 !empty( $this->dynamicHandlers[$hook] ) ||
358 !empty( $this->registry->getExtensionHooks()[$hook] )
372 public function register(
string $hook, $callback ) {
373 $deprecatedHooks = $this->registry->getDeprecatedHooks();
374 $deprecated = $deprecatedHooks->isHookDeprecated( $hook );
376 $info = $deprecatedHooks->getDeprecationInfo( $hook );
377 if ( empty( $info[
'silent'] ) ) {
378 $handler = $this->normalizeHandler( $callback, $hook );
380 "$hook hook (used in " . $handler[
'functionName'] .
")",
381 $info[
'deprecatedVersion'] ??
false,
382 $info[
'component'] ??
false
386 $this->dynamicHandlers[$hook][] = $callback;
398 if ( $this->tombstones[$hook] ?? false ) {
403 $handlers = $this->dynamicHandlers[$hook] ?? [];
404 $keys = array_keys( $handlers );
407 for ( $i = count(
$keys ) - 1; $i >= 0; $i-- ) {
411 if ( $v === self::TOMBSTONE ) {
419 $handlers = array_intersect_key( $handlers, array_fill_keys(
$keys,
true ) );
422 $handlers = array_merge(
423 $this->registry->getGlobalHooks()[$hook] ?? [],
424 $this->dynamicHandlers[$hook] ?? []
436 $names = array_merge(
437 array_keys( $this->dynamicHandlers ),
438 array_keys( $this->registry->getGlobalHooks() ),
439 array_keys( $this->registry->getExtensionHooks() )
442 return array_unique( $names );
453 $names = array_merge(
454 array_keys( $this->dynamicHandlers ),
455 array_keys( $this->registry->getExtensionHooks() ),
456 array_keys( $this->registry->getGlobalHooks() ),
459 return array_unique( $names );
473 public function getHandlers(
string $hook, array $options = [] ): array {
474 if ( $this->tombstones[$hook] ?? false ) {
479 $deprecatedHooks = $this->registry->getDeprecatedHooks();
480 $registeredHooks = $this->registry->getExtensionHooks();
481 if ( isset( $registeredHooks[$hook] ) ) {
482 foreach ( $registeredHooks[$hook] as $hookReference ) {
484 $handlerSpec = $hookReference[
'handler'];
486 $flaggedDeprecated = !empty( $hookReference[
'deprecated'] );
487 $deprecated = $deprecatedHooks->isHookDeprecated( $hook );
488 if ( $deprecated && $flaggedDeprecated ) {
491 $handlerName = $handlerSpec[
'name'];
493 !empty( $options[
'noServices'] ) && (
494 isset( $handlerSpec[
'services'] ) ||
495 isset( $handlerSpec[
'optional_services'] )
498 throw new UnexpectedValueException(
499 "The handler for the hook $hook registered in " .
500 "{$hookReference['extensionPath']} has a service dependency, " .
501 "but this hook does not allow it." );
503 if ( !isset( $this->handlersByName[$handlerName] ) ) {
504 $this->handlersByName[$handlerName] =
505 $this->objectFactory->createObject( $handlerSpec );
507 $handlers[] = $this->handlersByName[$handlerName];
520 $deprecatedHooks = $this->registry->getDeprecatedHooks();
521 $registeredHooks = $this->registry->getExtensionHooks();
522 foreach ( $registeredHooks as $name => $handlers ) {
523 if ( $deprecatedHooks->isHookDeprecated( $name ) ) {
524 $deprecationInfo = $deprecatedHooks->getDeprecationInfo( $name );
525 if ( !empty( $deprecationInfo[
'silent'] ) ) {
528 $version = $deprecationInfo[
'deprecatedVersion'] ??
'';
529 $component = $deprecationInfo[
'component'] ??
'MediaWiki';
530 foreach ( $handlers as $handler ) {
531 if ( !isset( $handler[
'deprecated'] ) || !$handler[
'deprecated'] ) {
532 MWDebug::sendRawDeprecated(
533 "Hook $name was deprecated in $component $version " .
534 "but is registered in " . $handler[
'extensionPath']
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
New debugger system that outputs a toolbar on page view.