Page MenuHomeWickedGov Phorge

CategoryLinksTable.php
No OneTemporary

Size
11 KB
Referenced Files
None
Subscribers
None

CategoryLinksTable.php

<?php
namespace MediaWiki\Deferred\LinksUpdate;
use Collation;
use MediaWiki\Category\Category;
use MediaWiki\Config\Config;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\HookContainer\HookRunner;
use MediaWiki\JobQueue\JobQueueGroup;
use MediaWiki\JobQueue\Jobs\CategoryCountUpdateJob;
use MediaWiki\JobQueue\Utils\PurgeJobUtils;
use MediaWiki\Language\ILanguageConverter;
use MediaWiki\Languages\LanguageConverterFactory;
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\Page\PageReferenceValue;
use MediaWiki\Page\WikiPageFactory;
use MediaWiki\Parser\ParserOutput;
use MediaWiki\Parser\ParserOutputLinkTypes;
use MediaWiki\Parser\Sanitizer;
use MediaWiki\Storage\NameTableStore;
use MediaWiki\Title\NamespaceInfo;
use MediaWiki\Title\Title;
use Wikimedia\ObjectCache\WANObjectCache;
use Wikimedia\Rdbms\ILoadBalancer;
use Wikimedia\Rdbms\IResultWrapper;
/**
* categorylinks
*
* Link ID format: string[]
* - 0: Category name
* - 1: User-specified sort key (cl_sortkey_prefix)
*
* @since 1.38
*/
class CategoryLinksTable extends TitleLinksTable {
public const VIRTUAL_DOMAIN = 'virtual-categorylinks';
/**
* @var array Associative array of new links, with the category name in the
* key. The value is a list consisting of the sort key prefix and the sort
* key.
*/
private $newLinks = [];
/**
* @var array|null Associative array of existing links, or null if it has
* not been loaded yet
*/
private $existingLinks;
/**
* @var array Associative array of saved timestamps, if there is a force
* refresh due to a page move
*/
private $savedTimestamps = null;
/** @var ILanguageConverter */
private $languageConverter;
/** @var \Collation */
private $collation;
/** @var string The collation name for cl_collation */
private $collationName;
/** @var string The table name */
private $tableName = 'categorylinks';
/** @var bool */
private $isTempTable;
/** @var string The category type, which depends on the source page */
private $categoryType;
/** @var NamespaceInfo */
private $namespaceInfo;
/** @var WikiPageFactory */
private $wikiPageFactory;
private NameTableStore $collationNameStore;
private JobQueueGroup $jobQueueGroup;
private HookRunner $hookRunner;
/**
* @param LanguageConverterFactory $converterFactory
* @param NamespaceInfo $namespaceInfo
* @param WikiPageFactory $wikiPageFactory
* @param ILoadBalancer $loadBalancer
* @param WANObjectCache $WANObjectCache
* @param Config $config
* @param JobqueueGroup $jobQueueGroup
* @param HookContainer $hookContainer
* @param Collation $collation
* @param string $collationName
* @param string $tableName
* @param bool $isTempTable
*/
public function __construct(
LanguageConverterFactory $converterFactory,
NamespaceInfo $namespaceInfo,
WikiPageFactory $wikiPageFactory,
ILoadBalancer $loadBalancer,
WANObjectCache $WANObjectCache,
Config $config,
JobqueueGroup $jobQueueGroup,
HookContainer $hookContainer,
Collation $collation,
$collationName,
$tableName,
$isTempTable
) {
$this->languageConverter = $converterFactory->getLanguageConverter();
$this->namespaceInfo = $namespaceInfo;
$this->wikiPageFactory = $wikiPageFactory;
$this->collation = $collation;
$this->jobQueueGroup = $jobQueueGroup;
$this->hookRunner = new HookRunner( $hookContainer );
$this->collationName = $collationName;
$this->tableName = $tableName;
$this->isTempTable = $isTempTable;
$this->collationNameStore = new NameTableStore(
$loadBalancer,
$WANObjectCache,
LoggerFactory::getInstance( 'SecondaryDataUpdate' ),
'collation',
'collation_id',
'collation_name'
);
}
/**
* Cache the category type after the source page has been set
*/
public function startUpdate() {
$this->categoryType = $this->namespaceInfo
->getCategoryLinkType( $this->getSourcePage()->getNamespace() );
}
public function setParserOutput( ParserOutput $parserOutput ) {
$this->newLinks = [];
$sourceTitle = Title::castFromPageIdentity( $this->getSourcePage() );
$sortKeyInputs = [];
foreach (
$parserOutput->getLinkList( ParserOutputLinkTypes::CATEGORY )
as [ 'link' => $targetTitle, 'sort' => $sortKey ]
) {
'@phan-var string $sortKey'; // sort key will never be null
if ( $sortKey == '' ) {
$sortKey = $parserOutput->getPageProperty( "defaultsort" ) ?? '';
}
$sortKey = $this->languageConverter->convertCategoryKey( $sortKey );
// Clean up the sort key, regardless of source
$sortKey = Sanitizer::decodeCharReferences( $sortKey );
$sortKey = str_replace( "\n", '', $sortKey );
// If the sort key is longer then 255 bytes, it is truncated by DB,
// and then doesn't match when comparing existing vs current
// categories, causing T27254.
$sortKeyPrefix = mb_strcut( $sortKey, 0, 255 );
$name = $targetTitle->getDBkey();
$targetTitle = Title::castFromLinkTarget( $targetTitle );
$this->languageConverter->findVariantLink( $name, $targetTitle, true );
// Ignore the returned text, DB key should be used for links (T328477).
$name = $targetTitle->getDBKey();
// Treat custom sort keys as a prefix, so that if multiple
// things are forced to sort as '*' or something, they'll
// sort properly in the category rather than in page_id
// order or such.
$sortKeyInputs[$name] = $sourceTitle->getCategorySortkey( $sortKeyPrefix );
$this->newLinks[$name] = [ $sortKeyPrefix ];
}
$sortKeys = $this->collation->getSortKeys( $sortKeyInputs );
foreach ( $sortKeys as $name => $sortKey ) {
$this->newLinks[$name][1] = $sortKey;
}
}
/** @inheritDoc */
protected function getTableName() {
return $this->tableName;
}
/** @inheritDoc */
protected function getFromField() {
return 'cl_from';
}
/** @inheritDoc */
protected function getExistingFields() {
$fields = [ 'lt_title', 'cl_sortkey_prefix' ];
if ( $this->needForcedLinkRefresh() ) {
$fields[] = 'cl_timestamp';
}
return $fields;
}
/**
* Get the new link IDs. The link ID is a list with the name in the first
* element and the sort key prefix in the second element.
*
* @return iterable<array>
*/
protected function getNewLinkIDs() {
foreach ( $this->newLinks as $name => [ $prefix, ] ) {
yield [ (string)$name, $prefix ];
}
}
/**
* Get the existing links from the database
*/
private function fetchExistingLinks() {
$this->existingLinks = [];
$this->savedTimestamps = [];
$force = $this->needForcedLinkRefresh();
foreach ( $this->fetchExistingRows() as $row ) {
$this->existingLinks[$row->lt_title] = $row->cl_sortkey_prefix;
if ( $force ) {
$this->savedTimestamps[$row->lt_title] = $row->cl_timestamp;
}
}
}
/**
* Get the existing links as an associative array, with the category name
* in the key and the sort key prefix in the value.
*
* @return array
*/
private function getExistingLinks() {
if ( $this->existingLinks === null ) {
$this->fetchExistingLinks();
}
return $this->existingLinks;
}
private function getSavedTimestamps(): array {
if ( $this->savedTimestamps === null ) {
$this->fetchExistingLinks();
}
return $this->savedTimestamps;
}
/**
* @return \Generator
*/
protected function getExistingLinkIDs() {
foreach ( $this->getExistingLinks() as $name => $sortkey ) {
yield [ (string)$name, $sortkey ];
}
}
/** @inheritDoc */
protected function isExisting( $linkId ) {
$links = $this->getExistingLinks();
[ $name, $prefix ] = $linkId;
return \array_key_exists( $name, $links ) && $links[$name] === $prefix;
}
/** @inheritDoc */
protected function isInNewSet( $linkId ) {
[ $name, $prefix ] = $linkId;
return \array_key_exists( $name, $this->newLinks )
&& $this->newLinks[$name][0] === $prefix;
}
/** @inheritDoc */
protected function insertLink( $linkId ) {
[ $name, $prefix ] = $linkId;
$sortKey = $this->newLinks[$name][1];
$savedTimestamps = $this->getSavedTimestamps();
// Preserve cl_timestamp in the case of a forced refresh
$timestamp = $this->getDB()->timestamp( $savedTimestamps[$name] ?? 0 );
$targetFields = [];
$targetFields['cl_target_id'] = $this->linkTargetLookup->acquireLinkTargetId(
$this->makeTitle( $linkId ),
$this->getDB()
);
$targetFields['cl_collation_id'] = $this->collationNameStore->acquireId( $this->collationName );
$this->insertRow( $targetFields + [
'cl_sortkey' => $sortKey,
'cl_timestamp' => $timestamp,
'cl_sortkey_prefix' => $prefix,
'cl_type' => $this->categoryType,
] );
}
/** @inheritDoc */
protected function deleteLink( $linkId ) {
$this->deleteRow( [
'cl_target_id' => $this->linkTargetLookup->acquireLinkTargetId(
$this->makeTitle( $linkId ),
$this->getDB()
)
] );
}
/** @inheritDoc */
protected function needForcedLinkRefresh() {
// cl_sortkey and possibly cl_type will change if it is a page move
return $this->isMove();
}
/** @inheritDoc */
protected function makePageReferenceValue( $linkId ): PageReferenceValue {
return PageReferenceValue::localReference( NS_CATEGORY, $linkId[0] );
}
/** @inheritDoc */
protected function makeTitle( $linkId ): Title {
return Title::makeTitle( NS_CATEGORY, $linkId[0] );
}
/** @inheritDoc */
protected function deduplicateLinkIds( $linkIds ) {
$seen = [];
foreach ( $linkIds as $linkId ) {
if ( !\array_key_exists( $linkId[0], $seen ) ) {
$seen[$linkId[0]] = true;
yield $linkId;
}
}
}
protected function finishUpdate() {
if ( $this->isTempTable ) {
// Don't do invalidations for temporary collations
return;
}
// A update of sortkey on move is detected as insert + delete,
// but the categories does not need to update the counters or invalidate caches
$allInsertedLinks = array_column( $this->insertedLinks, 0 );
$allDeletedLinks = array_column( $this->deletedLinks, 0 );
$insertedLinks = array_diff( $allInsertedLinks, $allDeletedLinks );
$deletedLinks = array_diff( $allDeletedLinks, $allInsertedLinks );
$this->invalidateCategories( $insertedLinks, $deletedLinks );
if ( $insertedLinks || $deletedLinks ) {
$this->jobQueueGroup->lazyPush(
CategoryCountUpdateJob::newSpec(
$this->getSourcePage(),
$insertedLinks,
$deletedLinks,
$this->getBatchSize()
)
);
}
$wp = $this->wikiPageFactory->newFromTitle( $this->getSourcePage() );
foreach ( $insertedLinks as $catName ) {
$cat = Category::newFromName( $catName );
$this->hookRunner->onCategoryAfterPageAdded( $cat, $wp );
}
foreach ( $deletedLinks as $catName ) {
$cat = Category::newFromName( $catName );
$this->hookRunner->onCategoryAfterPageRemoved( $cat, $wp, $this->getSourcePage()->getId() );
}
}
private function invalidateCategories( array $insertedLinks, array $deletedLinks ) {
$changedCategoryNames = array_merge(
$insertedLinks,
$deletedLinks
);
PurgeJobUtils::invalidatePages(
$this->getDB(), NS_CATEGORY, $changedCategoryNames );
}
protected function linksTargetNormalizationStage(): int {
return SCHEMA_COMPAT_NEW;
}
/** @inheritDoc */
protected function virtualDomain(): string {
return self::VIRTUAL_DOMAIN;
}
protected function fetchExistingRows(): IResultWrapper {
return $this->getDB()->newSelectQueryBuilder()
->select( $this->getExistingFields() )
->from( $this->getTableName() )
->join( 'linktarget', null, [ 'cl_target_id=lt_id' ] )
->where( $this->getFromConds() )
->caller( __METHOD__ )
->fetchResultSet();
}
}

File Metadata

Mime Type
text/x-php
Expires
Wed, Aug 19, 02:58 (2 w, 1 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
da/06/5cfe08e133f16ecb242e3cb620fa
Default Alt Text
CategoryLinksTable.php (11 KB)

Event Timeline