Completed
Push — master ( 951284...b5c57e )
by
unknown
06:36 queued 11s
created

repo/includes/RepoHooks.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Wikibase\Repo;
4
5
use ApiBase;
6
use ApiEditPage;
7
use ApiModuleManager;
8
use ApiQuery;
9
use ApiQuerySiteinfo;
10
use CentralIdLookup;
11
use Content;
12
use ContentHandler;
13
use ExtensionRegistry;
14
use HistoryPager;
15
use IContextSource;
16
use LogEntry;
17
use MediaWiki\Linker\LinkTarget;
18
use MediaWiki\MediaWikiServices;
19
use MediaWiki\Revision\RevisionRecord;
20
use MediaWiki\Revision\SlotRecord;
21
use MediaWiki\User\UserIdentity;
22
use MWException;
23
use OutputPage;
24
use PageProps;
25
use Parser;
26
use ParserOptions;
27
use ParserOutput;
28
use RecentChange;
29
use ResourceLoader;
30
use Skin;
31
use SkinTemplate;
32
use StubUserLang;
33
use Title;
34
use User;
35
use Wikibase\Lib\Formatters\AutoCommentFormatter;
36
use Wikibase\Lib\LibHooks;
37
use Wikibase\Lib\ParserFunctions\CommaSeparatedList;
38
use Wikibase\Lib\Store\EntityRevision;
39
use Wikibase\Lib\Store\Sql\EntityChangeLookup;
40
use Wikibase\Repo\Api\MetaDataBridgeConfig;
41
use Wikibase\Repo\Content\EntityContent;
42
use Wikibase\Repo\Content\EntityHandler;
43
use Wikibase\Repo\Hooks\Helpers\OutputPageEntityViewChecker;
44
use Wikibase\Repo\Hooks\InfoActionHookHandler;
45
use Wikibase\Repo\Hooks\OutputPageEntityIdReader;
46
use Wikibase\Repo\Hooks\SidebarBeforeOutputHookHandler;
47
use Wikibase\Repo\Notifications\RepoEntityChange;
48
use Wikibase\Repo\ParserOutput\PlaceholderEmittingEntityTermsView;
49
use Wikibase\Repo\ParserOutput\TermboxFlag;
50
use Wikibase\Repo\ParserOutput\TermboxVersionParserCacheValueRejector;
51
use Wikibase\Repo\ParserOutput\TermboxView;
52
use Wikibase\Repo\Store\Sql\DispatchStats;
53
use Wikibase\Repo\Store\Sql\SqlSubscriptionLookup;
54
use Wikibase\View\ViewHooks;
55
use WikiPage;
56
57
/**
58
 * File defining the hook handlers for the Wikibase extension.
59
 *
60
 * @license GPL-2.0-or-later
61
 */
