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 $deprecatedVersion = $info[
'deprecatedVersion'] ??
false;
379 $component = $info[
'component'] ??
false;
381 "$hook hook", $deprecatedVersion, $component
385 $this->dynamicHandlers[$hook][] = $callback;
397 if ( $this->tombstones[$hook] ?? false ) {
402 $handlers = $this->dynamicHandlers[$hook] ?? [];
403 $keys = array_keys( $handlers );
406 for ( $i = count(
$keys ) - 1; $i >= 0; $i-- ) {
410 if ( $v === self::TOMBSTONE ) {
418 $handlers = array_intersect_key( $handlers, array_fill_keys(
$keys,
true ) );
421 $handlers = array_merge(
422 $this->registry->getGlobalHooks()[$hook] ?? [],
423 $this->dynamicHandlers[$hook] ?? []
441 public function getHandlers(
string $hook, array $options = [] ): array {
442 if ( $this->tombstones[$hook] ?? false ) {
447 $deprecatedHooks = $this->registry->getDeprecatedHooks();
448 $registeredHooks = $this->registry->getExtensionHooks();
449 if ( isset( $registeredHooks[$hook] ) ) {
450 foreach ( $registeredHooks[$hook] as $hookReference ) {
452 $handlerSpec = $hookReference[
'handler'];
454 $flaggedDeprecated = !empty( $hookReference[
'deprecated'] );
455 $deprecated = $deprecatedHooks->isHookDeprecated( $hook );
456 if ( $deprecated && $flaggedDeprecated ) {
459 $handlerName = $handlerSpec[
'name'];
461 !empty( $options[
'noServices'] ) && (
462 isset( $handlerSpec[
'services'] ) ||
463 isset( $handlerSpec[
'optional_services'] )
466 throw new UnexpectedValueException(
467 "The handler for the hook $hook registered in " .
468 "{$hookReference['extensionPath']} has a service dependency, " .
469 "but this hook does not allow it." );
471 if ( !isset( $this->handlersByName[$handlerName] ) ) {
472 $this->handlersByName[$handlerName] =
473 $this->objectFactory->createObject( $handlerSpec );
475 $handlers[] = $this->handlersByName[$handlerName];
488 $deprecatedHooks = $this->registry->getDeprecatedHooks();
489 $registeredHooks = $this->registry->getExtensionHooks();
490 foreach ( $registeredHooks as $name => $handlers ) {
491 if ( $deprecatedHooks->isHookDeprecated( $name ) ) {
492 $deprecationInfo = $deprecatedHooks->getDeprecationInfo( $name );
493 if ( !empty( $deprecationInfo[
'silent'] ) ) {
496 $version = $deprecationInfo[
'deprecatedVersion'] ??
'';
497 $component = $deprecationInfo[
'component'] ??
'MediaWiki';
498 foreach ( $handlers as $handler ) {
499 if ( !isset( $handler[
'deprecated'] ) || !$handler[
'deprecated'] ) {
500 MWDebug::sendRawDeprecated(
501 "Hook $name was deprecated in $component $version " .
502 "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.