repo/includes/RepoHooks.php
<?php
namespace Wikibase\Repo;
use LogEntry;
use MediaWiki\Api\ApiBase;
use MediaWiki\Api\ApiEditPage;
use MediaWiki\Api\ApiMain;
use MediaWiki\Api\ApiModuleManager;
use MediaWiki\Api\ApiQuery;
use MediaWiki\Api\ApiQuerySiteinfo;
use MediaWiki\Content\Content;
use MediaWiki\Context\IContextSource;
use MediaWiki\Linker\LinkTarget;
use MediaWiki\MediaWikiServices;
use MediaWiki\Output\OutputPage;
use MediaWiki\Pager\HistoryPager;
use MediaWiki\Parser\Parser;
use MediaWiki\Parser\ParserOutput;
use MediaWiki\Registration\ExtensionRegistry;
use MediaWiki\ResourceLoader\ResourceLoader;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\SlotRecord;
use MediaWiki\StubObject\StubUserLang;
use MediaWiki\Title\Title;
use MediaWiki\User\User;
use MediaWiki\User\UserIdentity;
use RuntimeException;
use Skin;
use SkinTemplate;
use Throwable;
use UnexpectedValueException;
use Wikibase\DataModel\Entity\Item;
use Wikibase\DataModel\Entity\Property;
use Wikibase\Lib\ContentLanguages;
use Wikibase\Lib\Formatters\AutoCommentFormatter;
use Wikibase\Lib\LibHooks;
use Wikibase\Lib\ParserFunctions\CommaSeparatedList;
use Wikibase\Lib\SettingsArray;
use Wikibase\Lib\StaticContentLanguages;
use Wikibase\Lib\Store\EntityRevision;
use Wikibase\Lib\UnionContentLanguages;
use Wikibase\Lib\WikibaseContentLanguages;
use Wikibase\Repo\Api\MetaDataBridgeConfig;
use Wikibase\Repo\Api\ModifyEntity;
use Wikibase\Repo\Content\EntityContent;
use Wikibase\Repo\Content\EntityHandler;
use Wikibase\Repo\Hooks\Helpers\OutputPageEntityViewChecker;
use Wikibase\Repo\Hooks\InfoActionHookHandler;
use Wikibase\Repo\Hooks\OutputPageEntityIdReader;
use Wikibase\Repo\Hooks\SidebarBeforeOutputHookHandler;
use Wikibase\Repo\ParserOutput\PlaceholderEmittingEntityTermsView;
use Wikibase\Repo\ParserOutput\TermboxFlag;
use Wikibase\Repo\ParserOutput\TermboxView;
use Wikibase\Repo\Store\RateLimitingIdGenerator;
use Wikibase\Repo\Store\Sql\SqlSubscriptionLookup;
use Wikibase\View\ViewHooks;
use WikiImporter;
use Wikimedia\Rdbms\IDBAccessObject;
use WikiPage;
/**
* File defining the hook handlers for the Wikibase extension.
*
* @license GPL-2.0-or-later
*/
final class RepoHooks {
/**
* Handler for the BeforePageDisplay hook, that conditionally adds the wikibase
* mobile styles and injects the wikibase.ui.entitysearch module replacing the
* native search box with the entity selector widget.
*
* It additionally schedules a WikibasePingback
*
* @param OutputPage $out
* @param Skin $skin
*/
public static function onBeforePageDisplay( OutputPage $out, Skin $skin ) {
$entityNamespaceLookup = WikibaseRepo::getEntityNamespaceLookup();
$namespace = $out->getTitle()->getNamespace();
$isEntityTitle = $entityNamespaceLookup->isNamespaceWithEntities( $namespace );
$settings = WikibaseRepo::getSettings();
if ( $settings->getSetting( 'enableEntitySearchUI' ) === true ) {
$skinName = $skin->getSkinName();
if ( $skinName === 'vector-2022' ) {
$out->addModules( 'wikibase.vector.searchClient' );
} elseif ( $skinName !== 'minerva' ) {
// Minerva uses its own search widget.
$out->addModules( 'wikibase.ui.entitysearch' );
}
}
if ( $settings->getSetting( 'wikibasePingback' ) ) {
WikibasePingback::schedulePingback();
}
if ( $isEntityTitle && WikibaseRepo::getMobileSite() ) {
$out->addModules( 'wikibase.mobile' );
$useNewTermbox = $settings->getSetting( 'termboxEnabled' );
$entityType = $entityNamespaceLookup->getEntityType( $namespace );
$isEntityTypeWithTermbox = $entityType === Item::ENTITY_TYPE
|| $entityType === Property::ENTITY_TYPE;
if ( $useNewTermbox && $isEntityTypeWithTermbox ) {
$out->addModules( 'wikibase.termbox' );
$out->addModuleStyles( [ 'wikibase.termbox.styles' ] );
}
}
}
/**
* Handler for the SetupAfterCache hook, completing the content and namespace setup.
* This updates the $wgContentHandlers and $wgNamespaceContentModels registries
* according to information provided by entity type definitions and the entityNamespaces
* setting for the local entity source.
*/
public static function onSetupAfterCache() {
global $wgContentHandlers,
$wgNamespaceContentModels;
if ( WikibaseRepo::getSettings()->getSetting( 'defaultEntityNamespaces' ) ) {
self::defaultEntityNamespaces();
}
$namespaces = WikibaseRepo::getLocalEntitySource()->getEntityNamespaceIds();
$namespaceLookup = WikibaseRepo::getEntityNamespaceLookup();
// Register entity namespaces.
// Note that $wgExtraNamespaces and $wgNamespaceAliases have already been processed at this
// point and should no longer be touched.
$contentModelIds = WikibaseRepo::getContentModelMappings();
foreach ( $namespaces as $entityType => $namespace ) {
// TODO: once there is a mechanism for registering the default content model for
// slots other than the main slot, do that!
// XXX: we should probably not just ignore $entityTypes that don't match $contentModelIds.
if ( !isset( $wgNamespaceContentModels[$namespace] )
&& isset( $contentModelIds[$entityType] )
&& $namespaceLookup->getEntitySlotRole( $entityType ) === SlotRecord::MAIN
) {
$wgNamespaceContentModels[$namespace] = $contentModelIds[$entityType];
}
}
// Register callbacks for instantiating ContentHandlers for EntityContent.
foreach ( $contentModelIds as $entityType => $model ) {
$wgContentHandlers[$model] = function () use ( $entityType ) {
$entityContentFactory = WikibaseRepo::getEntityContentFactory();
return $entityContentFactory->getContentHandlerForType( $entityType );
};
}
}
/**
* @suppress PhanUndeclaredConstant
*/
private static function defaultEntityNamespaces(): void {
global $wgExtraNamespaces, $wgNamespacesToBeSearchedDefault;
$baseNs = 120;
self::ensureConstant( 'WB_NS_ITEM', $baseNs );
self::ensureConstant( 'WB_NS_ITEM_TALK', $baseNs + 1 );
self::ensureConstant( 'WB_NS_PROPERTY', $baseNs + 2 );
self::ensureConstant( 'WB_NS_PROPERTY_TALK', $baseNs + 3 );
$wgExtraNamespaces[WB_NS_ITEM] = 'Item';
$wgExtraNamespaces[WB_NS_ITEM_TALK] = 'Item_talk';
$wgExtraNamespaces[WB_NS_PROPERTY] = 'Property';
$wgExtraNamespaces[WB_NS_PROPERTY_TALK] = 'Property_talk';
$wgNamespacesToBeSearchedDefault[WB_NS_ITEM] = true;
}
/**
* Ensure that a constant is set to a certain (integer) value,
* defining it or checking its value if it was already defined.
*/
private static function ensureConstant( string $name, int $value ): void {
if ( !defined( $name ) ) {
define( $name, $value );
} elseif ( constant( $name ) !== $value ) {
$actual = constant( $name );
throw new UnexpectedValueException(
"Expecting constant $name to be set to $value instead of $actual"
);
}
}
/**
* Hook to add PHPUnit test cases.
* @see https://www.mediawiki.org/wiki/Manual:Hooks/UnitTestsList
*
* @param string[] &$paths
*/
public static function registerUnitTests( array &$paths ) {
$paths[] = __DIR__ . '/../tests/phpunit/';
$paths[] = __DIR__ . '/../rest-api/tests/phpunit/';
}
/**
* Handler for the NamespaceIsMovable hook.
*
* Implemented to prevent moving pages that are in an entity namespace.
*
* @see https://www.mediawiki.org/wiki/Manual:Hooks/NamespaceIsMovable
*
* @param int $ns Namespace ID
* @param bool &$movable
*/
public static function onNamespaceIsMovable( $ns, &$movable ) {
if ( self::isNamespaceUsedByLocalEntities( $ns ) ) {
$movable = false;
}
}
private static function isNamespaceUsedByLocalEntities( $namespace ) {
$namespaceLookup = WikibaseRepo::getEntityNamespaceLookup();
// TODO: this logic seems badly misplaced, probably WikibaseRepo should be asked and be
// providing different and more appropriate EntityNamespaceLookup instance
// However looking at the current use of EntityNamespaceLookup, it seems to be used
// for different kinds of things, which calls for more systematic audit and changes.
if ( !$namespaceLookup->isEntityNamespace( $namespace ) ) {
return false;
}
$entityType = $namespaceLookup->getEntityType( $namespace );
if ( $entityType === null ) {
return false;
}
$entitySource = WikibaseRepo::getEntitySourceDefinitions()->getDatabaseSourceForEntityType(
$entityType
);
if ( $entitySource === null ) {
return false;
}
$localEntitySourceName = WikibaseRepo::getSettings()->getSetting( 'localEntitySourceName' );
if ( $entitySource->getSourceName() === $localEntitySourceName ) {
return true;
}
return false;
}
/**
* Called when a revision was inserted due to an edit.
*
* @see https://www.mediawiki.org/wiki/Manual:Hooks/RevisionFromEditComplete
*
* @param WikiPage $wikiPage
* @param RevisionRecord $revisionRecord
* @param int $baseID
* @param UserIdentity $user
*/
public static function onRevisionFromEditComplete(
WikiPage $wikiPage,
RevisionRecord $revisionRecord,
$baseID,
UserIdentity $user
) {
$entityContentFactory = WikibaseRepo::getEntityContentFactory();
if ( $entityContentFactory->isEntityContentModel( $wikiPage->getContent()->getModel() ) ) {
self::notifyEntityStoreWatcherOnUpdate(
// @phan-suppress-next-line PhanTypeMismatchArgumentSuperType Content model is checked
$revisionRecord->getContent( SlotRecord::MAIN ),
$revisionRecord
);
$notifier = WikibaseRepo::getChangeNotifier();
$parentId = $revisionRecord->getParentId();
if ( !$parentId ) {
$notifier->notifyOnPageCreated( $revisionRecord );
} else {
$parent = MediaWikiServices::getInstance()
->getRevisionLookup()
->getRevisionById( $parentId );
if ( !$parent ) {
wfLogWarning(
__METHOD__ . ': Cannot notify on page modification: '
. 'failed to load parent revision with ID ' . $parentId
);
} else {
$notifier->notifyOnPageModified( $revisionRecord, $parent );
}
}
}
}
private static function notifyEntityStoreWatcherOnUpdate(
EntityContent $content,
RevisionRecord $revision
) {
$watcher = WikibaseRepo::getEntityStoreWatcher();
// Notify storage/lookup services that the entity was updated. Needed to track page-level changes.
// May be redundant in some cases. Take care not to cause infinite regress.
if ( $content->isRedirect() ) {
$watcher->redirectUpdated(
$content->getEntityRedirect(),
$revision->getId()
);
} else {
$watcher->entityUpdated( new EntityRevision(
$content->getEntity(),
$revision->getId(),
$revision->getTimestamp()
) );
}
}
/**
* Occurs after the delete article request has been processed.
* @see https://www.mediawiki.org/wiki/Manual:Hooks/ArticleDeleteComplete
*
* @param WikiPage $wikiPage
* @param User $user
* @param string $reason
* @param int $id id of the article that was deleted
* @param Content|null $content
* @param LogEntry $logEntry
*/
public static function onArticleDeleteComplete(
WikiPage $wikiPage,
User $user,
$reason,
$id,
?Content $content,
LogEntry $logEntry
) {
$entityContentFactory = WikibaseRepo::getEntityContentFactory();
// Bail out if we are not looking at an entity
if ( !$content || !$entityContentFactory->isEntityContentModel( $content->getModel() ) ) {
return;
}
/** @var EntityContent $content */
'@phan-var EntityContent $content';
// Notify storage/lookup services that the entity was deleted. Needed to track page-level deletion.
// May be redundant in some cases. Take care not to cause infinite regress.
WikibaseRepo::getEntityStoreWatcher()->entityDeleted( $content->getEntityId() );
$notifier = WikibaseRepo::getChangeNotifier();
$notifier->notifyOnPageDeleted( $content, $user, $logEntry->getTimestamp() );
}
/**
* Handle changes for undeletions
*
* @param Title $title
* @param bool $created
* @param string $comment
*/
public static function onArticleUndelete( Title $title, $created, $comment ) {
$entityContentFactory = WikibaseRepo::getEntityContentFactory();
// Bail out if we are not looking at an entity
if ( !$entityContentFactory->isEntityContentModel( $title->getContentModel() ) ) {
return;
}
$revisionId = $title->getLatestRevID();
$revisionRecord = MediaWikiServices::getInstance()
->getRevisionLookup()
->getRevisionById( $revisionId );
if ( !$revisionRecord ) {
return;
}
$content = $revisionRecord->getContent( SlotRecord::MAIN );
if ( !( $content instanceof EntityContent ) ) {
return;
}
$notifier = WikibaseRepo::getChangeNotifier();
$notifier->notifyOnPageUndeleted( $revisionRecord );
}
/**
* Allows to add user preferences.
* @see https://www.mediawiki.org/wiki/Manual:Hooks/GetPreferences
*
* NOTE: Might make sense to put the inner functionality into a well structured Preferences file once this
* becomes more.
*
* @param User $user
* @param array[] &$preferences
*/
public static function onGetPreferences( User $user, array &$preferences ) {
$preferences['wb-acknowledgedcopyrightversion'] = [
'type' => 'api',
];
$preferences['wb-dismissleavingsitenotice'] = [
'type' => 'api',
];
$preferences['wb-reftabs-mode'] = [
'type' => 'api',
];
$preferences['wikibase-entitytermsview-showEntitytermslistview'] = [
'type' => 'toggle',
'label-message' => 'wikibase-setting-entitytermsview-showEntitytermslistview',
'help-message' => 'wikibase-setting-entitytermsview-showEntitytermslistview-help',
'section' => 'rendering/advancedrendering',
'default' => '1',
];
$preferences['wb-dont-show-again-mul-popup'] = [
'type' => 'api',
];
}
/**
* Called after fetching the core default user options.
* @see https://www.mediawiki.org/wiki/Manual:Hooks/UserGetDefaultOptions
*
* @param array &$defaultOptions
*/
public static function onUserGetDefaultOptions( array &$defaultOptions ) {
// pre-select default language in the list of fallback languages
$defaultLang = $defaultOptions['language'];
$defaultOptions[ 'wb-languages-' . $defaultLang ] = 1;
}
/**
* Modify line endings on history page.
* @see https://www.mediawiki.org/wiki/Manual:Hooks/PageHistoryLineEnding
*
* @param HistoryPager $history
* @param \stdClass $row
* @param string &$html
* @param array $classes
*/
public static function onPageHistoryLineEnding( HistoryPager $history, $row, &$html, array $classes ) {
// Note: This assumes that HistoryPager::getTitle returns a Title.
$entityContentFactory = WikibaseRepo::getEntityContentFactory();
$services = MediaWikiServices::getInstance();
$title = $history->getTitle();
$revisionRecord = $services->getRevisionFactory()->newRevisionFromRow(
$row,
IDBAccessObject::READ_NORMAL,
$title
);
$linkTarget = $revisionRecord->getPageAsLinkTarget();
if ( $entityContentFactory->isEntityContentModel( $title->getContentModel() )
&& $title->getLatestRevID() !== $revisionRecord->getId()
&& $services->getPermissionManager()->quickUserCan(
'edit',
$history->getUser(),
$linkTarget
)
&& !$revisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
) {
$link = $services->getLinkRenderer()->makeKnownLink(
$linkTarget,
$history->msg( 'wikibase-restoreold' )->text(),
[],
[
'action' => 'edit',
'restore' => $revisionRecord->getId(),
]
);
$html .= ' ' . $history->msg( 'parentheses' )->rawParams( $link )->escaped();
}
}
/**
* Alter the structured navigation links in SkinTemplates.
* @see https://www.mediawiki.org/wiki/Manual:Hooks/SkinTemplateNavigation::Universal
*
* @todo T282549 Consider moving some of this logic into a place where it can be more adequately tested
*
* @param SkinTemplate $skinTemplate
* @param array[] &$links
*/
public static function onSkinTemplateNavigationUniversal( SkinTemplate $skinTemplate, array &$links ) {
$entityContentFactory = WikibaseRepo::getEntityContentFactory();
$title = $skinTemplate->getRelevantTitle();
if ( $entityContentFactory->isEntityContentModel( $title->getContentModel() ) ) {
unset( $links['views']['edit'] );
unset( $links['views']['viewsource'] );
if ( MediaWikiServices::getInstance()->getPermissionManager()
->quickUserCan( 'edit', $skinTemplate->getUser(), $title )
) {
$out = $skinTemplate->getOutput();
$request = $skinTemplate->getRequest();
$old = !$out->isRevisionCurrent()
&& !$request->getCheck( 'diff' );
$restore = $request->getCheck( 'restore' );
if ( $old || $restore ) {
// insert restore tab into views array, at the second position
$revid = $restore
? $request->getText( 'restore' )
: $out->getRevisionId();
$rev = MediaWikiServices::getInstance()
->getRevisionLookup()
->getRevisionById( $revid );
if ( !$rev || $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
return;
}
$head = array_slice( $links['views'], 0, 1 );
$tail = array_slice( $links['views'], 1 );
$neck = [
'restore' => [
'class' => $restore ? 'selected' : false,
'text' => $skinTemplate->getLanguage()->ucfirst(
wfMessage( 'wikibase-restoreold' )->text()
),
'href' => $title->getLocalURL( [
'action' => 'edit',
'restore' => $revid,
] ),
],
];
$links['views'] = array_merge( $head, $neck, $tail );
}
}
}
}
/**
* Used to append a css class to the body, so the page can be identified as Wikibase item page.
* @see https://www.mediawiki.org/wiki/Manual:Hooks/OutputPageBodyAttributes
*
* @param OutputPage $out
* @param Skin $skin
* @param array &$bodyAttrs
*/
public static function onOutputPageBodyAttributes( OutputPage $out, Skin $skin, array &$bodyAttrs ) {
$outputPageEntityIdReader = new OutputPageEntityIdReader(
new OutputPageEntityViewChecker( WikibaseRepo::getEntityContentFactory() ),
WikibaseRepo::getEntityIdParser()
);
$entityId = $outputPageEntityIdReader->getEntityIdFromOutputPage( $out );
if ( $entityId === null ) {
return;
}
// TODO: preg_replace kind of ridiculous here, should probably change the ENTITY_TYPE constants instead
$entityType = preg_replace( '/^wikibase-/i', '', $entityId->getEntityType() );
// add class to body so it's clear this is a wb item:
$bodyAttrs['class'] .= ' wb-entitypage wb-' . $entityType . 'page';
// add another class with the ID of the item:
$bodyAttrs['class'] .= ' wb-' . $entityType . 'page-' . $entityId->getSerialization();
if ( $skin->getRequest()->getCheck( 'diff' ) ) {
$bodyAttrs['class'] .= ' wb-diffpage';
}
if ( $out->getTitle() && $out->getRevisionId() !== $out->getTitle()->getLatestRevID() ) {
$bodyAttrs['class'] .= ' wb-oldrevpage';
}
}
/**
* Handler for the ApiCheckCanExecute hook in ApiMain.
*
* This implementation causes the execution of ApiEditPage (action=edit) to fail
* for all namespaces reserved for Wikibase entities. This prevents direct text-level editing
* of structured data, and it also prevents other types of content being created in these
* namespaces.
*
* @param ApiBase $module The API module being called
* @param User $user The user calling the API
* @param array|string|null &$message Output-parameter for the message the call should fail
* with. This can be a message key or an array as expected by {@see ApiBase::dieWithError}.
*
* @return bool true to continue execution, false to abort and with $message as an error message.
*/
public static function onApiCheckCanExecute( ApiBase $module, User $user, &$message ) {
if ( $module instanceof ApiEditPage ) {
$params = $module->extractRequestParams();
$pageObj = $module->getTitleOrPageId( $params );
$namespace = $pageObj->getTitle()->getNamespace();
// XXX FIXME: ApiEditPage doesn't expose the slot, but this 'magically' works if the edit is
// to a MAIN slot and the entity is stored in a non-MAIN slot, because it falls back.
// To be verified that this keeps working once T200570 is done in MediaWiki itself.
$slots = $params['slots'] ?? [ SlotRecord::MAIN ];
/**
* Don't make Wikibase check if a user can execute when the namespace in question does
* not refer to a namespace used locally for Wikibase entities.
*/
$localEntitySource = WikibaseRepo::getLocalEntitySource();
if ( !in_array( $namespace, $localEntitySource->getEntityNamespaceIds() ) ) {
return true;
}
$entityContentFactory = WikibaseRepo::getEntityContentFactory();
$entityTypes = WikibaseRepo::getEnabledEntityTypes();
$contentHandlerFactory = MediaWikiServices::getInstance()->getContentHandlerFactory();
foreach ( $entityContentFactory->getEntityContentModels() as $contentModel ) {
/** @var EntityHandler $handler */
$handler = $contentHandlerFactory->getContentHandler( $contentModel );
'@phan-var EntityHandler $handler';
if ( !in_array( $handler->getEntityType(), $entityTypes ) ) {
// If the entity type isn't enabled then Wikibase shouldn't be checking anything.
continue;
}
if (
$handler->getEntityNamespace() === $namespace &&
in_array( $handler->getEntitySlotRole(), $slots, true )
) {
// XXX: This is most probably redundant with setting
// ContentHandler::supportsDirectApiEditing to false.
// trying to use ApiEditPage on an entity namespace
$params = $module->extractRequestParams();
// allow undo
if ( $params['undo'] > 0 ) {
return true;
}
// fail
$message = [
'wikibase-no-direct-editing',
$pageObj->getTitle()->getNsText(),
];
return false;
}
}
}
return true;
}
/**
* Handler for the TitleGetRestrictionTypes hook.
*
* Implemented to prevent people from protecting pages from being
* created or moved in an entity namespace (which is pointless).
*
* @see https://www.mediawiki.org/wiki/Manual:Hooks/TitleGetRestrictionTypes
*
* @param Title $title
* @param string[] &$types The types of protection available
*/
public static function onTitleGetRestrictionTypes( Title $title, array &$types ) {
$namespaceLookup = WikibaseRepo::getLocalEntityNamespaceLookup();
if ( $namespaceLookup->isEntityNamespace( $title->getNamespace() ) ) {
// Remove create and move protection for Wikibase namespaces
$types = array_diff( $types, [ 'create', 'move' ] );
}
}
/**
* Hook handler for AbuseFilter's AbuseFilter-contentToString hook, implemented
* to provide a custom text representation of Entities for filtering.
*
* @param Content $content
* @param string &$text The resulting text
*
* @return bool
*/
public static function onAbuseFilterContentToString( Content $content, &$text ) {
if ( $content instanceof EntityContent ) {
$text = $content->getTextForFilters();
return false;
}
return true;
}
/**
* Handler for the FormatAutocomments hook, implementing localized formatting
* for machine readable autocomments generated by SummaryFormatter.
*
* @param string &$comment reference to the autocomment text
* @param bool $pre true if there is content before the autocomment
* @param string $auto the autocomment unformatted
* @param bool $post true if there is content after the autocomment
* @param Title|null $title use for further information
* @param bool $local shall links be generated locally or globally
*/
public static function onFormat( &$comment, $pre, $auto, $post, $title, $local ) {
// phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgTitle
global $wgLang, $wgTitle;
// If it is possible to avoid loading the whole page then the code will be lighter on the server.
if ( !( $title instanceof Title ) ) {
$title = $wgTitle;
}
if ( !( $title instanceof Title ) ) {
return;
}
$namespaceLookup = WikibaseRepo::getEntityNamespaceLookup();
$entityType = $namespaceLookup->getEntityType( $title->getNamespace() );
if ( $entityType === null ) {
return;
}
if ( $wgLang instanceof StubUserLang ) {
StubUserLang::unstub( $wgLang );
}
$formatter = new AutoCommentFormatter( $wgLang, [ 'wikibase-' . $entityType, 'wikibase-entity' ] );
$formattedComment = $formatter->formatAutoComment( $auto );
if ( is_string( $formattedComment ) ) {
$comment = $formatter->wrapAutoComment( $pre, $formattedComment, $post );
}
}
/**
* Called when pushing meta-info from the ParserOutput into OutputPage.
* Used to transfer 'wikibase-view-chunks' and entity data from ParserOutput to OutputPage.
*
* @param OutputPage $outputPage
* @param ParserOutput $parserOutput
*/
public static function onOutputPageParserOutput( OutputPage $outputPage, ParserOutput $parserOutput ) {
// Set in PlaceholderEmittingEntityTermsView.
$placeholders = $parserOutput->getExtensionData( 'wikibase-view-chunks' );
if ( $placeholders !== null ) {
$outputPage->setProperty( 'wikibase-view-chunks', $placeholders );
}
// Set in PlaceholderEmittingEntityTermsView.
$termsListItems = $parserOutput->getExtensionData( 'wikibase-terms-list-items' );
if ( $termsListItems !== null ) {
$outputPage->setProperty( 'wikibase-terms-list-items', $termsListItems );
}
// Set in PlaceholderEmittingEntityTermsView
$entityLabels = $parserOutput->getExtensionData( 'wikibase-entity-labels' );
if ( $entityLabels !== null ) {
$outputPage->setProperty( 'wikibase-entity-labels', $entityLabels );
}
// Used in ViewEntityAction and EditEntityAction to override the page HTML title
// with the label, if available, or else the id. Passed via parser output
// and output page to save overhead of fetching content and accessing an entity
// on page view.
$meta = $parserOutput->getExtensionData( 'wikibase-meta-tags' );
$outputPage->setProperty( 'wikibase-meta-tags', $meta );
$outputPage->setProperty(
TermboxView::TERMBOX_MARKUP,
$parserOutput->getExtensionData( TermboxView::TERMBOX_MARKUP )
);
// Array with <link rel="alternate"> tags for the page HEAD.
$alternateLinks = $parserOutput->getExtensionData( 'wikibase-alternate-links' );
if ( $alternateLinks !== null ) {
foreach ( $alternateLinks as $link ) {
$outputPage->addLink( $link );
}
}
}
/**
* Handler for the ContentModelCanBeUsedOn hook, used to prevent pages of inappropriate type
* to be placed in an entity namespace.
*
* @param string $contentModel
* @param LinkTarget $title Actually a Title object, but we only require getNamespace
* @param bool &$ok
*
* @return bool
*/
public static function onContentModelCanBeUsedOn( $contentModel, LinkTarget $title, &$ok ) {
$namespaceLookup = WikibaseRepo::getEntityNamespaceLookup();
$contentModelIds = WikibaseRepo::getContentModelMappings();
// Find any entity type that is mapped to the title namespace
$expectedEntityType = $namespaceLookup->getEntityType( $title->getNamespace() );
// If we don't expect an entity type, then don't check anything else.
if ( $expectedEntityType === null ) {
return true;
}
// If the entity type is not from the local source, don't check anything else
$entitySource = WikibaseRepo::getEntitySourceDefinitions()->getDatabaseSourceForEntityType( $expectedEntityType );
if ( $entitySource === null ||
$entitySource->getSourceName() !== WikibaseRepo::getLocalEntitySource()->getSourceName()
) {
return true;
}
// XXX: If the slot is not the main slot, then assume someone isn't somehow trying
// to add another content type there. We want to actually check per slot type here.
// This should be fixed with https://gerrit.wikimedia.org/r/#/c/mediawiki/core/+/434544/
$expectedSlot = $namespaceLookup->getEntitySlotRole( $expectedEntityType );
if ( $expectedSlot !== SlotRecord::MAIN ) {
return true;
}
// If the namespace is an entity namespace, the content model
// must be the model assigned to that namespace.
$expectedModel = $contentModelIds[$expectedEntityType];
if ( $expectedModel !== $contentModel ) {
$ok = false;
return false;
}
return true;
}
/**
* Exposes configuration values to the action=query&meta=siteinfo API, including lists of
* property and data value types, sparql endpoint, and several base URLs and URIs.
*
* @param ApiQuerySiteinfo $api
* @param array &$data
*/
public static function onAPIQuerySiteInfoGeneralInfo( ApiQuerySiteinfo $api, array &$data ) {
$repoSettings = WikibaseRepo::getSettings();
$dataTypes = WikibaseRepo::getDataTypeFactory()->getTypes();
$propertyTypes = [];
foreach ( $dataTypes as $id => $type ) {
$propertyTypes[$id] = [ 'valuetype' => $type->getDataValueType() ];
}
$data['wikibase-propertytypes'] = $propertyTypes;
$data['wikibase-conceptbaseuri'] = WikibaseRepo::getLocalEntitySource()->getConceptBaseUri();
$geoShapeStorageBaseUrl = $repoSettings->getSetting( 'geoShapeStorageBaseUrl' );
$data['wikibase-geoshapestoragebaseurl'] = $geoShapeStorageBaseUrl;
$tabularDataStorageBaseUrl = $repoSettings->getSetting( 'tabularDataStorageBaseUrl' );
$data['wikibase-tabulardatastoragebaseurl'] = $tabularDataStorageBaseUrl;
$sparqlEndpoint = $repoSettings->getSetting( 'sparqlEndpoint' );
if ( is_string( $sparqlEndpoint ) ) {
$data['wikibase-sparql'] = $sparqlEndpoint;
}
}
/**
* Called by Import.php. Implemented to prevent the import of entities.
*
* @param WikiImporter $importer
* @param array $pageInfo
* @param array $revisionInfo
*/
public static function onImportHandleRevisionXMLTag( $importer, $pageInfo, $revisionInfo ) {
if ( isset( $revisionInfo['model'] ) ) {
$contentModels = WikibaseRepo::getContentModelMappings();
$allowImport = WikibaseRepo::getSettings()->getSetting( 'allowEntityImport' );
if ( !$allowImport && in_array( $revisionInfo['model'], $contentModels ) ) {
// Skip entities.
// XXX: This is rather rough.
throw new RuntimeException(
'To avoid ID conflicts, the import of Wikibase entities is not supported.'
. ' You can enable imports using the "allowEntityImport" setting.'
);
}
}
}
/**
* Add Concept URI link to the toolbox section of the sidebar.
*
* @param Skin $skin
* @param string[] &$sidebar
* @return void
*/
public static function onSidebarBeforeOutput( Skin $skin, array &$sidebar ): void {
$hookHandler = new SidebarBeforeOutputHookHandler(
WikibaseRepo::getLocalEntitySource()->getConceptBaseUri(),
WikibaseRepo::getEntityIdLookup(),
WikibaseRepo::getEntityLookup(),
WikibaseRepo::getEntityNamespaceLookup(),
WikibaseRepo::getLogger()
);
$conceptUriLink = $hookHandler->buildConceptUriLink( $skin );
if ( $conceptUriLink === null ) {
return;
}
$sidebar['TOOLBOX']['wb-concept-uri'] = $conceptUriLink;
}
/**
* Register ResourceLoader modules with dynamic dependencies.
*
* @param ResourceLoader $resourceLoader
*/
public static function onResourceLoaderRegisterModules( ResourceLoader $resourceLoader ) {
$moduleTemplate = [
'localBasePath' => __DIR__ . '/..',
'remoteExtPath' => 'Wikibase/repo',
];
$modules = [
'wikibase.WikibaseContentLanguages' => $moduleTemplate + [
'packageFiles' => [
'resources/wikibase.WikibaseContentLanguages.js',
[
'name' => 'resources/contentLanguages.json',
'callback' => function () {
$contentLanguages = WikibaseRepo::getWikibaseContentLanguages();
return [
WikibaseContentLanguages::CONTEXT_MONOLINGUAL_TEXT => $contentLanguages
->getContentLanguages( WikibaseContentLanguages::CONTEXT_MONOLINGUAL_TEXT )
->getLanguages(),
WikibaseContentLanguages::CONTEXT_TERM => $contentLanguages
->getContentLanguages( WikibaseContentLanguages::CONTEXT_TERM )
->getLanguages(),
];
},
],
],
'dependencies' => [
'util.ContentLanguages',
'util.inherit',
'wikibase',
'wikibase.getLanguageNameByCode',
],
],
'wikibase.special.languageLabelDescriptionAliases' => $moduleTemplate + [
'scripts' => [
'resources/wikibase.special/wikibase.special.languageLabelDescriptionAliases.js',
],
'dependencies' => [
'wikibase.getLanguageNameByCode',
'oojs-ui',
],
'messages' => [
'wikibase-label-edit-placeholder',
'wikibase-label-edit-placeholder-language-aware',
'wikibase-label-edit-placeholder-mul',
'wikibase-description-edit-placeholder',
'wikibase-description-edit-placeholder-language-aware',
'wikibase-item-description-edit-not-supported',
'wikibase-property-description-edit-not-supported',
'wikibase-aliases-edit-placeholder',
'wikibase-aliases-edit-placeholder-language-aware',
'wikibase-aliases-edit-placeholder-mul',
],
],
];
$isUlsLoaded = ExtensionRegistry::getInstance()->isLoaded( 'UniversalLanguageSelector' );
if ( $isUlsLoaded ) {
$modules['wikibase.WikibaseContentLanguages']['dependencies'][] = 'ext.uls.languagenames';
$modules['wikibase.special.languageLabelDescriptionAliases']['dependencies'][] = 'ext.uls.mediawiki';
}
$resourceLoader->register( $modules );
}
/**
* Adds the Wikis using the entity in action=info
*
* @param IContextSource $context
* @param array[] &$pageInfo
*/
public static function onInfoAction( IContextSource $context, array &$pageInfo ) {
$namespaceChecker = WikibaseRepo::getEntityNamespaceLookup();
$title = $context->getTitle();
if ( !$title || !$namespaceChecker->isNamespaceWithEntities( $title->getNamespace() ) ) {
// shorten out
return;
}
$mediaWikiServices = MediaWikiServices::getInstance();
$subscriptionLookup = new SqlSubscriptionLookup( WikibaseRepo::getRepoDomainDbFactory( $mediaWikiServices )->newRepoDb() );
$entityIdLookup = WikibaseRepo::getEntityIdLookup( $mediaWikiServices );
$siteLookup = $mediaWikiServices->getSiteLookup();
$pageProps = $mediaWikiServices->getPageProps();
$infoActionHookHandler = new InfoActionHookHandler(
$namespaceChecker,
$subscriptionLookup,
$siteLookup,
$entityIdLookup,
$context,
$pageProps
);
$pageInfo = $infoActionHookHandler->handle( $context, $pageInfo );
}
/**
* Handler for the ParserOptionsRegister hook to add a "wb" option for cache-splitting
*
* This registers a lazy-loaded parser option with its value being the EntityHandler
* parser version. Non-Wikibase parses will ignore this option, while Wikibase parses
* will trigger its loading via ParserOutput::recordOption() and thereby include it
* in the cache key to fragment the cache by EntityHandler::PARSER_VERSION.
*
* @param array &$defaults Options and their defaults
* @param array &$inCacheKey Whether each option splits the parser cache
* @param array &$lazyOptions Initializers for lazy-loaded options
*/
public static function onParserOptionsRegister( &$defaults, &$inCacheKey, &$lazyOptions ) {
$defaults['wb'] = null;
$inCacheKey['wb'] = true;
$lazyOptions['wb'] = function () {
return EntityHandler::PARSER_VERSION;
};
$defaults['termboxVersion'] = null;
$inCacheKey['termboxVersion'] = true;
$lazyOptions['termboxVersion'] = function () {
return TermboxFlag::getInstance()->shouldRenderTermbox() ?
TermboxView::TERMBOX_VERSION . TermboxView::CACHE_VERSION :
PlaceholderEmittingEntityTermsView::TERMBOX_VERSION . PlaceholderEmittingEntityTermsView::CACHE_VERSION;
};
$defaults['wbMobile'] = null;
$inCacheKey['wbMobile'] = true;
$lazyOptions['wbMobile'] = fn () => WikibaseRepo::getMobileSite();
}
public static function onApiQueryModuleManager( ApiModuleManager $moduleManager ) {
global $wgWBRepoSettings;
if ( isset( $wgWBRepoSettings['dataBridgeEnabled'] ) && $wgWBRepoSettings['dataBridgeEnabled'] ) {
$moduleManager->addModule(
'wbdatabridgeconfig',
'meta',
[
'class' => MetaDataBridgeConfig::class,
'factory' => function( ApiQuery $apiQuery, string $moduleName, SettingsArray $repoSettings ) {
return new MetaDataBridgeConfig(
$repoSettings,
$apiQuery,
$moduleName,
function ( string $pagename ): ?string {
$pageTitle = Title::newFromText( $pagename );
return $pageTitle ? $pageTitle->getFullURL() : null;
}
);
},
'services' => [
'WikibaseRepo.Settings',
],
]
);
}
}
/**
* Register the parser functions.
*
* @param Parser $parser
*/
public static function onParserFirstCallInit( Parser $parser ) {
$parser->setFunctionHook(
CommaSeparatedList::NAME,
[ CommaSeparatedList::class, 'handle' ]
);
}
public static function onRegistration() {
global $wgResourceModules, $wgRateLimits;
LibHooks::onRegistration();
ViewHooks::onRegistration();
$wgResourceModules = array_merge(
$wgResourceModules,
require __DIR__ . '/../resources/Resources.php'
);
self::inheritDefaultRateLimits( $wgRateLimits );
}
/**
* Make the 'wikibase-idgenerator' rate limit inherit the 'create' rate limit,
* or the 'edit' rate limit if no 'create' limit is defined,
* unless the 'wikibase-idgenerator' rate limit was itself customized.
*
* @param array &$rateLimits should be $wgRateLimits or a similar array
*/
public static function inheritDefaultRateLimits( array &$rateLimits ) {
if ( isset( $rateLimits['wikibase-idgenerator']['&inherit-create-edit'] ) ) {
unset( $rateLimits['wikibase-idgenerator']['&inherit-create-edit'] );
$limits = $rateLimits['create'] ?? $rateLimits['edit'] ?? [];
foreach ( $limits as $group => $limit ) {
if ( !isset( $rateLimits['wikibase-idgenerator'][$group] ) ) {
$rateLimits['wikibase-idgenerator'][$group] = $limit;
}
}
}
}
/**
* Attempt to create an entity locks an entity id (for items, it would be Q####) and if saving fails
* due to validation issues for example, that id would be wasted.
* We want to penalize the user by adding a bigger number to ratelimit and slow them down
* to avoid bots wasting significant number of Q-ids by sending faulty data over and over again.
* See T284538 for more information.
*
* @param ApiMain $apiMain
* @param Throwable $e
* @return bool|void
*/
public static function onApiMainOnException( $apiMain, $e ) {
$module = $apiMain->getModule();
if ( !$module instanceof ModifyEntity ) {
return;
}
$repoSettings = WikibaseRepo::getSettings();
$idGeneratorInErrorPingLimiterValue = $repoSettings->getSetting( 'idGeneratorInErrorPingLimiter' );
if ( !$idGeneratorInErrorPingLimiterValue || !$module->isFreshIdAssigned() ) {
return;
}
$apiMain->getUser()->pingLimiter( RateLimitingIdGenerator::RATELIMIT_NAME, $idGeneratorInErrorPingLimiterValue );
}
/** @param ContentLanguages[] &$contentLanguages */
public static function onWikibaseContentLanguages( array &$contentLanguages ): void {
if ( !WikibaseRepo::getSettings()->getSetting( 'tmpEnableMulLanguageCode' ) ) {
return;
}
if ( $contentLanguages[WikibaseContentLanguages::CONTEXT_TERM]->hasLanguage( 'mul' ) ) {
return;
}
$contentLanguages[WikibaseContentLanguages::CONTEXT_TERM] = new UnionContentLanguages(
$contentLanguages[WikibaseContentLanguages::CONTEXT_TERM],
new StaticContentLanguages( [ 'mul' ] )
);
}
public static function onMaintenanceShellStart(): void {
require_once __DIR__ . '/MaintenanceShellStart.php';
}
/**
* Handler for the VectorSearchResourceLoaderConfig hook to overwrite search pattern highlighting for wikibase
*/
public static function onVectorSearchResourceLoaderConfig( array &$vectorSearchConfig ): void {
$settings = WikibaseRepo::getSettings();
if ( $settings->getSetting( 'enableEntitySearchUI' ) === true ) {
$vectorSearchConfig['highlightQuery'] = false;
}
}
}