62
final class RepoHooks {
63
64
	/**
65
	 * Handler for the BeforePageDisplay hook, simply injects wikibase.ui.entitysearch module
66
	 * replacing the native search box with the entity selector widget.
67
	 *
68
	 * @param OutputPage $out
69
	 * @param Skin $skin
70
	 */
71
	public static function onBeforePageDisplay( OutputPage $out, Skin $skin ) {
72
		$settings = WikibaseRepo::getDefaultInstance()->getSettings();
73
		if ( $settings->getSetting( 'enableEntitySearchUI' ) === true ) {
74
			$out->addModules( 'wikibase.ui.entitysearch' );
75
		}
76
	}
77
78
	/**
79
	 * Handler for the BeforePageDisplayMobile hook that adds the wikibase mobile styles.
80
	 *
81
	 * @param OutputPage $out
82
	 * @param Skin $skin
83
	 */
84
	public static function onBeforePageDisplayMobile( OutputPage $out, Skin $skin ) {
85
		$title = $out->getTitle();
86
		$repo = WikibaseRepo::getDefaultInstance();
87
		$entityNamespaceLookup = $repo->getEntityNamespaceLookup();
88
		$isEntityTitle = $entityNamespaceLookup->isNamespaceWithEntities( $title->getNamespace() );
89
		$useNewTermbox = $repo->getSettings()->getSetting( 'termboxEnabled' );
90
91
		if ( $isEntityTitle ) {
92
			$out->addModules( 'wikibase.mobile' );
93
94
			if ( $useNewTermbox ) {
95
				$out->addModules( 'wikibase.termbox' );
96
				$out->addModuleStyles( [ 'wikibase.termbox.styles' ] );
97
			}
98
		}
99
	}
100
101
	/**
102
	 * Handler for the SetupAfterCache hook, completing the content and namespace setup.
103
	 * This updates the $wgContentHandlers and $wgNamespaceContentModels registries
104
	 * according to information provided by entity type definitions and the entityNamespaces
105
	 * setting.
106
	 *
107
	 * @throws MWException
108
	 */
109
	public static function onSetupAfterCache() {
110
		global $wgContentHandlers,
111
			$wgNamespaceContentModels;
112
113
		$wikibaseRepo = WikibaseRepo::getDefaultInstance();
114
		$namespaces = $wikibaseRepo->getLocalEntityNamespaces();
115
		$namespaceLookup = $wikibaseRepo->getEntityNamespaceLookup();
116
117
		// Register entity namespaces.
118
		// Note that $wgExtraNamespaces and $wgNamespaceAliases have already been processed at this
119
		// point and should no longer be touched.
120
		$contentModelIds = $wikibaseRepo->getContentModelMappings();
121
122
		foreach ( $namespaces as $entityType => $namespace ) {
123
			// TODO: once there is a mechanism for registering the default content model for
124
			// slots other than the main slot, do that!
125
			// XXX: we should probably not just ignore $entityTypes that don't match $contentModelIds.
126
			if ( !isset( $wgNamespaceContentModels[$namespace] )
127
				&& isset( $contentModelIds[$entityType] )
128
				&& $namespaceLookup->getEntitySlotRole( $namespace ) === 'main'
129
			) {
130
				$wgNamespaceContentModels[$namespace] = $contentModelIds[$entityType];
131
			}
132
		}
133
134
		// Register callbacks for instantiating ContentHandlers for EntityContent.
135
		foreach ( $contentModelIds as $entityType => $model ) {
136
			$wgContentHandlers[$model] = function () use ( $wikibaseRepo, $entityType ) {
137
				$entityContentFactory = $wikibaseRepo->getEntityContentFactory();
138
				return $entityContentFactory->getContentHandlerForType( $entityType );
139
			};
140
		}
141
142
		return true;
143
	}
144
145
	/**
146
	 * Hook to add PHPUnit test cases.
147
	 * @see https://www.mediawiki.org/wiki/Manual:Hooks/UnitTestsList
148
	 *
149
	 * @param string[] &$paths
150
	 */
151
	public static function registerUnitTests( array &$paths ) {
152
		$paths[] = __DIR__ . '/../tests/phpunit/';
153
	}
154
155
	/**
156
	 * Handler for the NamespaceIsMovable hook.
157
	 *
158
	 * Implemented to prevent moving pages that are in an entity namespace.
159
	 *
160
	 * @see https://www.mediawiki.org/wiki/Manual:Hooks/NamespaceIsMovable
161
	 *
162
	 * @param int $ns Namespace ID
163
	 * @param bool &$movable
164
	 */
165
	public static function onNamespaceIsMovable( $ns, &$movable ) {
166
		if ( self::isNamespaceUsedByLocalEntities( $ns ) ) {
167
			$movable = false;
168
		}
169
	}
170
171
	private static function isNamespaceUsedByLocalEntities( $namespace ) {
172
		$wikibaseRepo = WikibaseRepo::getDefaultInstance();
173
		$namespaceLookup = $wikibaseRepo->getEntityNamespaceLookup();
174
175
		// TODO: this logic seems badly misplaced, probably WikibaseRepo should be asked and be
176
		// providing different and more appropriate EntityNamespaceLookup instance
177
		// However looking at the current use of EntityNamespaceLookup, it seems to be used
178
		// for different kinds of things, which calls for more systematic audit and changes.
179
		if ( !$namespaceLookup->isEntityNamespace( $namespace ) ) {
180
			return false;
181
		}
182
183
		$entityType = $namespaceLookup->getEntityType( $namespace );
184
185
		if ( $entityType === null ) {
186
			return false;
187
		}
188
189
		$entitySource = $wikibaseRepo->getEntitySourceDefinitions()->getSourceForEntityType(
190
			$entityType
191
		);
192
		if ( $entitySource === null ) {
193
			return false;
194
		}
195
196
		$localEntitySourceName = $wikibaseRepo->getSettings()->getSetting( 'localEntitySourceName' );
197
		if ( $entitySource->getSourceName() === $localEntitySourceName ) {
198
			return true;
199
		}
200
201
		return false;
202
	}
203
204
	/**
205
	 * Called when a revision was inserted due to an edit.
206
	 *
207
	 * @see https://www.mediawiki.org/wiki/Manual:Hooks/RevisionFromEditComplete
208
	 *
209
	 * @param WikiPage $wikiPage
210
	 * @param RevisionRecord $revisionRecord
211
	 * @param int $baseID
212
	 * @param UserIdentity $user
213
	 */
214
	public static function onRevisionFromEditComplete(
215
		WikiPage $wikiPage,
216
		RevisionRecord $revisionRecord,
217
		$baseID,
218
		UserIdentity $user
219
	) {
220
		$entityContentFactory = WikibaseRepo::getDefaultInstance()->getEntityContentFactory();
221
222
		if ( $entityContentFactory->isEntityContentModel( $wikiPage->getContent()->getModel() ) ) {
223
			self::notifyEntityStoreWatcherOnUpdate(
224
				$revisionRecord->getContent( SlotRecord::MAIN ),
225
				$revisionRecord
226
			);
227
228
			$notifier = WikibaseRepo::getDefaultInstance()->getChangeNotifier();
229
			$parentId = $revisionRecord->getParentId();
230
231
			if ( !$parentId ) {
232
				$notifier->notifyOnPageCreated( $revisionRecord );
233
			} else {
234
				$parent = MediaWikiServices::getInstance()
235
					->getRevisionLookup()
236
					->getRevisionById( $parentId );
237
238
				if ( !$parent ) {
239
					wfLogWarning(
240
						__METHOD__ . ': Cannot notify on page modification: '
241
						. 'failed to load parent revision with ID ' . $parentId
242
					);
243
				} else {
244
					$notifier->notifyOnPageModified( $revisionRecord, $parent );
245
				}
246
			}
247
		}
248
	}
249
250
	private static function notifyEntityStoreWatcherOnUpdate(
251
		EntityContent $content,
252
		RevisionRecord $revision
253
	) {
254
		$watcher = WikibaseRepo::getDefaultInstance()->getEntityStoreWatcher();
255
256
		// Notify storage/lookup services that the entity was updated. Needed to track page-level changes.
257
		// May be redundant in some cases. Take care not to cause infinite regress.
258
		if ( $content->isRedirect() ) {
259
			$watcher->redirectUpdated(
260
				$content->getEntityRedirect(),
261
				$revision->getId()
262
			);
263
		} else {
264
			$watcher->entityUpdated( new EntityRevision(
265
				$content->getEntity(),
266
				$revision->getId(),
267
				$revision->getTimestamp()
268
			) );
269
		}
270
	}
271
272
	/**
273
	 * Occurs after the delete article request has been processed.
274
	 * @see https://www.mediawiki.org/wiki/Manual:Hooks/ArticleDeleteComplete
275
	 *
276
	 * @param WikiPage $wikiPage
277
	 * @param User $user
278
	 * @param string $reason
279
	 * @param int $id id of the article that was deleted
280
	 * @param Content|null $content
281
	 * @param LogEntry $logEntry
282
	 *
283
	 * @throws MWException
284
	 */
285
	public static function onArticleDeleteComplete(
286
		WikiPage $wikiPage,
287
		User $user,
288
		$reason,
289
		$id,
290
		?Content $content,
291
		LogEntry $logEntry
292
	) {
293
		$wikibaseRepo = WikibaseRepo::getDefaultInstance();
294
		$entityContentFactory = $wikibaseRepo->getEntityContentFactory();
295
296
		// Bail out if we are not looking at an entity
297
		if ( !$content || !$entityContentFactory->isEntityContentModel( $content->getModel() ) ) {
298
			return;
299
		}
300
301
		/** @var EntityContent $content */
302
		'@phan-var EntityContent $content';
303
304
		// Notify storage/lookup services that the entity was deleted. Needed to track page-level deletion.
305
		// May be redundant in some cases. Take care not to cause infinite regress.
306
		$wikibaseRepo->getEntityStoreWatcher()->entityDeleted( $content->getEntityId() );
307
308
		$notifier = $wikibaseRepo->getChangeNotifier();
309
		$notifier->notifyOnPageDeleted( $content, $user, $logEntry->getTimestamp() );
310
	}
311
312
	/**
313
	 * Handle changes for undeletions
314
	 *
315
	 * @param Title $title
316
	 * @param bool $created
317
	 * @param string $comment
318
	 */
319
	public static function onArticleUndelete( Title $title, $created, $comment ) {
320
		$wikibaseRepo = WikibaseRepo::getDefaultInstance();
321
		$entityContentFactory = $wikibaseRepo->getEntityContentFactory();
322
323
		// Bail out if we are not looking at an entity
324
		if ( !$entityContentFactory->isEntityContentModel( $title->getContentModel() ) ) {
325
			return;
326
		}
327
328
		$revisionId = $title->getLatestRevID();
329
		$revisionRecord = MediaWikiServices::getInstance()
330
			->getRevisionLookup()
331
			->getRevisionById( $revisionId );
332
		$content = $revisionRecord ? $revisionRecord->getContent( SlotRecord::MAIN ) : null;
333
334
		if ( !( $content instanceof EntityContent ) ) {
335
			return;
336
		}
337
338
		$notifier = $wikibaseRepo->getChangeNotifier();
339
		$notifier->notifyOnPageUndeleted( $revisionRecord );
340
	}
341
342
	/**
343
	 * Nasty hack to inject information from RC into the change notification saved earlier
344
	 * by the onRevisionFromEditComplete hook handler.
345
	 *
346
	 * @see https://www.mediawiki.org/wiki/Manual:Hooks/RecentChange_save
347
	 *
348
	 * @todo find a better way to do this!
349
	 *
350
	 * @param RecentChange $recentChange
351
	 */
352
	public static function onRecentChangeSave( RecentChange $recentChange ) {
353
		$logType = $recentChange->getAttribute( 'rc_log_type' );
354
		$logAction = $recentChange->getAttribute( 'rc_log_action' );
355
		$revId = $recentChange->getAttribute( 'rc_this_oldid' );
356
357
		if ( $revId <= 0 ) {
358
			// If we don't have a revision ID, we have no chance to find the right change to update.
359
			// NOTE: As of February 2015, RC entries for undeletion have rc_this_oldid = 0.
360
			return;
361
		}
362
363
		if ( $logType === null || ( $logType === 'delete' && $logAction === 'restore' ) ) {
364
			$changeLookup = WikibaseRepo::getDefaultInstance()->getStore()->getEntityChangeLookup();
365
366
			/** @var RepoEntityChange $change */
367
			$change = $changeLookup->loadByRevisionId( $revId, EntityChangeLookup::FROM_MASTER );
368
			'@phan-var RepoEntityChange $change';
369
370
			if ( $change ) {
371
				$changeStore = WikibaseRepo::getDefaultInstance()->getStore()->getChangeStore();
372
373
				$centralIdLookup = CentralIdLookup::factoryNonLocal();
374
				if ( $centralIdLookup === null ) {
375
					$centralUserId = 0;
376
				} else {
377
					$repoUser = $recentChange->getPerformer();
378
					$centralUserId = $centralIdLookup->centralIdFromLocalUser(
379
						$repoUser
380
					);
381
				}
382
383
				$change->setMetadataFromRC( $recentChange, $centralUserId );
384
				$changeStore->saveChange( $change );
385
			}
386
		}
387
	}
388
389
	/**
390
	 * Allows to add user preferences.
391
	 * @see https://www.mediawiki.org/wiki/Manual:Hooks/GetPreferences
392
	 *
393
	 * NOTE: Might make sense to put the inner functionality into a well structured Preferences file once this
394
	 *       becomes more.
395
	 *
396
	 * @param User $user
397
	 * @param array[] &$preferences
398
	 */
399
	public static function onGetPreferences( User $user, array &$preferences ) {
400
		$preferences['wb-acknowledgedcopyrightversion'] = [
401
			'type' => 'api'
402
		];
403
404
		$preferences['wb-dismissleavingsitenotice'] = [
405
			'type' => 'api'
406
		];
407
408
		$preferences['wikibase-entitytermsview-showEntitytermslistview'] = [
409
			'type' => 'toggle',
410
			'label-message' => 'wikibase-setting-entitytermsview-showEntitytermslistview',
411
			'help-message' => 'wikibase-setting-entitytermsview-showEntitytermslistview-help',
412
			'section' => 'rendering/advancedrendering',
413
			'default' => '1',
414
		];
415
	}
416
417
	/**
418
	 * Called after fetching the core default user options.
419
	 * @see https://www.mediawiki.org/wiki/Manual:Hooks/UserGetDefaultOptions
420
	 *
421
	 * @param array &$defaultOptions
422
	 */
423
	public static function onUserGetDefaultOptions( array &$defaultOptions ) {
424
		// pre-select default language in the list of fallback languages
425
		$defaultLang = $defaultOptions['language'];
426
		$defaultOptions[ 'wb-languages-' . $defaultLang ] = 1;
427
	}
428
429
	/**
430
	 * Modify line endings on history page.
431
	 * @see https://www.mediawiki.org/wiki/Manual:Hooks/PageHistoryLineEnding
432
	 *
433
	 * @param HistoryPager $history
434
	 * @param object $row
435
	 * @param string &$html
436
	 * @param array $classes
437
	 */
438
	public static function onPageHistoryLineEnding( HistoryPager $history, $row, &$html, array $classes ) {
439
		// Note: This assumes that HistoryPager::getTitle returns a Title.
440
		$entityContentFactory = WikibaseRepo::getDefaultInstance()->getEntityContentFactory();
441
442
		$wikiPage = $history->getWikiPage();
443
		$services = MediaWikiServices::getInstance();
444
445
		$revisionRecord = $services->getRevisionFactory()->newRevisionFromRow( $row );
446
		$linkTarget = $revisionRecord->getPageAsLinkTarget();
447
448
		if ( $entityContentFactory->isEntityContentModel( $history->getTitle()->getContentModel() )
449
			&& $wikiPage->getLatest() !== $revisionRecord->getId()
450
			&& $services->getPermissionManager()->quickUserCan(
451
				'edit',
452
				$history->getUser(),
453
				$linkTarget
454
			)
455
			&& !$revisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
456
		) {
457
			$link = $services->getLinkRenderer()->makeKnownLink(
458
				$linkTarget,
459
				$history->msg( 'wikibase-restoreold' )->text(),
460
				[],
461
				[
462
					'action' => 'edit',
463
					'restore' => $revisionRecord->getId()
464
				]
465
			);
466
467
			$html .= ' ' . $history->msg( 'parentheses' )->rawParams( $link )->escaped();
468
		}
469
	}
470
471
	/**
472
	 * Alter the structured navigation links in SkinTemplates.
473
	 * @see https://www.mediawiki.org/wiki/Manual:Hooks/SkinTemplateNavigation
474
	 *
475
	 * @param SkinTemplate $skinTemplate
476
	 * @param array[] &$links
477
	 */
478
	public static function onPageTabs( SkinTemplate $skinTemplate, array &$links ) {
479
		$entityContentFactory = WikibaseRepo::getDefaultInstance()->getEntityContentFactory();
480
481
		$title = $skinTemplate->getRelevantTitle();
482
483
		if ( $entityContentFactory->isEntityContentModel( $title->getContentModel() ) ) {
484
			unset( $links['views']['edit'] );
485
			unset( $links['views']['viewsource'] );
486
487
			if ( MediaWikiServices::getInstance()->getPermissionManager()
488
					->quickUserCan( 'edit', $skinTemplate->getUser(), $title )
489
			) {
490
				$out = $skinTemplate->getOutput();
491
				$request = $skinTemplate->getRequest();
492
				$old = !$out->isRevisionCurrent()
493
					&& !$request->getCheck( 'diff' );
494
495
				$restore = $request->getCheck( 'restore' );
496
497
				if ( $old || $restore ) {
498
					// insert restore tab into views array, at the second position
499
500
					$revid = $restore
501
						? $request->getText( 'restore' )
502
						: $out->getRevisionId();
503
504
					$rev = MediaWikiServices::getInstance()
505
						->getRevisionLookup()
506
						->getRevisionById( $revid );
507
					if ( !$rev || $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
508
						return;
509
					}
510
511
					$head = array_slice( $links['views'], 0, 1 );
512
					$tail = array_slice( $links['views'], 1 );
513
					$neck = [
514
						'restore' => [
515
							'class' => $restore ? 'selected' : false,
516
							'text' => $skinTemplate->getLanguage()->ucfirst(
517
								wfMessage( 'wikibase-restoreold' )->text()
518
							),
519
							'href' => $title->getLocalURL( [
520
								'action' => 'edit',
521
								'restore' => $revid
522
							] ),
523
						]
524
					];
525
526
					$links['views'] = array_merge( $head, $neck, $tail );
527
				}
528
			}
529
		}
530
	}
531
532
	/**
533
	 * Reorder the groups for the special pages
534
	 *
535
	 * @param array &$groups
536
	 * @param bool $moveOther
537
	 */
538
	public static function onSpecialPageReorderPages( &$groups, $moveOther ) {
539
		$groups = array_merge( [ 'wikibaserepo' => null ], $groups );
540
	}
541
542
	/**
543
	 * Used to append a css class to the body, so the page can be identified as Wikibase item page.
544
	 * @see https://www.mediawiki.org/wiki/Manual:Hooks/OutputPageBodyAttributes
545
	 *
546
	 * @param OutputPage $out
547
	 * @param Skin $skin
548
	 * @param array &$bodyAttrs
549
	 */
550
	public static function onOutputPageBodyAttributes( OutputPage $out, Skin $skin, array &$bodyAttrs ) {
551
		$wikibaseRepo = WikibaseRepo::getDefaultInstance();
552
		$outputPageEntityIdReader = new OutputPageEntityIdReader(
553
			new OutputPageEntityViewChecker( $wikibaseRepo->getEntityContentFactory() ),
554
			$wikibaseRepo->getEntityIdParser()
555
		);
556
557
		$entityId = $outputPageEntityIdReader->getEntityIdFromOutputPage( $out );
558
559
		if ( $entityId === null ) {
560
			return;
561
		}
562
563
		// TODO: preg_replace kind of ridiculous here, should probably change the ENTITY_TYPE constants instead
564
		$entityType = preg_replace( '/^wikibase-/i', '', $entityId->getEntityType() );
565
566
		// add class to body so it's clear this is a wb item:
567
		$bodyAttrs['class'] .= ' wb-entitypage wb-' . $entityType . 'page';
568
		// add another class with the ID of the item:
569
		$bodyAttrs['class'] .= ' wb-' . $entityType . 'page-' . $entityId->getSerialization();
570
571
		if ( $skin->getRequest()->getCheck( 'diff' ) ) {
572
			$bodyAttrs['class'] .= ' wb-diffpage';
573
		}
574
575
		if ( $out->getTitle() && $out->getRevisionId() !== $out->getTitle()->getLatestRevID() ) {
576
			$bodyAttrs['class'] .= ' wb-oldrevpage';
577
		}
578
	}
579
580
	/**
581
	 * Handler for the ApiCheckCanExecute hook in ApiMain.
582
	 *
583
	 * This implementation causes the execution of ApiEditPage (action=edit) to fail
584
	 * for all namespaces reserved for Wikibase entities. This prevents direct text-level editing
585
	 * of structured data, and it also prevents other types of content being created in these
586
	 * namespaces.
587
	 *
588
	 * @param ApiBase $module The API module being called
589
	 * @param User    $user   The user calling the API
590
	 * @param array|string|null &$message Output-parameter holding for the message the call should fail with.
591
	 *                            This can be a message key or an array as expected by ApiBase::dieUsageMsg().
592
	 *
593
	 * @return bool true to continue execution, false to abort and with $message as an error message.
594
	 */
595
	public static function onApiCheckCanExecute( ApiBase $module, User $user, &$message ) {
596
		if ( $module instanceof ApiEditPage ) {
597
			$params = $module->extractRequestParams();
598
			$pageObj = $module->getTitleOrPageId( $params );
599
			$namespace = $pageObj->getTitle()->getNamespace();
600
601
			// XXX FIXME: ApiEditPage doesn't expose the slot, but this 'magically' works if the edit is
602
			// to a MAIN slot and the entity is stored in a non-MAIN slot, because it falls back.
603
			// To be verified that this keeps working once T200570 is done in MediaWiki itself.
604
			$slots = $params['slots'] ?? [ SlotRecord::MAIN ];
605
606
			$wikibaseRepo = WikibaseRepo::getDefaultInstance();
607
608
			/**
609
			 * Don't make Wikibase check if a user can execute when the namespace in question does
610
			 * not refer to a namespace used locally for Wikibase entities.
611
			 */
612
			$localEntitySource = $wikibaseRepo->getLocalEntitySource();
613
			if ( !in_array( $namespace, $localEntitySource->getEntityNamespaceIds() ) ) {
614
					return true;
615
			}
616
617
			$entityContentFactory = $wikibaseRepo->getEntityContentFactory();
618
			$entityTypes = $wikibaseRepo->getEnabledEntityTypes();
619
620
			foreach ( $entityContentFactory->getEntityContentModels() as $contentModel ) {
621
				/** @var EntityHandler $handler */
622
				$handler = ContentHandler::getForModelID( $contentModel );
623
				'@phan-var EntityHandler $handler';
624
625
				if ( !in_array( $handler->getEntityType(), $entityTypes ) ) {
626
					// If the entity type isn't enabled then Wikibase shouldn't be checking anything.
627
					continue;
628
				}
629
630
				if (
631
					$handler->getEntityNamespace() === $namespace &&
632
					in_array( $handler->getEntitySlotRole(), $slots, true )
633
				) {
634
					// XXX: This is most probably redundant with setting
635
					// ContentHandler::supportsDirectApiEditing to false.
636
					// trying to use ApiEditPage on an entity namespace
637
					$params = $module->extractRequestParams();
638
639
					// allow undo
640
					if ( $params['undo'] > 0 ) {
641
						return true;
642
					}
643
644
					// fail
645
					$message = [
646
						'wikibase-no-direct-editing',
647
						$pageObj->getTitle()->getNsText()
648
					];
649
650
					return false;
651
				}
652
			}
653
		}
654
655
		return true;
656
	}
657
658
	/**
659
	 * Handler for the TitleGetRestrictionTypes hook.
660
	 *
661
	 * Implemented to prevent people from protecting pages from being
662
	 * created or moved in an entity namespace (which is pointless).
663
	 *
664
	 * @see https://www.mediawiki.org/wiki/Manual:Hooks/TitleGetRestrictionTypes
665
	 *
666
	 * @param Title $title
667
	 * @param string[] &$types The types of protection available
668
	 */
669
	public static function onTitleGetRestrictionTypes( Title $title, array &$types ) {
670
		$namespaceLookup = WikibaseRepo::getDefaultInstance()->getLocalEntityNamespaceLookup();
671
672
		if ( $namespaceLookup->isEntityNamespace( $title->getNamespace() ) ) {
673
			// Remove create and move protection for Wikibase namespaces
674
			$types = array_diff( $types, [ 'create', 'move' ] );
675
		}
676
	}
677
678
	/**
679
	 * Hook handler for AbuseFilter's AbuseFilter-contentToString hook, implemented
680
	 * to provide a custom text representation of Entities for filtering.
681
	 *
682
	 * @param Content $content
683
	 * @param string  &$text The resulting text
684
	 *
685
	 * @return bool
686
	 */
687
	public static function onAbuseFilterContentToString( Content $content, &$text ) {
688
		if ( $content instanceof EntityContent ) {
689
			$text = $content->getTextForFilters();
690
691
			return false;
692
		}
693
694
		return true;
695
	}
696
697
	/**
698
	 * Handler for the FormatAutocomments hook, implementing localized formatting
699
	 * for machine readable autocomments generated by SummaryFormatter.
700
	 *
701
	 * @param string &$comment reference to the autocomment text
702
	 * @param bool $pre true if there is content before the autocomment
703
	 * @param string $auto the autocomment unformatted
704
	 * @param bool $post true if there is content after the autocomment
705
	 * @param Title|null $title use for further information
706
	 * @param bool $local shall links be generated locally or globally
707
	 */
708
	public static function onFormat( &$comment, $pre, $auto, $post, $title, $local ) {
0 ignored issues
show
The parameter $local is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
709
		global $wgLang, $wgTitle;
710
711
		// If it is possible to avoid loading the whole page then the code will be lighter on the server.
712
		if ( !( $title instanceof Title ) ) {
713
			$title = $wgTitle;
714
		}
715
716
		if ( !( $title instanceof Title ) ) {
717
			return;
718
		}
719
720
		$namespaceLookup = WikibaseRepo::getDefaultInstance()->getEntityNamespaceLookup();
721
		$entityType = $namespaceLookup->getEntityType( $title->getNamespace() );
722
		if ( $entityType === null ) {
723
			return;
724
		}
725
726
		if ( $wgLang instanceof StubUserLang ) {
727
			StubUserLang::unstub( $wgLang );
728
		}
729
730
		$formatter = new AutoCommentFormatter( $wgLang, [ 'wikibase-' . $entityType, 'wikibase-entity' ] );
731
		$formattedComment = $formatter->formatAutoComment( $auto );
732
733
		if ( is_string( $formattedComment ) ) {
734
			$comment = $formatter->wrapAutoComment( $pre, $formattedComment, $post );
735
		}
736
	}
737
738
	/**
739
	 * Called when pushing meta-info from the ParserOutput into OutputPage.
740
	 * Used to transfer 'wikibase-view-chunks' and entity data from ParserOutput to OutputPage.
741
	 *
742
	 * @param OutputPage $out
743
	 * @param ParserOutput $parserOutput
744
	 */
745
	public static function onOutputPageParserOutput( OutputPage $out, ParserOutput $parserOutput ) {
746
		// Set in EntityParserOutputGenerator.
747
		$placeholders = $parserOutput->getExtensionData( 'wikibase-view-chunks' );
748
		if ( $placeholders !== null ) {
749
			$out->setProperty( 'wikibase-view-chunks', $placeholders );
750
		}
751
752
		// Set in EntityParserOutputGenerator.
753
		$termsListItems = $parserOutput->getExtensionData( 'wikibase-terms-list-items' );
754
		if ( $termsListItems !== null ) {
755
			$out->setProperty( 'wikibase-terms-list-items', $termsListItems );
756
		}
757
758
		// Used in ViewEntityAction and EditEntityAction to override the page HTML title
759
		// with the label, if available, or else the id. Passed via parser output
760
		// and output page to save overhead of fetching content and accessing an entity
761
		// on page view.
762
		$meta = $parserOutput->getExtensionData( 'wikibase-meta-tags' );
763
		$out->setProperty( 'wikibase-meta-tags', $meta );
764
765
		$out->setProperty(
766
			TermboxView::TERMBOX_MARKUP,
767
			$parserOutput->getExtensionData( TermboxView::TERMBOX_MARKUP )
768
		);
769
770
		// Array with <link rel="alternate"> tags for the page HEAD.
771
		$alternateLinks = $parserOutput->getExtensionData( 'wikibase-alternate-links' );
772
		if ( $alternateLinks !== null ) {
773
			foreach ( $alternateLinks as $link ) {
774
				$out->addLink( $link );
775
			}
776
		}
777
	}
778
779
	/**
780
	 * Handler for the ContentModelCanBeUsedOn hook, used to prevent pages of inappropriate type
781
	 * to be placed in an entity namespace.
782
	 *
783
	 * @param string $contentModel
784
	 * @param LinkTarget $title Actually a Title object, but we only require getNamespace
785
	 * @param bool &$ok
786
	 *
787
	 * @return bool
788
	 */
789
	public static function onContentModelCanBeUsedOn( $contentModel, LinkTarget $title, &$ok ) {
790
		$wikibaseRepo = WikibaseRepo::getDefaultInstance();
791
792
		$namespaceLookup = $wikibaseRepo->getEntityNamespaceLookup();
793
		$contentModelIds = $wikibaseRepo->getContentModelMappings();
794
795
		// Find any entity type that is mapped to the title namespace
796
		$expectedEntityType = $namespaceLookup->getEntityType( $title->getNamespace() );
797
798
		// If we don't expect an entity type, then don't check anything else.
799
		if ( $expectedEntityType === null ) {
800
			return true;
801
		}
802
803
		// If the entity type is not from the local source, don't check anything else
804
		$entitySource = $wikibaseRepo->getEntitySourceDefinitions()->getSourceForEntityType( $expectedEntityType );
805
		if ( $entitySource->getSourceName() !== $wikibaseRepo->getLocalEntitySource()->getSourceName() ) {
806
			return true;
807
		}
808
809
		// XXX: If the slot is not the main slot, then assume someone isn't somehow trying
810
		// to add another content type there. We want to actually check per slot type here.
811
		// This should be fixed with https://gerrit.wikimedia.org/r/#/c/mediawiki/core/+/434544/
812
		$expectedSlot = $namespaceLookup->getEntitySlotRole( $expectedEntityType );
813
		if ( $expectedSlot !== 'main' ) {
814
			return true;
815
		}
816
817
		// If the namespace is an entity namespace, the content model
818
		// must be the model assigned to that namespace.
819
		$expectedModel = $contentModelIds[$expectedEntityType];
820
		if ( $expectedModel !== $contentModel ) {
821
			$ok = false;
822
			return false;
823
		}
824
825
		return true;
826
	}
827
828
	/**
829
	 * Exposes configuration values to the action=query&meta=siteinfo API, including lists of
830
	 * property and data value types, sparql endpoint, and several base URLs and URIs.
831
	 *
832
	 * @param ApiQuerySiteinfo $api
833
	 * @param array &$data
834
	 */
835
	public static function onAPIQuerySiteInfoGeneralInfo( ApiQuerySiteinfo $api, array &$data ) {
836
		$wikibaseRepo = WikibaseRepo::getDefaultInstance();
837
		$dataTypes = $wikibaseRepo->getDataTypeFactory()->getTypes();
838
		$propertyTypes = [];
839
840
		foreach ( $dataTypes as $id => $type ) {
841
			$propertyTypes[$id] = [ 'valuetype' => $type->getDataValueType() ];
842
		}
843
844
		$data['wikibase-propertytypes'] = $propertyTypes;
845
846
		$conceptBaseUri = $wikibaseRepo->getSettings()->getSetting( 'conceptBaseUri' );
847
		$data['wikibase-conceptbaseuri'] = $conceptBaseUri;
848
849
		$geoShapeStorageBaseUrl = $wikibaseRepo->getSettings()->getSetting( 'geoShapeStorageBaseUrl' );
850
		$data['wikibase-geoshapestoragebaseurl'] = $geoShapeStorageBaseUrl;
851
852
		$tabularDataStorageBaseUrl = $wikibaseRepo->getSettings()->getSetting( 'tabularDataStorageBaseUrl' );
853
		$data['wikibase-tabulardatastoragebaseurl'] = $tabularDataStorageBaseUrl;
854
855
		$sparqlEndpoint = $wikibaseRepo->getSettings()->getSetting( 'sparqlEndpoint' );
856
		if ( is_string( $sparqlEndpoint ) ) {
857
			$data['wikibase-sparql'] = $sparqlEndpoint;
858
		}
859
	}
860
861
	/**
862
	 * Helper for onAPIQuerySiteInfoStatisticsInfo
863
	 *
864
	 * @param object $row
865
	 * @return array
866
	 */
867
	private static function formatDispatchRow( $row ) {
868
		$data = [
869
			'pending' => $row->chd_pending,
870
			'lag' => $row->chd_lag,
871
		];
872
		if ( isset( $row->chd_site ) ) {
873
			$data['site'] = $row->chd_site;
874
		}
875
		if ( isset( $row->chd_seen ) ) {
876
			$data['position'] = $row->chd_seen;
877
		}
878
		if ( isset( $row->chd_touched ) ) {
879
			$data['touched'] = wfTimestamp( TS_ISO_8601, $row->chd_touched );
880
		}
881
882
		return $data;
883
	}
884
885
	/**
886
	 * Adds DispatchStats info to the API
887
	 *
888
	 * @param array[] &$data
889
	 */
890
	public static function onAPIQuerySiteInfoStatisticsInfo( array &$data ) {
891
		$stats = new DispatchStats();
892
		$stats->load();
893
		if ( $stats->hasStats() ) {
894
			$data['dispatch'] = [
895
				'oldest' => [
896
					'id' => $stats->getMinChangeId(),
897
					'timestamp' => $stats->getMinChangeTimestamp(),
898
				],
899
				'newest' => [
900
					'id' => $stats->getMaxChangeId(),
901
					'timestamp' => $stats->getMaxChangeTimestamp(),
902
				],
903
				'freshest' => self::formatDispatchRow( $stats->getFreshest() ),
904
				'median' => self::formatDispatchRow( $stats->getMedian() ),
905
				'stalest' => self::formatDispatchRow( $stats->getStalest() ),
906
				'average' => self::formatDispatchRow( $stats->getAverage() ),
907
			];
908
		}
909
	}
910
911
	/**
912
	 * Called by Import.php. Implemented to prevent the import of entities.
913
	 *
914
	 * @param object $importer unclear, see Bug T66657
915
	 * @param array $pageInfo
916
	 * @param array $revisionInfo
917
	 *
918
	 * @throws MWException
919
	 */
920
	public static function onImportHandleRevisionXMLTag( $importer, $pageInfo, $revisionInfo ) {
921
		if ( isset( $revisionInfo['model'] ) ) {
922
			$wikibaseRepo = WikibaseRepo::getDefaultInstance();
923
			$contentModels = $wikibaseRepo->getContentModelMappings();
924
			$allowImport = $wikibaseRepo->getSettings()->getSetting( 'allowEntityImport' );
925
926
			if ( !$allowImport && in_array( $revisionInfo['model'], $contentModels ) ) {
927
				// Skip entities.
928
				// XXX: This is rather rough.
929
				throw new MWException(
930
					'To avoid ID conflicts, the import of Wikibase entities is not supported.'
931
						. ' You can enable imports using the "allowEntityImport" setting.'
932
				);
933
			}
934
		}
935
	}
936
937
	/**
938
	 * Add Concept URI link to the toolbox section of the sidebar.
939
	 *
940
	 * @param Skin $skin
941
	 * @param string[] &$sidebar
942
	 * @return void
943
	 */
944
	public static function onSidebarBeforeOutput( Skin $skin, array &$sidebar ): void {
945
		$repo = WikibaseRepo::getDefaultInstance();
946
		$hookHandler = new SidebarBeforeOutputHookHandler(
947
			$repo->getSettings()->getSetting( 'conceptBaseUri' ),
948
			$repo->getEntityIdLookup(),
949
			$repo->getEntityLookup(),
950
			$repo->getEntityNamespaceLookup(),
951
			$repo->getLogger()
952
		);
953
954
		$conceptUriLink = $hookHandler->buildConceptUriLink( $skin );
955
956
		if ( $conceptUriLink === null ) {
957
			return;
958
		}
959
960
		$sidebar['TOOLBOX']['wb-concept-uri'] = $conceptUriLink;
961
	}
962
963
	/**
964
	 * Register ResourceLoader modules with dynamic dependencies.
965
	 *
966
	 * @param ResourceLoader $resourceLoader
967
	 */
968
	public static function onResourceLoaderRegisterModules( ResourceLoader $resourceLoader ) {
969
		$moduleTemplate = [
970
			'localBasePath' => __DIR__ . '/..',
971
			'remoteExtPath' => 'Wikibase/repo',
972
		];
973
974
		$modules = [
975
			'wikibase.WikibaseContentLanguages' => $moduleTemplate + [
976
				'scripts' => [
977
					'resources/wikibase.WikibaseContentLanguages.js',
978
				],
979
				'dependencies' => [
980
					'util.ContentLanguages',
981
					'util.inherit',
982
					'wikibase',
983
				],
984
				'targets' => [ 'desktop', 'mobile' ],
985
			],
986
			'wikibase.special.languageLabelDescriptionAliases' => $moduleTemplate + [
987
				'scripts' => [
988
					'resources/wikibase.special/wikibase.special.languageLabelDescriptionAliases.js',
989
				],
990
				'dependencies' => [
991
					'oojs-ui',
992
				],
993
				'messages' => [
994
					'wikibase-label-edit-placeholder',
995
					'wikibase-label-edit-placeholder-language-aware',
996
					'wikibase-description-edit-placeholder',
997
					'wikibase-description-edit-placeholder-language-aware',
998
					'wikibase-aliases-edit-placeholder',
999
					'wikibase-aliases-edit-placeholder-language-aware',
1000
				],
1001
			],
1002
		];
1003
1004
		$isUlsLoaded = ExtensionRegistry::getInstance()->isLoaded( 'UniversalLanguageSelector' );
1005
		if ( $isUlsLoaded ) {
1006
			$modules['wikibase.WikibaseContentLanguages']['dependencies'][] = 'ext.uls.languagenames';
1007
			$modules['wikibase.special.languageLabelDescriptionAliases']['dependencies'][] = 'ext.uls.mediawiki';
1008
		}
1009
1010
		$resourceLoader->register( $modules );
1011
	}
1012
1013
	/**
1014
	 * Adds the Wikis using the entity in action=info
1015
	 *
1016
	 * @param IContextSource $context
1017
	 * @param array[] &$pageInfo
1018
	 */
1019
	public static function onInfoAction( IContextSource $context, array &$pageInfo ) {
1020
		$wikibaseRepo = WikibaseRepo::getDefaultInstance();
1021
1022
		$namespaceChecker = $wikibaseRepo->getEntityNamespaceLookup();
1023
		$title = $context->getTitle();
1024
1025
		if ( !$title || !$namespaceChecker->isNamespaceWithEntities( $title->getNamespace() ) ) {
1026
			// shorten out
1027
			return;
1028
		}
1029
1030
		$mediaWikiServices = MediaWikiServices::getInstance();
1031
		$loadBalancer = $mediaWikiServices->getDBLoadBalancer();
1032
		$subscriptionLookup = new SqlSubscriptionLookup( $loadBalancer );
1033
		$entityIdLookup = $wikibaseRepo->getEntityIdLookup();
1034
1035
		$siteLookup = $mediaWikiServices->getSiteLookup();
1036
1037
		$infoActionHookHandler = new InfoActionHookHandler(
1038
			$namespaceChecker,
1039
			$subscriptionLookup,
1040
			$siteLookup,
1041
			$entityIdLookup,
1042
			$context,
1043
			PageProps::getInstance()
1044
		);
1045
1046
		$pageInfo = $infoActionHookHandler->handle( $context, $pageInfo );
1047
	}
1048
1049
	/**
1050
	 * Handler for the ApiMaxLagInfo to add dispatching lag stats
1051
	 *
1052
	 * @see https://www.mediawiki.org/wiki/Manual:Hooks/ApiMaxLagInfo
1053
	 *
1054
	 * @param array &$lagInfo
1055
	 */
1056
	public static function onApiMaxLagInfo( array &$lagInfo ) {
1057
1058
		$dispatchLagToMaxLagFactor = WikibaseRepo::getDefaultInstance()->getSettings()->getSetting(
1059
			'dispatchLagToMaxLagFactor'
1060
		);
1061
1062
		if ( $dispatchLagToMaxLagFactor <= 0 ) {
1063
			return;
1064
		}
1065
1066
		$stats = new DispatchStats();
1067
		$stats->load();
1068
		$median = $stats->getMedian();
1069
1070
		if ( $median ) {
1071
			$maxDispatchLag = $median->chd_lag / (float)$dispatchLagToMaxLagFactor;
1072
			if ( $maxDispatchLag > $lagInfo['lag'] ) {
1073
				$lagInfo = [
1074
					'host' => $median->chd_site,
1075
					'lag' => $maxDispatchLag,
1076
					'type' => 'wikibase-dispatching',
1077
					'dispatchLag' => $median->chd_lag,
1078
				];
1079
			}
1080
		}
1081
	}
1082
1083
	/**
1084
	 * Handler for the ParserOptionsRegister hook to add a "wb" option for cache-splitting
1085
	 *
1086
	 * This registers a lazy-loaded parser option with its value being the EntityHandler
1087
	 * parser version. Non-Wikibase parses will ignore this option, while Wikibase parses
1088
	 * will trigger its loading via ParserOutput::recordOption() and thereby include it
1089
	 * in the cache key to fragment the cache by EntityHandler::PARSER_VERSION.
1090
	 *
1091
	 * @param array &$defaults Options and their defaults
1092
	 * @param array &$inCacheKey Whether each option splits the parser cache
1093
	 * @param array &$lazyOptions Initializers for lazy-loaded options
1094
	 */
1095
	public static function onParserOptionsRegister( &$defaults, &$inCacheKey, &$lazyOptions ) {
1096
		$defaults['wb'] = null;
1097
		$inCacheKey['wb'] = true;
1098
		$lazyOptions['wb'] = function () {
1099
			return EntityHandler::PARSER_VERSION;
1100
		};
1101
		$defaults['termboxVersion'] = null;
1102
		$inCacheKey['termboxVersion'] = true;
1103
		$lazyOptions['termboxVersion'] = function () {
1104
			return TermboxFlag::getInstance()->shouldRenderTermbox() ?
1105
				TermboxView::TERMBOX_VERSION . TermboxView::CACHE_VERSION :
1106
				PlaceholderEmittingEntityTermsView::TERMBOX_VERSION . PlaceholderEmittingEntityTermsView::CACHE_VERSION;
1107
		};
1108
	}
1109
1110
	public static function onRejectParserCacheValue( ParserOutput $parserValue, WikiPage $wikiPage, ParserOptions $parserOpts ) {
1111
		$rejector = new TermboxVersionParserCacheValueRejector( TermboxFlag::getInstance() );
1112
		return $rejector->keepCachedValue( $parserValue, $parserOpts );
1113
	}
1114
1115
	public static function onApiQueryModuleManager( ApiModuleManager $moduleManager ) {
1116
		global $wgWBRepoSettings;
1117
1118
		if ( isset( $wgWBRepoSettings['dataBridgeEnabled'] ) && $wgWBRepoSettings['dataBridgeEnabled'] ) {
1119
			$moduleManager->addModule(
1120
				'wbdatabridgeconfig',
1121
				'meta',
1122
				[
1123
					'class' => MetaDataBridgeConfig::class,
1124
					'factory' => function( ApiQuery $apiQuery, $moduleName ) {
1125
						$repo = WikibaseRepo::getDefaultInstance();
1126
1127
						return new MetaDataBridgeConfig(
1128
							$repo->getSettings(),
1129
							$apiQuery,
1130
							$moduleName,
1131
							function ( string $pagename ): ?string {
1132
								$pageTitle = Title::newFromText( $pagename );
1133
								return $pageTitle ? $pageTitle->getFullURL() : null;
1134
							}
1135
						);
1136
					},
1137
				]
1138
			);
1139
		}
1140
	}
1141
1142
	public static function onMediaWikiPHPUnitTestStartTest( $test ) {
1143
		WikibaseRepo::resetClassStatics();
1144
	}
1145
1146
	/**
1147
	 * Register the parser functions.
1148
	 *
1149
	 * @param Parser $parser
1150
	 */
1151
	public static function onParserFirstCallInit( Parser $parser ) {
1152
		$parser->setFunctionHook(
1153
			CommaSeparatedList::NAME,
1154
			[ CommaSeparatedList::class, 'handle' ]
1155
		);
1156
	}
1157
1158
	public static function onRegistration() {
1159
		global $wgResourceModules;
1160
1161
		LibHooks::onRegistration();
1162
		ViewHooks::onRegistration();
1163
1164
		$wgResourceModules = array_merge(
1165
			$wgResourceModules,
1166
			require __DIR__ . '/../resources/Resources.php'
1167
		);
1168
	}
1169
}
1170