diff options
author | Kunal Mehta <legoktm@debian.org> | 2022-01-14 15:35:37 -0800 |
---|---|---|
committer | Kunal Mehta <legoktm@debian.org> | 2022-01-14 15:38:18 -0800 |
commit | 6971d100e1ad851fde84664b1cd895250917855e (patch) | |
tree | 18b77499abe801759edac13a5db5af5ef3ae0509 | |
parent | 758e39cb3d8d54a326e19355869ad1c016f7acad (diff) |
Revert "LinksUpdate refactor" and follow-ups
This reverts commit 682aad7557ebb09c2aefa84d2c0c1f6c87ea5b76.
This reverts commit 87d8ccbd3e5280582a1bd60771b821ee5bbc95a7.
This reverts commit 1aecb692f64b3166cbaf1a7de9d85790ebc8759f.
This reverts commit d3b2b800678e91fd1a6177d80fde790c9006d423.
Bug: T299244
Change-Id: I9f17df9ad49c1cf75fcf36e1c1e7d72cf50caf15
36 files changed, 1374 insertions, 2867 deletions
diff --git a/autoload.php b/autoload.php index f0d0c8b8561f..fa1940419eb6 100644 --- a/autoload.php +++ b/autoload.php @@ -762,8 +762,8 @@ $wgAutoloadLocalClasses = [ 'LinkFilter' => __DIR__ . '/includes/LinkFilter.php', 'LinkHolderArray' => __DIR__ . '/includes/parser/LinkHolderArray.php', 'Linker' => __DIR__ . '/includes/Linker.php', - 'LinksDeletionUpdate' => __DIR__ . '/includes/deferred/LinksUpdate/LinksDeletionUpdate.php', - 'LinksUpdate' => __DIR__ . '/includes/deferred/LinksUpdate/LinksUpdate.php', + 'LinksDeletionUpdate' => __DIR__ . '/includes/deferred/LinksDeletionUpdate.php', + 'LinksUpdate' => __DIR__ . '/includes/deferred/LinksUpdate.php', 'ListToggle' => __DIR__ . '/includes/ListToggle.php', 'ListVariants' => __DIR__ . '/maintenance/language/listVariants.php', 'LoadBalancer' => __DIR__ . '/includes/libs/rdbms/loadbalancer/LoadBalancer.php', diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index ba745fd94dd5..634341fad873 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -148,7 +148,6 @@ class AutoLoader { 'MediaWiki\\Config\\' => __DIR__ . '/config/', 'MediaWiki\\Content\\' => __DIR__ . '/content/', 'MediaWiki\\DB\\' => __DIR__ . '/db/', - 'MediaWiki\\Deferred\\LinksUpdate\\' => __DIR__ . '/deferred/LinksUpdate/', 'MediaWiki\\Diff\\' => __DIR__ . '/diff/', 'MediaWiki\\Edit\\' => __DIR__ . '/edit/', 'MediaWiki\\EditPage\\' => __DIR__ . '/editpage/', diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 8e76c3605cf3..783831b72e8b 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -8858,17 +8858,6 @@ $wgCategoryPagingLimit = 200; $wgCategoryCollation = 'uppercase'; /** - * Additional category collations to store during LinksUpdate. This can be used - * to perform online migration of categories from one collation to another. An - * array of associative arrays each having the following keys: - * - table: (string) The table name - * - collation: (string) The collation to use for cl_sortkey - * - fakeCollation: (string) The collation name to insert into cl_collation - * @since 1.38 - */ -$wgTempCategoryCollations = []; - -/** * Array holding default tracking category names. * * Array contains the system messages for each tracking category. diff --git a/includes/MovePage.php b/includes/MovePage.php index e020aaf33b7f..1e2804b07792 100644 --- a/includes/MovePage.php +++ b/includes/MovePage.php @@ -693,6 +693,35 @@ class MovePage { $nullRevision = $moveAttemptResult; } + // Refresh the sortkey for this row. Be careful to avoid resetting + // cl_timestamp, which may disturb time-based lists on some sites. + // @todo This block should be killed, it's duplicating code + // from LinksUpdate::getCategoryInsertions() and friends. + $prefixes = $dbw->select( + 'categorylinks', + [ 'cl_sortkey_prefix', 'cl_to' ], + [ 'cl_from' => $pageid ], + __METHOD__ + ); + $type = $this->nsInfo->getCategoryLinkType( $this->newTitle->getNamespace() ); + $collation = $this->collationFactory->getCategoryCollation(); + foreach ( $prefixes as $prefixRow ) { + $prefix = $prefixRow->cl_sortkey_prefix; + $catTo = $prefixRow->cl_to; + $dbw->update( 'categorylinks', + [ + 'cl_sortkey' => $collation->getSortKey( + $this->newTitle->getCategorySortkey( $prefix ) ), + 'cl_collation' => $this->options->get( 'CategoryCollation' ), + 'cl_type' => $type, + 'cl_timestamp=cl_timestamp' ], + [ + 'cl_from' => $pageid, + 'cl_to' => $catTo ], + __METHOD__ + ); + } + $redirid = $this->oldTitle->getArticleID(); if ( $protected ) { @@ -752,6 +781,25 @@ class MovePage { $logEntry->publish( $logId ); } + // Update *_from_namespace fields as needed + if ( $this->oldTitle->getNamespace() != $this->newTitle->getNamespace() ) { + $dbw->update( 'pagelinks', + [ 'pl_from_namespace' => $this->newTitle->getNamespace() ], + [ 'pl_from' => $pageid ], + __METHOD__ + ); + $dbw->update( 'templatelinks', + [ 'tl_from_namespace' => $this->newTitle->getNamespace() ], + [ 'tl_from' => $pageid ], + __METHOD__ + ); + $dbw->update( 'imagelinks', + [ 'il_from_namespace' => $this->newTitle->getNamespace() ], + [ 'il_from' => $pageid ], + __METHOD__ + ); + } + # Update watchlists $oldtitle = $this->oldTitle->getDBkey(); $newtitle = $this->newTitle->getDBkey(); @@ -980,7 +1028,6 @@ class MovePage { $options = [ 'changed' => false, 'moved' => true, - 'oldtitle' => $this->oldTitle, 'oldcountable' => $oldcountable, 'causeAction' => 'edit-page', 'causeAgent' => $user->getName(), diff --git a/includes/Storage/DerivedPageDataUpdater.php b/includes/Storage/DerivedPageDataUpdater.php index 541d7fb467aa..1ff1fee40fc7 100644 --- a/includes/Storage/DerivedPageDataUpdater.php +++ b/includes/Storage/DerivedPageDataUpdater.php @@ -31,10 +31,10 @@ use IDBAccessObject; use InvalidArgumentException; use JobQueueGroup; use Language; +use LinksUpdate; use LogicException; use MediaWiki\Content\IContentHandlerFactory; use MediaWiki\Content\Transform\ContentTransformer; -use MediaWiki\Deferred\LinksUpdate\LinksUpdate; use MediaWiki\Edit\PreparedEdit; use MediaWiki\HookContainer\HookContainer; use MediaWiki\HookContainer\HookRunner; @@ -179,7 +179,6 @@ class DerivedPageDataUpdater implements IDBAccessObject, LoggerAwareInterface, P 'newrev' => false, 'created' => false, 'moved' => false, - 'oldtitle' => null, 'restored' => false, 'oldrevision' => null, 'oldcountable' => null, @@ -1136,7 +1135,6 @@ class DerivedPageDataUpdater implements IDBAccessObject, LoggerAwareInterface, P * - changed: bool, whether the revision changed the content (default true) * - created: bool, whether the revision created the page (default false) * - moved: bool, whether the page was moved (default false) - * - oldtitle: PageIdentity, if the page was moved this is the source title (default null) * - restored: bool, whether the page was undeleted (default false) * - oldrevision: RevisionRecord object for the pre-update revision (default null) * - triggeringUser: The user triggering the update (UserIdentity, defaults to the @@ -1426,9 +1424,6 @@ class DerivedPageDataUpdater implements IDBAccessObject, LoggerAwareInterface, P $parserOutput, $recursive ); - if ( $this->options['moved'] ) { - $linksUpdate->setMoveDetails( $this->options['oldtitle'] ); - } $allUpdates[] = $linksUpdate; // NOTE: Run updates for all slots, not just the modified slots! Otherwise, diff --git a/includes/collation/CollationFactory.php b/includes/collation/CollationFactory.php index 6710c5f7e437..060f810d674f 100644 --- a/includes/collation/CollationFactory.php +++ b/includes/collation/CollationFactory.php @@ -128,11 +128,7 @@ class CollationFactory { * @return Collation */ public function getCategoryCollation(): Collation { - return $this->makeCollation( $this->getDefaultCollationName() ); - } - - public function getDefaultCollationName(): string { - return $this->options->get( 'CategoryCollation' ); + return $this->makeCollation( $this->options->get( 'CategoryCollation' ) ); } /** diff --git a/includes/deferred/Hook/LinksUpdateAfterInsertHook.php b/includes/deferred/Hook/LinksUpdateAfterInsertHook.php index c251ff2964cb..b3a469c4a520 100644 --- a/includes/deferred/Hook/LinksUpdateAfterInsertHook.php +++ b/includes/deferred/Hook/LinksUpdateAfterInsertHook.php @@ -2,7 +2,7 @@ namespace MediaWiki\Hook; -use MediaWiki\Deferred\LinksUpdate\LinksUpdate; +use LinksUpdate; /** * This is a hook handler interface, see docs/Hooks.md. diff --git a/includes/deferred/Hook/LinksUpdateCompleteHook.php b/includes/deferred/Hook/LinksUpdateCompleteHook.php index c58461da292c..20f53ce3e89d 100644 --- a/includes/deferred/Hook/LinksUpdateCompleteHook.php +++ b/includes/deferred/Hook/LinksUpdateCompleteHook.php @@ -2,7 +2,7 @@ namespace MediaWiki\Hook; -use MediaWiki\Deferred\LinksUpdate\LinksUpdate; +use LinksUpdate; /** * This is a hook handler interface, see docs/Hooks.md. diff --git a/includes/deferred/Hook/LinksUpdateConstructedHook.php b/includes/deferred/Hook/LinksUpdateConstructedHook.php index d2af1c14e079..8cbba5397762 100644 --- a/includes/deferred/Hook/LinksUpdateConstructedHook.php +++ b/includes/deferred/Hook/LinksUpdateConstructedHook.php @@ -2,7 +2,7 @@ namespace MediaWiki\Hook; -use MediaWiki\Deferred\LinksUpdate\LinksUpdate; +use LinksUpdate; /** * This is a hook handler interface, see docs/Hooks.md. diff --git a/includes/deferred/Hook/LinksUpdateHook.php b/includes/deferred/Hook/LinksUpdateHook.php index b26f952b81eb..a4ffdba4ec38 100644 --- a/includes/deferred/Hook/LinksUpdateHook.php +++ b/includes/deferred/Hook/LinksUpdateHook.php @@ -2,7 +2,7 @@ namespace MediaWiki\Hook; -use MediaWiki\Deferred\LinksUpdate\LinksUpdate; +use LinksUpdate; /** * This is a hook handler interface, see docs/Hooks.md. diff --git a/includes/deferred/LinksUpdate/LinksDeletionUpdate.php b/includes/deferred/LinksDeletionUpdate.php index eb9ccde544cc..6b5c5d4ba5e4 100644 --- a/includes/deferred/LinksUpdate/LinksDeletionUpdate.php +++ b/includes/deferred/LinksDeletionUpdate.php @@ -19,18 +19,7 @@ * * @file */ - -namespace MediaWiki\Deferred\LinksUpdate; - -use Category; -use DeferredUpdates; -use EnqueueableDataUpdate; -use InvalidArgumentException; -use JobSpecification; use MediaWiki\MediaWikiServices; -use MWException; -use ParserOutput; -use WikiPage; /** * Update object handling the cleanup of links tables after a page was deleted. @@ -137,6 +126,3 @@ class LinksDeletionUpdate extends LinksUpdate implements EnqueueableDataUpdate { ]; } } - -/** @deprecated since 1.38 */ -class_alias( LinksDeletionUpdate::class, 'LinksDeletionUpdate' ); diff --git a/includes/deferred/LinksUpdate.php b/includes/deferred/LinksUpdate.php new file mode 100644 index 000000000000..b2a486149a46 --- /dev/null +++ b/includes/deferred/LinksUpdate.php @@ -0,0 +1,1298 @@ +<?php +/** + * Updater for link tracking tables after a page edit. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +use MediaWiki\HookContainer\ProtectedHookAccessorTrait; +use MediaWiki\Logger\LoggerFactory; +use MediaWiki\MediaWikiServices; +use MediaWiki\Page\PageIdentity; +use MediaWiki\Revision\RevisionRecord; +use MediaWiki\User\UserIdentity; +use Wikimedia\Rdbms\IDatabase; +use Wikimedia\ScopedCallback; + +/** + * Class the manages updates of *_link tables as well as similar extension-managed tables + * + * @note: LinksUpdate is managed by DeferredUpdates::execute(). Do not run this in a transaction. + * + * See docs/deferred.txt + */ +class LinksUpdate extends DataUpdate { + use ProtectedHookAccessorTrait; + + // @todo make members protected, but make sure extensions don't break + + /** @var int Page ID of the article linked from */ + public $mId; + + /** @var Title Title object of the article linked from */ + public $mTitle; + + /** @var ParserOutput */ + public $mParserOutput; + + /** + * @var int[][] Map of title strings to IDs for the links in the document + * @phan-var array<int,array<string,int>> + */ + public $mLinks; + + /** @var array DB keys of the images used, in the array key only */ + public $mImages; + + /** @var array Map of title strings to IDs for the template references, including broken ones */ + public $mTemplates; + + /** @var array URLs of external links, array key only */ + public $mExternals; + + /** @var array Map of category names to sort keys */ + public $mCategories; + + /** @var array Map of language codes to titles */ + public $mInterlangs; + + /** @var array 2-D map of (prefix => DBK => 1) */ + public $mInterwikis; + + /** @var array Map of arbitrary name to value */ + public $mProperties; + + /** @var bool Whether to queue jobs for recursive updates */ + public $mRecursive; + + /** @var RevisionRecord Revision for which this update has been triggered */ + private $mRevisionRecord; + + /** + * @var array[]|null Added links if calculated. + * @phan-var array<int,array{pl_from:int,pl_from_namespace:int,pl_namespace:int,pl_title:string}>|null + */ + private $linkInsertions = null; + + /** + * @var null|array Deleted links if calculated. + */ + private $linkDeletions = null; + + /** + * @var null|array[] Added external links if calculated. + */ + private $externalLinkInsertions = null; + + /** + * @var null|array Deleted external links if calculated. + */ + private $externalLinkDeletions = null; + + /** + * @var null|array Added properties if calculated. + */ + private $propertyInsertions = null; + + /** + * @var null|array Deleted properties if calculated. + */ + private $propertyDeletions = null; + + /** + * @var UserIdentity|null + */ + private $user; + + /** @var IDatabase */ + private $db; + + private $isStrictTestMode = false; + + /** + * @param PageIdentity $page The page we're updating + * @param ParserOutput $parserOutput Output from a full parse of this page + * @param bool $recursive Queue jobs for recursive updates? + * + * @throws MWException + */ + public function __construct( PageIdentity $page, ParserOutput $parserOutput, $recursive = true ) { + parent::__construct(); + + // NOTE: mTitle is public and used in hooks. Will need careful deprecation. + $this->mTitle = Title::castFromPageIdentity( $page ); + $this->mParserOutput = $parserOutput; + + $this->mLinks = $parserOutput->getLinks(); + $this->mImages = $parserOutput->getImages(); + $this->mTemplates = $parserOutput->getTemplates(); + $this->mExternals = $parserOutput->getExternalLinks(); + $this->mCategories = $parserOutput->getCategories(); + $this->mProperties = $parserOutput->getPageProperties(); + $this->mInterwikis = $parserOutput->getInterwikiLinks(); + + # Convert the format of the interlanguage links + # I didn't want to change it in the ParserOutput, because that array is passed all + # the way back to the skin, so either a skin API break would be required, or an + # inefficient back-conversion. + $ill = $parserOutput->getLanguageLinks(); + $this->mInterlangs = []; + foreach ( $ill as $link ) { + list( $key, $title ) = explode( ':', $link, 2 ); + $this->mInterlangs[$key] = $title; + } + + foreach ( $this->mCategories as &$sortkey ) { + # If the sortkey is longer then 255 bytes, it is truncated by DB, and then doesn't match + # when comparing existing vs current categories, causing T27254. + $sortkey = mb_strcut( $sortkey, 0, 255 ); + } + + $this->mRecursive = $recursive; + + $this->getHookRunner()->onLinksUpdateConstructed( $this ); + } + + /** + * Update link tables with outgoing links from an updated article + * + * @note this is managed by DeferredUpdates::execute(). Do not run this in a transaction. + */ + public function doUpdate() { + if ( !$this->mId ) { + // NOTE: subclasses may initialize mId directly! + $this->mId = $this->mTitle->getArticleID( Title::READ_LATEST ); + } + + if ( !$this->mId ) { + // Probably due to concurrent deletion or renaming of the page + $logger = LoggerFactory::getInstance( 'SecondaryDataUpdate' ); + $logger->notice( + 'LinksUpdate: The Title object yields no ID. Perhaps the page was deleted?', + [ + 'page_title' => $this->mTitle->getPrefixedDBkey(), + 'cause_action' => $this->getCauseAction(), + 'cause_agent' => $this->getCauseAgent() + ] + ); + + // nothing to do + return; + } + + if ( $this->ticket ) { + // Make sure all links update threads see the changes of each other. + // This handles the case when updates have to batched into several COMMITs. + $scopedLock = self::acquirePageLock( $this->getDB(), $this->mId ); + if ( !$scopedLock ) { + throw new RuntimeException( "Could not acquire lock for page ID '{$this->mId}'." ); + } + } + + $this->getHookRunner()->onLinksUpdate( $this ); + $this->doIncrementalUpdate(); + + // Commit and release the lock (if set) + ScopedCallback::consume( $scopedLock ); + // Run post-commit hook handlers without DBO_TRX + DeferredUpdates::addUpdate( new AutoCommitUpdate( + $this->getDB(), + __METHOD__, + function () { + $this->getHookRunner()->onLinksUpdateComplete( $this, $this->ticket ); + } + ) ); + } + + /** + * Acquire a session-level lock for performing link table updates for a page on a DB + * + * @param IDatabase $dbw + * @param int $pageId + * @param string $why One of (job, atomicity) + * @return ScopedCallback|null + * @since 1.27 + */ + public static function acquirePageLock( IDatabase $dbw, $pageId, $why = 'atomicity' ) { + $key = "{$dbw->getDomainID()}:LinksUpdate:$why:pageid:$pageId"; // per-wiki + $scopedLock = $dbw->getScopedLockAndFlush( $key, __METHOD__, 15 ); + if ( !$scopedLock ) { + $logger = LoggerFactory::getInstance( 'SecondaryDataUpdate' ); + $logger->info( "Could not acquire lock '{key}' for page ID '{page_id}'.", [ + 'key' => $key, + 'page_id' => $pageId, + ] ); + return null; + } + + return $scopedLock; + } + + protected function doIncrementalUpdate() { + # Page links + $existingPL = $this->getExistingLinks(); + $this->linkDeletions = $this->getLinkDeletions( $existingPL ); + $this->linkInsertions = $this->getLinkInsertions( $existingPL ); + $this->incrTableUpdate( 'pagelinks', 'pl', $this->linkDeletions, $this->linkInsertions ); + + # Image links + $existingIL = $this->getExistingImages(); + $imageDeletes = $this->getImageDeletions( $existingIL ); + $imageAdditions = $this->getImageAdditions( $existingIL ); + $this->incrTableUpdate( + 'imagelinks', + 'il', + $imageDeletes, + $this->getImageInsertions( $existingIL ) ); + + # Image change tags + $enabledTags = ChangeTags::getSoftwareTags(); + $mediaChangeTags = array_filter( [ + count( $imageAdditions ) && in_array( 'mw-add-media', $enabledTags ) ? 'mw-add-media' : '', + count( $imageDeletes ) && in_array( 'mw-remove-media', $enabledTags ) ? 'mw-remove-media' : '', + ] ); + $revisionRecord = $this->getRevisionRecord(); + if ( $revisionRecord && count( $mediaChangeTags ) ) { + ChangeTags::addTags( $mediaChangeTags, null, $revisionRecord->getId() ); + } + + # Invalidate all image description pages which had links added or removed + $imageUpdates = $imageDeletes + $imageAdditions; + $this->invalidateImageDescriptions( $imageUpdates ); + + # External links + $existingEL = $this->getExistingExternals(); + $this->externalLinkDeletions = $this->getExternalDeletions( $existingEL ); + $this->externalLinkInsertions = $this->getExternalInsertions( + $existingEL ); + $this->incrTableUpdate( + 'externallinks', + 'el', + $this->externalLinkDeletions, + $this->externalLinkInsertions ); + + # Language links + $existingLL = $this->getExistingInterlangs(); + $this->incrTableUpdate( + 'langlinks', + 'll', + $this->getInterlangDeletions( $existingLL ), + $this->getInterlangInsertions( $existingLL ) ); + + # Inline interwiki links + $existingIW = $this->getExistingInterwikis(); + $this->incrTableUpdate( + 'iwlinks', + 'iwl', + $this->getInterwikiDeletions( $existingIW ), + $this->getInterwikiInsertions( $existingIW ) ); + + # Template links + $existingTL = $this->getExistingTemplates(); + $this->incrTableUpdate( + 'templatelinks', + 'tl', + $this->getTemplateDeletions( $existingTL ), + $this->getTemplateInsertions( $existingTL ) ); + + # Category links + $existingCL = $this->getExistingCategories(); + $categoryDeletes = $this->getCategoryDeletions( $existingCL ); + $this->incrTableUpdate( + 'categorylinks', + 'cl', + $categoryDeletes, + $this->getCategoryInsertions( $existingCL ) ); + $categoryInserts = array_diff_assoc( $this->mCategories, $existingCL ); + $categoryUpdates = $categoryInserts + $categoryDeletes; + + # Page properties + $existingPP = $this->getExistingProperties(); + $this->propertyDeletions = $this->getPropertyDeletions( $existingPP ); + $this->incrTableUpdate( + 'page_props', + 'pp', + $this->propertyDeletions, + $this->getPropertyInsertions( $existingPP ) ); + + # Invalidate the necessary pages + $this->propertyInsertions = array_diff_assoc( $this->mProperties, $existingPP ); + $changed = $this->propertyDeletions + $this->propertyInsertions; + $this->invalidateProperties( $changed ); + + # Invalidate all categories which were added, deleted or changed (set symmetric difference) + $this->invalidateCategories( $categoryUpdates ); + $this->updateCategoryCounts( $categoryInserts, $categoryDeletes ); + + # Refresh links of all pages including this page + # This will be in a separate transaction + if ( $this->mRecursive ) { + $this->queueRecursiveJobs(); + } + + # Update the links table freshness for this title + $this->updateLinksTimestamp(); + } + + /** + * Queue recursive jobs for this page + * + * Which means do LinksUpdate on all pages that include the current page, + * using the job queue. + */ + protected function queueRecursiveJobs() { + $backlinkCache = MediaWikiServices::getInstance()->getBacklinkCacheFactory() + ->getBacklinkCache( $this->mTitle ); + $action = $this->getCauseAction(); + $agent = $this->getCauseAgent(); + + self::queueRecursiveJobsForTable( + $this->mTitle, 'templatelinks', $action, $agent, $backlinkCache + ); + if ( $this->mTitle->getNamespace() === NS_FILE ) { + // Process imagelinks in case the title is or was a redirect + self::queueRecursiveJobsForTable( + $this->mTitle, 'imagelinks', $action, $agent, $backlinkCache + ); + } + + // Get jobs for cascade-protected backlinks for a high priority queue. + // If meta-templates change to using a new template, the new template + // should be implicitly protected as soon as possible, if applicable. + // These jobs duplicate a subset of the above ones, but can run sooner. + // Which ever runs first generally no-ops the other one. + $jobs = []; + foreach ( $backlinkCache->getCascadeProtectedLinkPages() as $page ) { + $jobs[] = RefreshLinksJob::newPrioritized( + $page, + [ + 'causeAction' => $action, + 'causeAgent' => $agent + ] + ); + } + JobQueueGroup::singleton()->push( $jobs ); + } + + /** + * Queue a RefreshLinks job for any table. + * + * @param PageIdentity $page Page to do job for + * @param string $table Table to use (e.g. 'templatelinks') + * @param string $action Triggering action + * @param string $userName Triggering user name + * @param BacklinkCache|null $backlinkCache + */ + public static function queueRecursiveJobsForTable( + PageIdentity $page, $table, $action = 'unknown', $userName = 'unknown', ?BacklinkCache $backlinkCache = null + ) { + $title = Title::castFromPageIdentity( $page ); + if ( !$backlinkCache ) { + wfDeprecatedMsg( __METHOD__ . " needs a BacklinkCache object, null passed", '1.37' ); + $backlinkCache = MediaWikiServices::getInstance()->getBacklinkCacheFactory() + ->getBacklinkCache( $title ); + } + if ( $backlinkCache->hasLinks( $table ) ) { + $job = new RefreshLinksJob( + $title, + [ + 'table' => $table, + 'recursive' => true, + ] + Job::newRootJobParams( // "overall" refresh links job info + "refreshlinks:{$table}:{$title->getPrefixedText()}" + ) + [ 'causeAction' => $action, 'causeAgent' => $userName ] + ); + + JobQueueGroup::singleton()->push( $job ); + } + } + + /** + * @param array $cats + */ + private function invalidateCategories( $cats ) { + PurgeJobUtils::invalidatePages( + $this->getDB(), NS_CATEGORY, array_map( 'strval', array_keys( $cats ) ) + ); + } + + /** + * Update all the appropriate counts in the category table. + * @param array $added Associative array of category name => sort key + * @param array $deleted Associative array of category name => sort key + */ + private function updateCategoryCounts( array $added, array $deleted ) { + global $wgUpdateRowsPerQuery; + + if ( !$added && !$deleted ) { + return; + } + + $domainId = $this->getDB()->getDomainID(); + $services = MediaWikiServices::getInstance(); + $wp = $services->getWikiPageFactory()->newFromTitle( $this->mTitle ); + $lbf = $services->getDBLoadBalancerFactory(); + // T163801: try to release any row locks to reduce contention + $lbf->commitAndWaitForReplication( __METHOD__, $this->ticket, [ 'domain' => $domainId ] ); + + foreach ( array_chunk( array_keys( $added ), $wgUpdateRowsPerQuery ) as $addBatch ) { + $wp->updateCategoryCounts( array_map( 'strval', $addBatch ), [], $this->mId ); + $lbf->commitAndWaitForReplication( + __METHOD__, $this->ticket, [ 'domain' => $domainId ] ); + } + + foreach ( array_chunk( array_keys( $deleted ), $wgUpdateRowsPerQuery ) as $deleteBatch ) { + $wp->updateCategoryCounts( [], array_map( 'strval', $deleteBatch ), $this->mId ); + $lbf->commitAndWaitForReplication( + __METHOD__, $this->ticket, [ 'domain' => $domainId ] ); + } + } + + /** + * @param array $images + */ + private function invalidateImageDescriptions( array $images ) { + PurgeJobUtils::invalidatePages( + $this->getDB(), NS_FILE, array_map( 'strval', array_keys( $images ) ) + ); + } + + /** + * Update a table by doing a delete query then an insert query + * @param string $table Table name + * @param string $prefix Field name prefix + * @param array $deletions + * @param array $insertions Rows to insert + */ + private function incrTableUpdate( $table, $prefix, $deletions, $insertions ) { + $services = MediaWikiServices::getInstance(); + $bSize = $services->getMainConfig()->get( 'UpdateRowsPerQuery' ); + $lbf = $services->getDBLoadBalancerFactory(); + + if ( $table === 'page_props' ) { + $fromField = 'pp_page'; + } else { + $fromField = "{$prefix}_from"; + } + + $deleteWheres = []; // list of WHERE clause arrays for each DB delete() call + if ( $table === 'pagelinks' || $table === 'templatelinks' || $table === 'iwlinks' ) { + $baseKey = ( $table === 'iwlinks' ) ? 'iwl_prefix' : "{$prefix}_namespace"; + + $curBatchSize = 0; + $curDeletionBatch = []; + $deletionBatches = []; + foreach ( $deletions as $ns => $dbKeys ) { + foreach ( $dbKeys as $dbKey => $unused ) { + $curDeletionBatch[$ns][$dbKey] = 1; + if ( ++$curBatchSize >= $bSize ) { + $deletionBatches[] = $curDeletionBatch; + $curDeletionBatch = []; + $curBatchSize = 0; + } + } + } + if ( $curDeletionBatch ) { + $deletionBatches[] = $curDeletionBatch; + } + + foreach ( $deletionBatches as $deletionBatch ) { + $deleteWheres[] = [ + $fromField => $this->mId, + $this->getDB()->makeWhereFrom2d( $deletionBatch, $baseKey, "{$prefix}_title" ) + ]; + } + } else { + if ( $table === 'langlinks' ) { + $toField = 'll_lang'; + } elseif ( $table === 'page_props' ) { + $toField = 'pp_propname'; + } else { + $toField = $prefix . '_to'; + } + + $deletionBatches = array_chunk( array_keys( $deletions ), $bSize ); + foreach ( $deletionBatches as $deletionBatch ) { + $deleteWheres[] = [ + $fromField => $this->mId, + $toField => array_map( 'strval', $deletionBatch ) + ]; + } + } + + $domainId = $this->getDB()->getDomainID(); + + foreach ( $deleteWheres as $deleteWhere ) { + $this->getDB()->delete( $table, $deleteWhere, __METHOD__ ); + $lbf->commitAndWaitForReplication( + __METHOD__, $this->ticket, [ 'domain' => $domainId ] + ); + } + + $insertBatches = array_chunk( $insertions, $bSize ); + foreach ( $insertBatches as $insertBatch ) { + $this->getDB()->insert( $table, $insertBatch, __METHOD__, + $this->getConflictOption() ); + $lbf->commitAndWaitForReplication( + __METHOD__, $this->ticket, [ 'domain' => $domainId ] + ); + } + + if ( count( $insertions ) ) { + $this->getHookRunner()->onLinksUpdateAfterInsert( $this, $table, $insertions ); + } + } + + /** + * Omit conflict resolution options from the insert query so that testing + * can confirm that the incremental update logic was correct. + * + * @param bool $mode + */ + public function setStrictTestMode( $mode = true ) { + $this->isStrictTestMode = $mode; + } + + /** + * @return array + */ + private function getConflictOption() { + if ( $this->isStrictTestMode ) { + return []; + } else { + return [ 'IGNORE' ]; + } + } + + /** + * Get an array of pagelinks insertions for passing to the DB + * Skips the titles specified by the 2-D array $existing + * @param array $existing + * @return array[] + * @phan-return array<int,array{pl_from:int,pl_from_namespace:int,pl_namespace:int,pl_title:string}> + */ + private function getLinkInsertions( $existing = [] ) { + $arr = []; + foreach ( $this->mLinks as $ns => $dbkeys ) { + $diffs = isset( $existing[$ns] ) + ? array_diff_key( $dbkeys, $existing[$ns] ) + : $dbkeys; + foreach ( $diffs as $dbk => $id ) { + $arr[] = [ + 'pl_from' => $this->mId, + 'pl_from_namespace' => $this->mTitle->getNamespace(), + 'pl_namespace' => $ns, + 'pl_title' => $dbk + ]; + } + } + + return $arr; + } + + /** + * Get an array of template insertions. Like getLinkInsertions() + * @param array $existing + * @return array + */ + private function getTemplateInsertions( $existing = [] ) { + $arr = []; + foreach ( $this->mTemplates as $ns => $dbkeys ) { + $diffs = isset( $existing[$ns] ) ? array_diff_key( $dbkeys, $existing[$ns] ) : $dbkeys; + foreach ( $diffs as $dbk => $id ) { + $arr[] = [ + 'tl_from' => $this->mId, + 'tl_from_namespace' => $this->mTitle->getNamespace(), + 'tl_namespace' => $ns, + 'tl_title' => $dbk + ]; + } + } + + return $arr; + } + + /** + * Get an array of image insertions + * Skips the names specified in $existing + * @param array $existing + * @return array + */ + private function getImageInsertions( $existing = [] ) { + $arr = []; + $diffs = $this->getImageAdditions( $existing ); + foreach ( $diffs as $iname => $dummy ) { + $arr[] = [ + 'il_from' => $this->mId, + 'il_from_namespace' => $this->mTitle->getNamespace(), + 'il_to' => $iname + ]; + } + + return $arr; + } + + /** + * Get an array of externallinks insertions. Skips the names specified in $existing + * @param array $existing + * @return array[] + */ + private function getExternalInsertions( $existing = [] ) { + $arr = []; + $diffs = array_diff_key( $this->mExternals, $existing ); + foreach ( $diffs as $url => $dummy ) { + foreach ( LinkFilter::makeIndexes( $url ) as $index ) { + $arr[] = [ + 'el_from' => $this->mId, + 'el_to' => $url, + 'el_index' => $index, + 'el_index_60' => substr( $index, 0, 60 ), + ]; + } + } + + return $arr; + } + + /** + * Get an array of category insertions + * + * @param array $existing Mapping existing category names to sort keys. If both + * match a link in $this, the link will be omitted from the output + * + * @return array + */ + private function getCategoryInsertions( $existing = [] ) { + global $wgCategoryCollation; + $diffs = array_diff_assoc( $this->mCategories, $existing ); + $arr = []; + + $languageConverter = MediaWikiServices::getInstance()->getLanguageConverterFactory() + ->getLanguageConverter(); + + $collation = MediaWikiServices::getInstance()->getCollationFactory()->getCategoryCollation(); + foreach ( $diffs as $name => $prefix ) { + $nt = Title::makeTitleSafe( NS_CATEGORY, $name ); + $languageConverter->findVariantLink( $name, $nt, true ); + + $type = MediaWikiServices::getInstance()->getNamespaceInfo()-> + getCategoryLinkType( $this->mTitle->getNamespace() ); + + # Treat custom sortkeys 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. + $sortkey = $collation->getSortKey( $this->mTitle->getCategorySortkey( $prefix ) ); + + $arr[] = [ + 'cl_from' => $this->mId, + 'cl_to' => $name, + 'cl_sortkey' => $sortkey, + 'cl_timestamp' => $this->getDB()->timestamp(), + 'cl_sortkey_prefix' => $prefix, + 'cl_collation' => $wgCategoryCollation, + 'cl_type' => $type, + ]; + } + + return $arr; + } + + /** + * Get an array of interlanguage link insertions + * + * @param array $existing Mapping existing language codes to titles + * + * @return array + */ + private function getInterlangInsertions( $existing = [] ) { + $diffs = array_diff_assoc( $this->mInterlangs, $existing ); + $arr = []; + foreach ( $diffs as $lang => $title ) { + $arr[] = [ + 'll_from' => $this->mId, + 'll_lang' => $lang, + 'll_title' => $title + ]; + } + + return $arr; + } + + /** + * Get an array of page property insertions + * @param array $existing + * @return array + */ + private function getPropertyInsertions( $existing = [] ) { + $diffs = array_diff_assoc( $this->mProperties, $existing ); + + $arr = []; + foreach ( array_keys( $diffs ) as $name ) { + $arr[] = $this->getPagePropRowData( (string)$name ); + } + + return $arr; + } + + /** + * Returns an associative array to be used for inserting a row into + * the page_props table. Besides the given property name, this will + * include the page id from $this->mId and any property value from + * $this->mProperties. + * + * The array returned will include the pp_sortkey field. + * The sortkey value is currently determined by getPropertySortKeyValue(). + * + * @note this assumes that $this->mProperties[$prop] is defined. + * + * @param string $prop The name of the property. + * + * @return array + */ + private function getPagePropRowData( $prop ) { + $value = $this->mProperties[$prop]; + + return [ + 'pp_page' => $this->mId, + 'pp_propname' => $prop, + 'pp_value' => $value, + 'pp_sortkey' => $this->getPropertySortKeyValue( $value ) + ]; + } + + /** + * Determines the sort key for the given property value. + * This will return $value if it is a float or int, + * 1 or resp. 0 if it is a bool, and null otherwise. + * + * @note In the future, we may allow the sortkey to be specified explicitly + * in ParserOutput::setProperty. + * + * @param mixed $value + * + * @return float|null + */ + private function getPropertySortKeyValue( $value ) { + if ( is_int( $value ) || is_float( $value ) || is_bool( $value ) ) { + return floatval( $value ); + } + + return null; + } + + /** + * Get an array of interwiki insertions for passing to the DB + * Skips the titles specified by the 2-D array $existing + * @param array $existing + * @return array + */ + private function getInterwikiInsertions( $existing = [] ) { + $arr = []; + foreach ( $this->mInterwikis as $prefix => $dbkeys ) { + $diffs = isset( $existing[$prefix] ) + ? array_diff_key( $dbkeys, $existing[$prefix] ) + : $dbkeys; + + foreach ( $diffs as $dbk => $id ) { + $arr[] = [ + 'iwl_from' => $this->mId, + 'iwl_prefix' => $prefix, + 'iwl_title' => $dbk + ]; + } + } + + return $arr; + } + + /** + * Given an array of existing images, returns $this images that are not in there + * and thus should be added. + * @param array $existing + * @return array + */ + private function getImageAdditions( $existing ) { + return array_diff_key( $this->mImages, $existing ); + } + + /** + * Given an array of existing links, returns those links which are not in $this + * and thus should be deleted. + * @param array $existing + * @return array + */ + private function getLinkDeletions( $existing ) { + $del = []; + foreach ( $existing as $ns => $dbkeys ) { + if ( isset( $this->mLinks[$ns] ) ) { + $del[$ns] = array_diff_key( $dbkeys, $this->mLinks[$ns] ); + } else { + $del[$ns] = $dbkeys; + } + } + + return $del; + } + + /** + * Given an array of existing templates, returns those templates which are not in $this + * and thus should be deleted. + * @param array $existing + * @return array + */ + private function getTemplateDeletions( $existing ) { + $del = []; + foreach ( $existing as $ns => $dbkeys ) { + if ( isset( $this->mTemplates[$ns] ) ) { + $del[$ns] = array_diff_key( $dbkeys, $this->mTemplates[$ns] ); + } else { + $del[$ns] = $dbkeys; + } + } + + return $del; + } + + /** + * Given an array of existing images, returns those images which are not in $this + * and thus should be deleted. + * @param array $existing + * @return array + */ + private function getImageDeletions( $existing ) { + return array_diff_key( $existing, $this->mImages ); + } + + /** + * Given an array of existing external links, returns those links which are not + * in $this and thus should be deleted. + * @param array $existing + * @return array + */ + private function getExternalDeletions( $existing ) { + return array_diff_key( $existing, $this->mExternals ); + } + + /** + * Given an array of existing categories, returns those categories which are not in $this + * and thus should be deleted. + * @param array $existing + * @return array + */ + private function getCategoryDeletions( $existing ) { + return array_diff_assoc( $existing, $this->mCategories ); + } + + /** + * Given an array of existing interlanguage links, returns those links which are not + * in $this and thus should be deleted. + * @param array $existing + * @return array + */ + private function getInterlangDeletions( $existing ) { + return array_diff_assoc( $existing, $this->mInterlangs ); + } + + /** + * Get array of properties which should be deleted. + * @param array $existing + * @return array + */ + private function getPropertyDeletions( $existing ) { + return array_diff_assoc( $existing, $this->mProperties ); + } + + /** + * Given an array of existing interwiki links, returns those links which are not in $this + * and thus should be deleted. + * @param array $existing + * @return array + */ + private function getInterwikiDeletions( $existing ) { + $del = []; + foreach ( $existing as $prefix => $dbkeys ) { + if ( isset( $this->mInterwikis[$prefix] ) ) { + $del[$prefix] = array_diff_key( $dbkeys, $this->mInterwikis[$prefix] ); + } else { + $del[$prefix] = $dbkeys; + } + } + + return $del; + } + + /** + * Get an array of existing links, as a 2-D array + * + * @return array + */ + private function getExistingLinks() { + $res = $this->getDB()->select( 'pagelinks', [ 'pl_namespace', 'pl_title' ], + [ 'pl_from' => $this->mId ], __METHOD__ ); + $arr = []; + foreach ( $res as $row ) { + if ( !isset( $arr[$row->pl_namespace] ) ) { + $arr[$row->pl_namespace] = []; + } + $arr[$row->pl_namespace][$row->pl_title] = 1; + } + + return $arr; + } + + /** + * Get an array of existing templates, as a 2-D array + * + * @return array + */ + private function getExistingTemplates() { + $res = $this->getDB()->select( 'templatelinks', [ 'tl_namespace', 'tl_title' ], + [ 'tl_from' => $this->mId ], __METHOD__ ); + $arr = []; + foreach ( $res as $row ) { + if ( !isset( $arr[$row->tl_namespace] ) ) { + $arr[$row->tl_namespace] = []; + } + $arr[$row->tl_namespace][$row->tl_title] = 1; + } + + return $arr; + } + + /** + * Get an array of existing images, image names in the keys + * + * @return array + */ + private function getExistingImages() { + $res = $this->getDB()->select( 'imagelinks', [ 'il_to' ], + [ 'il_from' => $this->mId ], __METHOD__ ); + $arr = []; + foreach ( $res as $row ) { + $arr[$row->il_to] = 1; + } + + return $arr; + } + + /** + * Get an array of existing external links, URLs in the keys + * + * @return array + */ + private function getExistingExternals() { + $res = $this->getDB()->select( 'externallinks', [ 'el_to' ], + [ 'el_from' => $this->mId ], __METHOD__ ); + $arr = []; + foreach ( $res as $row ) { + $arr[$row->el_to] = 1; + } + + return $arr; + } + + /** + * Get an array of existing categories, with the name in the key and sort key in the value. + * + * @return array + */ + private function getExistingCategories() { + $res = $this->getDB()->select( 'categorylinks', [ 'cl_to', 'cl_sortkey_prefix' ], + [ 'cl_from' => $this->mId ], __METHOD__ ); + $arr = []; + foreach ( $res as $row ) { + $arr[$row->cl_to] = $row->cl_sortkey_prefix; + } + + return $arr; + } + + /** + * Get an array of existing interlanguage links, with the language code in the key and the + * title in the value. + * + * @return array + */ + private function getExistingInterlangs() { + $res = $this->getDB()->select( 'langlinks', [ 'll_lang', 'll_title' ], + [ 'll_from' => $this->mId ], __METHOD__ ); + $arr = []; + foreach ( $res as $row ) { + $arr[$row->ll_lang] = $row->ll_title; + } + + return $arr; + } + + /** + * Get an array of existing inline interwiki links, as a 2-D array + * @return array [ prefix => [ dbkey => 1 ] ] + */ + private function getExistingInterwikis() { + $res = $this->getDB()->select( 'iwlinks', [ 'iwl_prefix', 'iwl_title' ], + [ 'iwl_from' => $this->mId ], __METHOD__ ); + $arr = []; + foreach ( $res as $row ) { + if ( !isset( $arr[$row->iwl_prefix] ) ) { + $arr[$row->iwl_prefix] = []; + } + $arr[$row->iwl_prefix][$row->iwl_title] = 1; + } + + return $arr; + } + + /** + * Get an array of existing categories, with the name in the key and sort key in the value. + * + * @return array Array of property names and values + */ + private function getExistingProperties() { + $res = $this->getDB()->select( 'page_props', [ 'pp_propname', 'pp_value' ], + [ 'pp_page' => $this->mId ], __METHOD__ ); + $arr = []; + foreach ( $res as $row ) { + $arr[$row->pp_propname] = $row->pp_value; + } + + return $arr; + } + + /** + * Return the title object of the page being updated + * @return Title + */ + public function getTitle() { + return $this->mTitle; + } + + /** + * Get the page_id of the page being updated + * + * @since 1.38 + * @return int + */ + public function getPageId() { + if ( $this->mId ) { + return $this->mId; + } else { + return $this->mTitle->getArticleID(); + } + } + + /** + * Returns parser output + * @since 1.19 + * @return ParserOutput + */ + public function getParserOutput() { + return $this->mParserOutput; + } + + /** + * Return the list of images used as generated by the parser + * @return array + */ + public function getImages() { + return $this->mImages; + } + + /** + * Set the RevisionRecord corresponding to this LinksUpdate + * + * @since 1.35 + * @param RevisionRecord $revisionRecord + */ + public function setRevisionRecord( RevisionRecord $revisionRecord ) { + $this->mRevisionRecord = $revisionRecord; + } + + /** + * @since 1.35 + * @return RevisionRecord|null + */ + public function getRevisionRecord() { + return $this->mRevisionRecord; + } + + /** + * Set the user who triggered this LinksUpdate + * + * @since 1.27 + * @param UserIdentity $user + */ + public function setTriggeringUser( UserIdentity $user ) { + $this->user = $user; + } + + /** + * Get the user who triggered this LinksUpdate + * + * @since 1.27 + * @return UserIdentity|null + */ + public function getTriggeringUser(): ?UserIdentity { + return $this->user; + } + + /** + * Invalidate any necessary link lists related to page property changes + * @param array $changed + */ + private function invalidateProperties( $changed ) { + global $wgPagePropLinkInvalidations; + + $jobs = []; + foreach ( $changed as $name => $value ) { + if ( isset( $wgPagePropLinkInvalidations[$name] ) ) { + $inv = $wgPagePropLinkInvalidations[$name]; + if ( !is_array( $inv ) ) { + $inv = [ $inv ]; + } + foreach ( $inv as $table ) { + $jobs[] = HTMLCacheUpdateJob::newForBacklinks( + $this->mTitle, + $table, + [ 'causeAction' => 'page-props' ] + ); + } + } + } + + JobQueueGroup::singleton()->lazyPush( $jobs ); + } + + /** + * Fetch page links added by this LinksUpdate. Only available after the update is complete. + * @since 1.22 + * @return null|array Array of Titles + */ + public function getAddedLinks() { + if ( $this->linkInsertions === null ) { + return null; + } + $result = []; + foreach ( $this->linkInsertions as $insertion ) { + $result[] = Title::makeTitle( $insertion['pl_namespace'], $insertion['pl_title'] ); + } + + return $result; + } + + /** + * Fetch page links removed by this LinksUpdate. Only available after the update is complete. + * @since 1.22 + * @return null|array Array of Titles + */ + public function getRemovedLinks() { + if ( $this->linkDeletions === null ) { + return null; + } + $result = []; + foreach ( $this->linkDeletions as $ns => $titles ) { + foreach ( $titles as $title => $unused ) { + $result[] = Title::makeTitle( $ns, $title ); + } + } + + return $result; + } + + /** + * Fetch external links added by this LinksUpdate. Only available after + * the update is complete. + * @since 1.33 + * @return null|array Array of Strings + */ + public function getAddedExternalLinks() { + if ( $this->externalLinkInsertions === null ) { + return null; + } + return array_column( $this->externalLinkInsertions, 'el_to' ); + } + + /** + * Fetch external links removed by this LinksUpdate. Only available after + * the update is complete. + * @since 1.33 + * @return null|string[] + */ + public function getRemovedExternalLinks() { + if ( $this->externalLinkDeletions === null ) { + return null; + } + return array_keys( $this->externalLinkDeletions ); + } + + /** + * Fetch page properties added by this LinksUpdate. + * Only available after the update is complete. + * @since 1.28 + * @return null|array + */ + public function getAddedProperties() { + return $this->propertyInsertions; + } + + /** + * Fetch page properties removed by this LinksUpdate. + * Only available after the update is complete. + * @since 1.28 + * @return null|array + */ + public function getRemovedProperties() { + return $this->propertyDeletions; + } + + /** + * Update links table freshness + */ + private function updateLinksTimestamp() { + if ( $this->mId ) { + // The link updates made here only reflect the freshness of the parser output + $timestamp = $this->mParserOutput->getCacheTime(); + $this->getDB()->update( 'page', + [ 'page_links_updated' => $this->getDB()->timestamp( $timestamp ) ], + [ 'page_id' => $this->mId ], + __METHOD__ + ); + } + } + + /** + * @return IDatabase + */ + protected function getDB() { + if ( !$this->db ) { + $this->db = wfGetDB( DB_PRIMARY ); + } + + return $this->db; + } + + /** + * Whether or not this LinksUpdate will also update pages which transclude the + * current page or otherwise depend on it. + * + * @return bool + */ + public function isRecursive() { + return $this->mRecursive; + } +} diff --git a/includes/deferred/LinksUpdate/CategoryLinksTable.php b/includes/deferred/LinksUpdate/CategoryLinksTable.php deleted file mode 100644 index a40ceff3a7da..000000000000 --- a/includes/deferred/LinksUpdate/CategoryLinksTable.php +++ /dev/null @@ -1,317 +0,0 @@ -<?php - -namespace MediaWiki\Deferred\LinksUpdate; - -use Collation; -use MediaWiki\DAO\WikiAwareEntity; -use MediaWiki\Languages\LanguageConverterFactory; -use MediaWiki\Page\PageReferenceValue; -use MediaWiki\Page\WikiPageFactory; -use NamespaceInfo; -use ParserOutput; -use PurgeJobUtils; -use Title; - -/** - * categorylinks - * - * Link ID format: string[] - * - 0: Category name - * - 1: User-specified sort key (cl_sortkey_prefix) - * - * @since 1.38 - */ -class CategoryLinksTable extends TitleLinksTable { - /** - * @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; - - /** - * @param LanguageConverterFactory $converterFactory - * @param NamespaceInfo $namespaceInfo - * @param WikiPageFactory $wikiPageFactory - * @param Collation $collation - * @param string $collationName - * @param string $tableName - * @param bool $isTempTable - */ - public function __construct( - LanguageConverterFactory $converterFactory, - NamespaceInfo $namespaceInfo, - WikiPageFactory $wikiPageFactory, - Collation $collation, - $collationName, - $tableName, - $isTempTable - ) { - $this->languageConverter = $converterFactory->getLanguageConverter(); - $this->namespaceInfo = $namespaceInfo; - $this->wikiPageFactory = $wikiPageFactory; - $this->collation = $collation; - $this->collationName = $collationName; - $this->tableName = $tableName; - $this->isTempTable = $isTempTable; - } - - /** - * 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->getCategories() as $name => $sortKeyPrefix ) { - // 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( $sortKeyPrefix, 0, 255 ); - - $targetTitle = Title::makeTitleSafe( NS_CATEGORY, $name ); - $this->languageConverter->findVariantLink( $name, $targetTitle, true ); - - // 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; - } - } - - protected function getTableName() { - return $this->tableName; - } - - protected function getFromField() { - return 'cl_from'; - } - - protected function getExistingFields() { - $fields = [ 'cl_to', '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, $sortKey ] ) { - yield [ $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->cl_to] = $row->cl_sortkey_prefix; - if ( $force ) { - $this->savedTimestamps[$row->cl_to] = $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() { - if ( $this->savedTimestamps === null ) { - $this->fetchExistingLinks(); - } - return $this->savedTimestamps; - } - - /** - * @return \Generator - */ - protected function getExistingLinkIDs() { - foreach ( $this->getExistingLinks() as $name => $sortkey ) { - yield [ $name, $sortkey ]; - } - } - - protected function isExisting( $linkId ) { - $links = $this->getExistingLinks(); - [ $name, $prefix ] = $linkId; - return \array_key_exists( $name, $links ) && $links[$name] === $prefix; - } - - protected function isInNewSet( $linkId ) { - [ $name, $prefix ] = $linkId; - return \array_key_exists( $name, $this->newLinks ) - && $this->newLinks[$name][0] === $prefix; - } - - 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 ); - - $this->insertRow( [ - 'cl_to' => $name, - 'cl_sortkey' => $sortKey, - 'cl_timestamp' => $timestamp, - 'cl_sortkey_prefix' => $prefix, - 'cl_collation' => $this->collationName, - 'cl_type' => $this->categoryType, - ] ); - } - - protected function deleteLink( $linkId ) { - $this->deleteRow( [ 'cl_to' => $linkId[0] ] ); - } - - protected function needForcedLinkRefresh() { - // cl_sortkey and possibly cl_type will change if it is a page move - return $this->isMove(); - } - - protected function makePageReferenceValue( $linkId ): PageReferenceValue { - return new PageReferenceValue( NS_CATEGORY, $linkId[0], WikiAwareEntity::LOCAL ); - } - - protected function makeTitle( $linkId ): Title { - return Title::makeTitle( NS_CATEGORY, $linkId[0] ); - } - - 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; - } - $this->invalidateCategories(); - $this->updateCategoryCounts(); - } - - private function invalidateCategories() { - $changedCategoryNames = array_unique( array_merge( - array_column( $this->insertedLinks, 0 ), - array_column( $this->deletedLinks, 0 ) - ) ); - PurgeJobUtils::invalidatePages( - $this->getDB(), NS_CATEGORY, $changedCategoryNames ); - } - - /** - * Update all the appropriate counts in the category table. - */ - private function updateCategoryCounts() { - if ( !$this->insertedLinks && !$this->deletedLinks ) { - return; - } - - $domainId = $this->getDB()->getDomainID(); - $wp = $this->wikiPageFactory->newFromTitle( $this->getSourcePage() ); - $lbf = $this->getLBFactory(); - $size = $this->getBatchSize(); - // T163801: try to release any row locks to reduce contention - $lbf->commitAndWaitForReplication( - __METHOD__, $this->getTransactionTicket(), [ 'domain' => $domainId ] ); - - if ( count( $this->insertedLinks ) + count( $this->deletedLinks ) < $size ) { - $wp->updateCategoryCounts( - array_column( $this->insertedLinks, 0 ), - array_column( $this->deletedLinks, 0 ), - $this->getSourcePageId() - ); - $lbf->commitAndWaitForReplication( - __METHOD__, $this->getTransactionTicket(), [ 'domain' => $domainId ] ); - } else { - $addedChunks = array_chunk( array_column( $this->insertedLinks, 0 ), $size ); - foreach ( $addedChunks as $chunk ) { - $wp->updateCategoryCounts( $chunk, [], $this->getSourcePageId() ); - $lbf->commitAndWaitForReplication( - __METHOD__, $this->getTransactionTicket(), [ 'domain' => $domainId ] ); - } - - $deletedChunks = array_chunk( array_column( $this->deletedLinks, 0 ), $size ); - foreach ( $deletedChunks as $chunk ) { - $wp->updateCategoryCounts( [], $chunk, $this->getSourcePageId() ); - $lbf->commitAndWaitForReplication( - __METHOD__, $this->getTransactionTicket(), [ 'domain' => $domainId ] ); - } - - } - } -} diff --git a/includes/deferred/LinksUpdate/ExternalLinksTable.php b/includes/deferred/LinksUpdate/ExternalLinksTable.php deleted file mode 100644 index 12a8db877e7b..000000000000 --- a/includes/deferred/LinksUpdate/ExternalLinksTable.php +++ /dev/null @@ -1,99 +0,0 @@ -<?php - -namespace MediaWiki\Deferred\LinksUpdate; - -use LinkFilter; -use ParserOutput; - -/** - * externallinks - * - * Link ID format: string URL - * - * @since 1.38 - */ -class ExternalLinksTable extends LinksTable { - private $newLinks = []; - private $existingLinks; - - public function setParserOutput( ParserOutput $parserOutput ) { - $this->newLinks = $parserOutput->getExternalLinks(); - } - - protected function getTableName() { - return 'externallinks'; - } - - protected function getFromField() { - return 'el_from'; - } - - protected function getExistingFields() { - return [ 'el_to' ]; - } - - /** - * Get the existing links as an array, where the key is the URL and the - * value is unused. - * - * @return array - */ - private function getExistingLinks() { - if ( $this->existingLinks === null ) { - $this->existingLinks = []; - foreach ( $this->fetchExistingRows() as $row ) { - $this->existingLinks[$row->el_to] = true; - } - } - return $this->existingLinks; - } - - protected function getNewLinkIDs() { - foreach ( $this->newLinks as $link => $unused ) { - yield $link; - } - } - - protected function getExistingLinkIDs() { - foreach ( $this->getExistingLinks() as $link => $unused ) { - yield $link; - } - } - - protected function isExisting( $linkId ) { - return \array_key_exists( $linkId, $this->getExistingLinks() ); - } - - protected function isInNewSet( $linkId ) { - return \array_key_exists( $linkId, $this->newLinks ); - } - - protected function insertLink( $linkId ) { - foreach ( LinkFilter::makeIndexes( $linkId ) as $index ) { - $this->insertRow( [ - 'el_to' => $linkId, - 'el_index' => $index, - 'el_index_60' => substr( $index, 0, 60 ), - ] ); - } - } - - protected function deleteLink( $linkId ) { - $this->deleteRow( [ 'el_to' => $linkId ] ); - } - - /** - * Get an array of URLs of the given type - * - * @param int $setType One of the link set constants as in LinksTable::getLinkIDs() - * @return string[] - */ - public function getStringArray( $setType ) { - $ids = $this->getLinkIDs( $setType ); - if ( is_array( $ids ) ) { - return $ids; - } else { - return iterator_to_array( $ids ); - } - } -} diff --git a/includes/deferred/LinksUpdate/GenericPageLinksTable.php b/includes/deferred/LinksUpdate/GenericPageLinksTable.php deleted file mode 100644 index a1bcd845218f..000000000000 --- a/includes/deferred/LinksUpdate/GenericPageLinksTable.php +++ /dev/null @@ -1,140 +0,0 @@ -<?php - -namespace MediaWiki\Deferred\LinksUpdate; - -use MediaWiki\DAO\WikiAwareEntity; -use MediaWiki\Page\PageReferenceValue; -use Title; - -/** - * Shared code for pagelinks and templatelinks. They are very similar tables - * since they both link to an arbitrary page identified by namespace and title. - * - * Link ID format: string[]: - * - 0: namespace ID - * - 1: title DB key - * - * @since 1.38 - */ -abstract class GenericPageLinksTable extends TitleLinksTable { - /** - * A 2d array representing the new links, with the namespace ID in the - * first key, the DB key in the second key, and the value arbitrary. - * - * @var array - */ - protected $newLinks = []; - - /** - * The existing links in the same format as self::$newLinks, or null if it - * has not been loaded yet. - * - * @var array|null - */ - private $existingLinks; - - /** - * Get the namespace field name - * - * @return string - */ - abstract protected function getNamespaceField(); - - /** - * Get the title (DB key) field name - * - * @return string - */ - abstract protected function getTitleField(); - - /** - * @return mixed - */ - abstract protected function getFromNamespaceField(); - - protected function getExistingFields() { - return [ - 'ns' => $this->getNamespaceField(), - 'title' => $this->getTitleField() - ]; - } - - /** - * Get existing links as an associative array - * - * @return array - */ - private function getExistingLinks() { - if ( $this->existingLinks === null ) { - $this->existingLinks = []; - foreach ( $this->fetchExistingRows() as $row ) { - $this->existingLinks[$row->ns][$row->title] = 1; - } - } - - return $this->existingLinks; - } - - protected function getNewLinkIDs() { - foreach ( $this->newLinks as $ns => $links ) { - foreach ( $links as $dbk => $unused ) { - yield [ $ns, $dbk ]; - } - } - } - - protected function getExistingLinkIDs() { - foreach ( $this->getExistingLinks() as $ns => $links ) { - foreach ( $links as $dbk => $unused ) { - yield [ $ns, $dbk ]; - } - } - } - - protected function isExisting( $linkId ) { - [ $ns, $dbk ] = $linkId; - return isset( $this->getExistingLinks()[$ns][$dbk] ); - } - - protected function isInNewSet( $linkId ) { - [ $ns, $dbk ] = $linkId; - return isset( $this->newLinks[$ns][$dbk] ); - } - - protected function insertLink( $linkId ) { - $this->insertRow( [ - $this->getFromNamespaceField() => $this->getSourcePage()->getNamespace(), - $this->getNamespaceField() => $linkId[0], - $this->getTitleField() => $linkId[1] - ] ); - } - - protected function deleteLink( $linkId ) { - $this->deleteRow( [ - $this->getNamespaceField() => $linkId[0], - $this->getTitleField() => $linkId[1] - ] ); - } - - protected function needForcedLinkRefresh() { - return $this->isCrossNamespaceMove(); - } - - protected function makePageReferenceValue( $linkId ): PageReferenceValue { - return new PageReferenceValue( $linkId[0], $linkId[1], WikiAwareEntity::LOCAL ); - } - - protected function makeTitle( $linkId ): Title { - return Title::makeTitle( $linkId[0], $linkId[1] ); - } - - protected function deduplicateLinkIds( $linkIds ) { - $seen = []; - foreach ( $linkIds as $linkId ) { - if ( !isset( $seen[$linkId[0]][$linkId[1]] ) ) { - $seen[$linkId[0]][$linkId[1]] = true; - yield $linkId; - } - } - } -} diff --git a/includes/deferred/LinksUpdate/ImageLinksTable.php b/includes/deferred/LinksUpdate/ImageLinksTable.php deleted file mode 100644 index 8ecfb2f5a8b4..000000000000 --- a/includes/deferred/LinksUpdate/ImageLinksTable.php +++ /dev/null @@ -1,142 +0,0 @@ -<?php - -namespace MediaWiki\Deferred\LinksUpdate; - -use ChangeTags; -use MediaWiki\DAO\WikiAwareEntity; -use MediaWiki\Page\PageReferenceValue; -use ParserOutput; -use PurgeJobUtils; -use Title; - -/** - * imagelinks - * - * Link ID format: string image name - * - * @since 1.38 - */ -class ImageLinksTable extends TitleLinksTable { - /** - * @var array New links with the name in the key, value arbitrary - */ - private $newLinks; - - /** - * @var array Existing links with the name in the key, value arbitrary - */ - private $existingLinks; - - public function setParserOutput( ParserOutput $parserOutput ) { - $this->newLinks = $parserOutput->getImages(); - } - - protected function getTableName() { - return 'imagelinks'; - } - - protected function getFromField() { - return 'il_from'; - } - - protected function getExistingFields() { - return [ 'il_to' ]; - } - - protected function getNewLinkIDs() { - foreach ( $this->newLinks as $link => $unused ) { - yield (string)$link; - } - } - - /** - * Get existing links with the name in the key, value arbitrary. - * - * @return array - */ - private function getExistingLinks() { - if ( $this->existingLinks === null ) { - $this->existingLinks = []; - foreach ( $this->fetchExistingRows() as $row ) { - $this->existingLinks[$row->il_to] = true; - } - } - return $this->existingLinks; - } - - protected function getExistingLinkIDs() { - foreach ( $this->getExistingLinks() as $link => $unused ) { - yield $link; - } - } - - protected function isExisting( $linkId ) { - return \array_key_exists( $linkId, $this->getExistingLinks() ); - } - - protected function isInNewSet( $linkId ) { - return \array_key_exists( $linkId, $this->newLinks ); - } - - protected function needExistingLinkRefresh() { - return $this->isCrossNamespaceMove(); - } - - protected function insertLink( $linkId ) { - $this->insertRow( [ - 'il_from_namespace' => $this->getSourcePage()->getNamespace(), - 'il_to' => $linkId - ] ); - } - - protected function deleteLink( $linkId ) { - $this->deleteRow( [ 'il_to' => $linkId ] ); - } - - protected function makePageReferenceValue( $linkId ): PageReferenceValue { - return new PageReferenceValue( NS_FILE, $linkId, WikiAwareEntity::LOCAL ); - } - - protected function makeTitle( $linkId ): Title { - return Title::makeTitle( NS_FILE, $linkId ); - } - - protected function deduplicateLinkIds( $linkIds ) { - if ( !is_array( $linkIds ) ) { - $linkIds = iterator_to_array( $linkIds ); - } - return array_unique( $linkIds ); - } - - protected function finishUpdate() { - $this->updateChangeTags(); - $this->invalidateImageDescriptions(); - } - - /** - * Add the mw-add-media or mw-remove-media change tags to the edit if appropriate - */ - private function updateChangeTags() { - $enabledTags = ChangeTags::getSoftwareTags(); - $mediaChangeTags = []; - if ( count( $this->insertedLinks ) && in_array( 'mw-add-media', $enabledTags ) ) { - $mediaChangeTags[] = 'mw-add-media'; - } - if ( count( $this->deletedLinks ) && in_array( 'mw-remove-media', $enabledTags ) ) { - $mediaChangeTags[] = 'mw-remove-media'; - } - $revisionRecord = $this->getRevision(); - if ( $revisionRecord && count( $mediaChangeTags ) ) { - ChangeTags::addTags( $mediaChangeTags, null, $revisionRecord->getId() ); - } - } - - /** - * Invalidate all image description pages which had links added or removed - */ - private function invalidateImageDescriptions() { - PurgeJobUtils::invalidatePages( - $this->getDB(), NS_FILE, - array_merge( $this->insertedLinks, $this->deletedLinks ) ); - } -} diff --git a/includes/deferred/LinksUpdate/InterwikiLinksTable.php b/includes/deferred/LinksUpdate/InterwikiLinksTable.php deleted file mode 100644 index a5b4f06099b5..000000000000 --- a/includes/deferred/LinksUpdate/InterwikiLinksTable.php +++ /dev/null @@ -1,97 +0,0 @@ -<?php - -namespace MediaWiki\Deferred\LinksUpdate; - -use ParserOutput; - -/** - * iwlinks - * - * Link ID format: string[] - * - 0: Interwiki prefix - * - 1: Foreign title - * - * @since 1.38 - */ -class InterwikiLinksTable extends LinksTable { - /** @var array */ - private $newLinks = []; - - /** @var array|null */ - private $existingLinks; - - public function setParserOutput( ParserOutput $parserOutput ) { - $this->newLinks = $parserOutput->getInterwikiLinks(); - } - - protected function getTableName() { - return 'iwlinks'; - } - - protected function getFromField() { - return 'iwl_from'; - } - - protected function getExistingFields() { - return [ 'iwl_prefix', 'iwl_title' ]; - } - - protected function getNewLinkIDs() { - foreach ( $this->newLinks as $prefix => $links ) { - foreach ( $links as $title => $unused ) { - yield [ $prefix, $title ]; - } - } - } - - /** - * Get the existing links as a 2-d array, with the prefix in the first key, - * the title in the second key, and the value arbitrary. - * - * @return array|null - */ - private function getExistingLinks() { - if ( $this->existingLinks === null ) { - $this->existingLinks = []; - foreach ( $this->fetchExistingRows() as $row ) { - $this->existingLinks[$row->iwl_prefix][$row->iwl_title] = true; - } - } - return $this->existingLinks; - } - - protected function getExistingLinkIDs() { - foreach ( $this->getExistingLinks() as $prefix => $links ) { - foreach ( $links as $title => $unused ) { - yield [ $prefix, $title ]; - } - } - } - - protected function isExisting( $linkId ) { - $links = $this->getExistingLinks(); - [ $prefix, $title ] = $linkId; - return isset( $links[$prefix][$title] ); - } - - protected function isInNewSet( $linkId ) { - [ $prefix, $title ] = $linkId; - return isset( $this->newLinks[$prefix][$title] ); - } - - protected function insertLink( $linkId ) { - [ $prefix, $title ] = $linkId; - $this->insertRow( [ - 'iwl_prefix' => $prefix, - 'iwl_title' => $title - ] ); - } - - protected function deleteLink( $linkId ) { - [ $prefix, $title ] = $linkId; - $this->deleteRow( [ - 'iwl_prefix' => $prefix, - 'iwl_title' => $title - ] ); - } -} diff --git a/includes/deferred/LinksUpdate/LangLinksTable.php b/includes/deferred/LinksUpdate/LangLinksTable.php deleted file mode 100644 index 09722338d58f..000000000000 --- a/includes/deferred/LinksUpdate/LangLinksTable.php +++ /dev/null @@ -1,99 +0,0 @@ -<?php - -namespace MediaWiki\Deferred\LinksUpdate; - -use ParserOutput; - -/** - * langlinks - * - * Link ID format: string[] - * - 0: Language code - * - 1: Foreign title - * - * @since 1.38 - */ -class LangLinksTable extends LinksTable { - private $newLinks = []; - private $existingLinks; - - public function setParserOutput( ParserOutput $parserOutput ) { - // Convert the format of the interlanguage links - // I didn't want to change it in the ParserOutput, because that array is passed all - // the way back to the skin, so either a skin API break would be required, or an - // inefficient back-conversion. - $ill = $parserOutput->getLanguageLinks(); - $this->newLinks = []; - foreach ( $ill as $link ) { - [ $key, $title ] = explode( ':', $link, 2 ); - $this->newLinks[$key] = $title; - } - } - - protected function getTableName() { - return 'langlinks'; - } - - protected function getFromField() { - return 'll_from'; - } - - protected function getExistingFields() { - return [ 'll_lang', 'll_title' ]; - } - - protected function getNewLinkIDs() { - foreach ( $this->newLinks as $key => $title ) { - yield [ $key, $title ]; - } - } - - /** - * Get the existing links as an array where the key is the language code - * and the value is the title of the target in that language. - * - * @return array - */ - private function getExistingLinks() { - if ( $this->existingLinks === null ) { - $this->existingLinks = []; - foreach ( $this->fetchExistingRows() as $row ) { - $this->existingLinks[$row->ll_lang] = $row->ll_title; - } - } - return $this->existingLinks; - } - - protected function getExistingLinkIDs() { - foreach ( $this->getExistingLinks() as $lang => $title ) { - yield [ $lang, $title ]; - } - } - - protected function isExisting( $linkId ) { - $links = $this->getExistingLinks(); - [ $lang, $title ] = $linkId; - return \array_key_exists( $lang, $links ) - && $links[$lang] === $title; - } - - protected function isInNewSet( $linkId ) { - [ $lang, $title ] = $linkId; - return \array_key_exists( $lang, $this->newLinks ) - && $this->newLinks[$lang] === $title; - } - - protected function insertLink( $linkId ) { - [ $lang, $title ] = $linkId; - $this->insertRow( [ - 'll_lang' => $lang, - 'll_title' => $title - ] ); - } - - protected function deleteLink( $linkId ) { - $this->deleteRow( [ - 'll_lang' => $linkId[0] - ] ); - } -} diff --git a/includes/deferred/LinksUpdate/LinksTable.php b/includes/deferred/LinksUpdate/LinksTable.php deleted file mode 100644 index 21a43b81098e..000000000000 --- a/includes/deferred/LinksUpdate/LinksTable.php +++ /dev/null @@ -1,536 +0,0 @@ -<?php - -namespace MediaWiki\Deferred\LinksUpdate; - -use MediaWiki\Page\PageIdentity; -use MediaWiki\Page\PageReference; -use MediaWiki\Revision\RevisionRecord; -use ParserOutput; -use Wikimedia\Rdbms\IDatabase; -use Wikimedia\Rdbms\IResultWrapper; -use Wikimedia\Rdbms\LBFactory; - -/** - * The base class for classes which update a single link table. - * - * A LinksTable object is a container for new and existing link sets outbound - * from a single page, and an abstraction of the associated DB schema. The - * object stores state related to an update of the outbound links of a page. - * - * Explanation of link ID concept - * ------------------------------ - * - * Link IDs identify a link in the new or old state, or in the change arrays. - * They are opaque to the base class and are type-hinted here as mixed. - * - * Conventionally, the link ID is string|string[] and contains the link target - * fields. - * - * The link ID should contain enough information so that the base class can - * tell whether an existing link is in the new set, or vice versa, for the - * purposes of incremental updates. If a change to a field would cause a DB - * update, the field should be in the link ID. - * - * For example, a change to cl_timestamp does not trigger an update, so - * cl_timestamp is not in the link ID. - * - * @stable to extend - * @since 1.38 - */ -abstract class LinksTable { - /** Link type: Inserted (added) links */ - public const INSERTED = 1; - - /** Link type: Deleted (removed) links */ - public const DELETED = 2; - - /** Link type: Changed (inserted or removed) links */ - public const CHANGED = 3; - - /** Link type: existing/old links */ - public const OLD = 4; - - /** Link type: new links (from the ParserOutput) */ - public const NEW = 5; - - /** - * Rows to delete. An array of associative arrays, each associative array - * being the conditions for a delete query. Common conditions should be - * leftmost in the associative array so that they can be factored out. - * - * @var array - */ - protected $rowsToDelete = []; - - /** - * Rows to insert. An array of associative arrays, each associative array - * mapping field names to values. - * - * @var array - */ - protected $rowsToInsert = []; - - /** @var array Link IDs for inserted links */ - protected $insertedLinks = []; - - /** @var array Link IDs for deleted links */ - protected $deletedLinks = []; - - /** @var LBFactory */ - private $lbFactory; - - /** @var IDatabase */ - private $db; - - /** @var PageIdentity */ - private $sourcePage; - - /** @var PageReference|null */ - private $movedPage; - - /** @var int */ - private $batchSize; - - /** @var mixed */ - private $ticket; - - /** @var RevisionRecord */ - private $revision; - - /** @var callable|null Callback for deprecated hook */ - private $afterUpdateHook; - - /** @var bool */ - protected $strictTestMode; - - /** - * This is called by the factory to inject dependencies for the base class. - * This is used instead of the constructor so that changes can be made to - * the injected parameters without breaking the subclass constructors. - * - * @param LBFactory $lbFactory - * @param PageIdentity $sourcePage - * @param int $batchSize - * @param callable|null $afterUpdateHook - */ - final public function injectBaseDependencies( - LBFactory $lbFactory, - PageIdentity $sourcePage, - $batchSize, - $afterUpdateHook - ) { - $this->lbFactory = $lbFactory; - $this->db = $this->lbFactory->getMainLB()->getConnection( DB_PRIMARY ); - $this->sourcePage = $sourcePage; - $this->batchSize = $batchSize; - $this->afterUpdateHook = $afterUpdateHook; - } - - /** - * Set the empty transaction ticket - * - * @param mixed $ticket - */ - public function setTransactionTicket( $ticket ) { - $this->ticket = $ticket; - } - - /** - * Set the revision associated with the edit. - * - * @param RevisionRecord $revision - */ - public function setRevision( RevisionRecord $revision ) { - $this->revision = $revision; - } - - /** - * Notify the object that the operation is a page move, and set the - * original title. - * - * @param PageReference $movedPage - */ - public function setMoveDetails( PageReference $movedPage ) { - $this->movedPage = $movedPage; - } - - /** - * Subclasses should implement this to extract the data they need from the - * ParserOutput. - * - * To support a future refactor of LinksDeletionUpdate, if this method is - * not called, the subclass should assume that the new state is empty. - * - * @param ParserOutput $parserOutput - */ - abstract public function setParserOutput( ParserOutput $parserOutput ); - - /** - * Get the table name. - * - * @return string - */ - abstract protected function getTableName(); - - /** - * Get the name of the field which links to page_id. - * - * @return string - */ - abstract protected function getFromField(); - - /** - * Get the fields to be used in fetchExistingRows(). Note that - * fetchExistingRows() is just a helper for subclasses. The value returned - * here is effectively private to the subclass. - * - * @return array - */ - abstract protected function getExistingFields(); - - /** - * Get an array (or iterator) of link IDs for the new state. - * - * See the LinksTable doc comment for an explanation of link IDs. - * - * @return iterable<mixed> - */ - abstract protected function getNewLinkIDs(); - - /** - * Get an array (or iterator) of link IDs for the existing state. The - * subclass should load the data from the database. There is - * fetchExistingRows() to make this easier but the subclass is responsible - * for caching. - * - * See the LinksTable doc comment for an explanation of link IDs. - * - * @return iterable<mixed> - */ - abstract protected function getExistingLinkIDs(); - - /** - * Determine whether a link (from the new set) is in the existing set. - * - * @param mixed $linkId - * @return bool - */ - abstract protected function isExisting( $linkId ); - - /** - * Determine whether a link (from the existing set) is in the new set. - * - * @param mixed $linkId - * @return bool - */ - abstract protected function isInNewSet( $linkId ); - - /** - * Insert a link identified by ID. The subclass is expected to queue the - * insertion by calling insertRow(). - * - * @param mixed $linkId - */ - abstract protected function insertLink( $linkId ); - - /** - * Delete a link identified by ID. The subclass is expected to queue the - * deletion by calling deleteRow(). - * - * @param mixed $linkId - */ - abstract protected function deleteLink( $linkId ); - - /** - * Subclasses can override this to return true in order to force - * reinsertion of all the links due to some property of the link - * changing for reasons not represented by the link ID. - * - * @return bool - */ - protected function needForcedLinkRefresh() { - return false; - } - - /** - * @stable to override - * @return IDatabase - */ - protected function getDB(): IDatabase { - return $this->db; - } - - /** - * @return LBFactory - */ - protected function getLBFactory(): LBFactory { - return $this->lbFactory; - } - - /** - * Get the page_id of the source page - * - * @return int - */ - protected function getSourcePageId(): int { - return $this->sourcePage->getId(); - } - - /** - * Get the source page, i.e. the page which is being updated and is the - * source of links. - * - * @return PageIdentity - */ - protected function getSourcePage(): PageIdentity { - return $this->sourcePage; - } - - /** - * Determine whether the page was moved - * - * @return bool - */ - protected function isMove() { - return $this->movedPage !== null; - } - - /** - * Determine whether the page was moved to a different namespace. - * - * @return bool - */ - protected function isCrossNamespaceMove() { - return $this->movedPage !== null - && $this->sourcePage->getNamespace() !== $this->movedPage->getNamespace(); - } - - /** - * Assuming the page was moved, get the original page title before the move. - * This will throw an exception if the page wasn't moved. - * - * @return PageReference - */ - protected function getMovedPage(): PageReference { - return $this->movedPage; - } - - /** - * Get the maximum number of rows to update in a batch. - * - * @return int - */ - protected function getBatchSize(): int { - return $this->batchSize; - } - - /** - * Get the empty transaction ticket, or null if there is none. - * - * @return mixed - */ - protected function getTransactionTicket() { - return $this->ticket; - } - - /** - * Get the RevisionRecord of the new revision, if the LinksUpdate caller - * injected one. - * - * @return RevisionRecord|null - */ - protected function getRevision(): ?RevisionRecord { - return $this->revision; - } - - /** - * Get field=>value associative array for the from field(s) - * - * @stable to override - * @return array - */ - protected function getFromConds() { - return [ $this->getFromField() => $this->getSourcePageId() ]; - } - - /** - * Do a select query to fetch the existing rows. This is a helper for - * subclasses. - * - * @return IResultWrapper - */ - protected function fetchExistingRows(): IResultWrapper { - return $this->getDB()->newSelectQueryBuilder() - ->select( $this->getExistingFields() ) - ->from( $this->getTableName() ) - ->where( $this->getFromConds() ) - ->caller( __METHOD__ ) - ->fetchResultSet(); - } - - /** - * Execute an edit/delete update - */ - final public function update() { - $this->startUpdate(); - $force = $this->needForcedLinkRefresh(); - foreach ( $this->getNewLinkIDs() as $link ) { - if ( $force || !$this->isExisting( $link ) ) { - $this->insertLink( $link ); - $this->insertedLinks[] = $link; - } - } - - foreach ( $this->getExistingLinkIDs() as $link ) { - if ( $force || !$this->isInNewSet( $link ) ) { - $this->deleteLink( $link ); - $this->deletedLinks[] = $link; - } - } - $this->doWrites(); - $this->finishUpdate(); - } - - /** - * Queue a row for insertion. Subclasses are expected to call this from - * insertLink(). The "from" field should not be included in the row. - * - * @param array $row Associative array mapping fields to values. - */ - protected function insertRow( $row ) { - $row += $this->getFromConds(); - $this->rowsToInsert[] = $row; - } - - /** - * Queue a deletion operation. Subclasses are expected to call this from - * deleteLink(). The "from" field does not need to be included in the - * conditions. - * - * Most often, the conditions match a single row, but this is not required. - * - * @param array $conds Associative array mapping fields to values, - * specifying the conditions for a delete query. - */ - protected function deleteRow( $conds ) { - // Put the "from" field leftmost, so it can be factored out - $conds = $this->getFromConds() + $conds; - $this->rowsToDelete[] = $conds; - } - - /** - * Subclasses can override this to do any necessary setup before the lock - * is acquired. - * - * @stable to override - */ - public function beforeLock() { - } - - /** - * Subclasses can override this to do any necessary setup before individual - * write operations begin. - * - * @stable to override - */ - protected function startUpdate() { - } - - /** - * Subclasses can override this to do any updates associated with their - * link data, for example dispatching HTML update jobs. - * - * @stable to override - */ - protected function finishUpdate() { - } - - /** - * Do the common DB operations - */ - protected function doWrites() { - $db = $this->getDB(); - $table = $this->getTableName(); - $domainId = $db->getDomainID(); - $batchSize = $this->getBatchSize(); - $ticket = $this->getTransactionTicket(); - - foreach ( array_chunk( $this->rowsToDelete, $batchSize ) as $chunk ) { - $factoredConds = $db->factorConds( $chunk ); - $db->delete( - $table, - $factoredConds, - __METHOD__ - ); - $this->lbFactory->commitAndWaitForReplication( - __METHOD__, $ticket, [ 'domain' => $domainId ] - ); - } - - $insertBatches = array_chunk( $this->rowsToInsert, $batchSize ); - foreach ( $insertBatches as $insertBatch ) { - $db->insert( $table, $insertBatch, __METHOD__, $this->getInsertOptions() ); - $this->lbFactory->commitAndWaitForReplication( - __METHOD__, $ticket, [ 'domain' => $domainId ] - ); - } - - if ( count( $this->rowsToInsert ) && $this->afterUpdateHook ) { - ( $this->afterUpdateHook )( $table, $this->rowsToInsert ); - } - } - - /** - * Omit conflict resolution options from the insert query so that testing - * can confirm that the incremental update logic was correct. - * - * @param bool $mode - */ - public function setStrictTestMode( $mode = true ) { - $this->strictTestMode = $mode; - } - - /** - * Get the options for the insert queries - * - * @return array - */ - protected function getInsertOptions() { - if ( $this->strictTestMode ) { - return []; - } else { - return [ 'IGNORE' ]; - } - } - - /** - * Get an array or iterator of link IDs of a given type. Some subclasses - * use this to provide typed data to callers. This is not public because - * link IDs are a private concept. - * - * @param int $setType One of the class constants: self::INSERTED, self::DELETED, - * self::CHANGED, self::OLD or self::NEW. - * @return iterable<mixed> - */ - protected function getLinkIDs( $setType ) { - switch ( $setType ) { - case self::INSERTED: - return $this->insertedLinks; - - case self::DELETED: - return $this->deletedLinks; - - case self::CHANGED: - return array_merge( $this->insertedLinks, $this->deletedLinks ); - - case self::OLD: - return $this->getExistingLinkIDs(); - - case self::NEW: - return $this->getNewLinkIDs(); - - default: - throw new \InvalidArgumentException( __METHOD__ . ": Unknown link type" ); - } - } -} diff --git a/includes/deferred/LinksUpdate/LinksTableGroup.php b/includes/deferred/LinksUpdate/LinksTableGroup.php deleted file mode 100644 index 4fb7c005d30e..000000000000 --- a/includes/deferred/LinksUpdate/LinksTableGroup.php +++ /dev/null @@ -1,293 +0,0 @@ -<?php - -namespace MediaWiki\Deferred\LinksUpdate; - -use MediaWiki\Collation\CollationFactory; -use MediaWiki\Config\ServiceOptions; -use MediaWiki\MediaWikiServices; -use MediaWiki\Page\PageIdentity; -use MediaWiki\Page\PageReference; -use MediaWiki\Revision\RevisionRecord; -use ParserOutput; -use Wikimedia\ObjectFactory\ObjectFactory; -use Wikimedia\Rdbms\LBFactory; - -/** - * @since 1.38 - */ -class LinksTableGroup { - /** - * ObjectFactory specifications for the subclasses. The following - * additional keys are defined: - * - * - serviceOptions: An array of configuration variable names. If this is - * set, the specified configuration will be sent to the subclass - * constructor as a ServiceOptions object. - * - needCollation: If true, the following additional args will be added: - * Collation, collation name and table name. - */ - private const CORE_LIST = [ - 'categorylinks' => [ - 'class' => CategoryLinksTable::class, - 'services' => [ - 'LanguageConverterFactory', - 'NamespaceInfo', - 'WikiPageFactory' - ], - 'needCollation' => true, - ], - 'externallinks' => [ - 'class' => ExternalLinksTable::class - ], - 'imagelinks' => [ - 'class' => ImageLinksTable::class - ], - 'iwlinks' => [ - 'class' => InterwikiLinksTable::class - ], - 'langlinks' => [ - 'class' => LangLinksTable::class - ], - 'pagelinks' => [ - 'class' => PageLinksTable::class - ], - 'page_props' => [ - 'class' => PagePropsTable::class, - 'services' => [ - 'JobQueueGroup' - ], - 'serviceOptions' => PagePropsTable::CONSTRUCTOR_OPTIONS - ], - 'templatelinks' => [ - 'class' => TemplateLinksTable::class - ] - ]; - - /** @var ObjectFactory */ - private $objectFactory; - - /** @var LBFactory */ - private $lbFactory; - - /** @var CollationFactory */ - private $collationFactory; - - /** @var PageIdentity */ - private $page; - - /** @var PageReference|null */ - private $movedPage; - - /** @var ParserOutput|null */ - private $parserOutput; - - /** @var int */ - private $batchSize; - - /** @var callable|null */ - private $afterUpdateHook; - - /** @var mixed */ - private $ticket; - - /** @var RevisionRecord|null */ - private $revision; - - /** @var LinksTable[] */ - private $tables = []; - - /** @var array */ - private $tempCollations; - - /** - * @param ObjectFactory $objectFactory - * @param LBFactory $lbFactory - * @param CollationFactory $collationFactory - * @param PageIdentity $page - * @param int $batchSize - * @param callable|null $afterUpdateHook - * @param array $tempCollations - */ - public function __construct( - ObjectFactory $objectFactory, - LBFactory $lbFactory, - CollationFactory $collationFactory, - PageIdentity $page, - $batchSize, - $afterUpdateHook, - array $tempCollations - ) { - $this->objectFactory = $objectFactory; - $this->lbFactory = $lbFactory; - $this->collationFactory = $collationFactory; - $this->page = $page; - $this->batchSize = $batchSize; - $this->afterUpdateHook = $afterUpdateHook; - $this->tempCollations = []; - foreach ( $tempCollations as $info ) { - $this->tempCollations[$info['table']] = $info; - } - } - - /** - * Set the ParserOutput object to be used in new and existing objects. - * - * @param ParserOutput $parserOutput - */ - public function setParserOutput( ParserOutput $parserOutput ) { - $this->parserOutput = $parserOutput; - foreach ( $this->tables as $table ) { - $table->setParserOutput( $parserOutput ); - } - } - - /** - * Set the original title in the case of a page move. - * - * @param PageReference $oldPage - */ - public function setMoveDetails( PageReference $oldPage ) { - $this->movedPage = $oldPage; - foreach ( $this->tables as $table ) { - $table->setMoveDetails( $oldPage ); - } - } - - /** - * Set the transaction ticket to be used in new and existing objects. - * - * @param mixed $ticket - */ - public function setTransactionTicket( $ticket ) { - $this->ticket = $ticket; - foreach ( $this->tables as $table ) { - $table->setTransactionTicket( $ticket ); - } - } - - /** - * Set the revision to be used in new and existing objects. - * - * @param RevisionRecord $revision - */ - public function setRevision( RevisionRecord $revision ) { - $this->revision = $revision; - foreach ( $this->tables as $table ) { - $table->setRevision( $revision ); - } - } - - /** - * Set the strict test mode - * - * @param bool $mode - */ - public function setStrictTestMode( $mode = true ) { - foreach ( $this->getAll() as $table ) { - $table->setStrictTestMode( $mode ); - } - } - - /** - * Get the spec array for a given table. - * - * @param string $tableName - * @return array - */ - private function getSpec( $tableName ) { - if ( isset( self::CORE_LIST[$tableName] ) ) { - $spec = self::CORE_LIST[$tableName]; - return $this->addCollationArgs( $spec, $tableName, false ); - } - if ( isset( $this->tempCollations[$tableName] ) ) { - $info = $this->tempCollations[$tableName]; - $spec = self::CORE_LIST['categorylinks']; - return $this->addCollationArgs( $spec, $tableName, true, $info ); - } - throw new \InvalidArgumentException( - __CLASS__ . ": unknown table name \"$tableName\"" ); - } - - /** - * Add extra args to the spec of a table that needs collation information - * - * @param array $spec - * @param string $tableName - * @param bool $isTempTable - * @param array $info Temporary collation info - * @return array ObjectFactory spec - */ - private function addCollationArgs( $spec, $tableName, $isTempTable, $info = [] ) { - if ( isset( $spec['needCollation'] ) ) { - if ( isset( $info['collation'] ) ) { - $collation = $this->collationFactory->makeCollation( $info['collation'] ); - $collationName = $info['fakeCollation'] ?? $info['collation']; - } else { - $collation = $this->collationFactory->getCategoryCollation(); - $collationName = $this->collationFactory->getDefaultCollationName(); - } - $spec['args'] = [ - $collation, - $info['fakeCollation'] ?? $collationName, - $tableName, - $isTempTable - ]; - unset( $spec['needCollation'] ); - } - return $spec; - } - - /** - * Get a LinksTable for a given table. - * - * @param string $tableName - * @return LinksTable - */ - public function get( $tableName ) { - if ( !isset( $this->tables[$tableName] ) ) { - $spec = $this->getSpec( $tableName ); - if ( isset( $spec['serviceOptions'] ) ) { - $config = MediaWikiServices::getInstance()->getMainConfig(); - $extraArgs = [ new ServiceOptions( $spec['serviceOptions'], $config ) ]; - unset( $spec['serviceOptions'] ); - } else { - $extraArgs = []; - } - /** @var LinksTable $table */ - $table = $this->objectFactory->createObject( $spec, [ 'extraArgs' => $extraArgs ] ); - $table->injectBaseDependencies( - $this->lbFactory, - $this->page, - $this->batchSize, - $this->afterUpdateHook - ); - if ( $this->parserOutput ) { - $table->setParserOutput( $this->parserOutput ); - } - if ( $this->movedPage ) { - $table->setMoveDetails( $this->movedPage ); - } - if ( $this->ticket ) { - $table->setTransactionTicket( $this->ticket ); - } - if ( $this->revision ) { - $table->setRevision( $this->revision ); - } - $this->tables[$tableName] = $table; - } - return $this->tables[$tableName]; - } - - /** - * Get LinksTable objects for all known links tables. - * @return iterable<LinksTable> - */ - public function getAll() { - foreach ( self::CORE_LIST as $tableName => $spec ) { - yield $this->get( $tableName ); - } - foreach ( $this->tempCollations as $tableName => $collation ) { - yield $this->get( $tableName ); - } - } -} diff --git a/includes/deferred/LinksUpdate/LinksUpdate.php b/includes/deferred/LinksUpdate/LinksUpdate.php deleted file mode 100644 index 4662e8fb40c9..000000000000 --- a/includes/deferred/LinksUpdate/LinksUpdate.php +++ /dev/null @@ -1,608 +0,0 @@ -<?php -/** - * Updater for link tracking tables after a page edit. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - */ - -namespace MediaWiki\Deferred\LinksUpdate; - -use AutoCommitUpdate; -use BacklinkCache; -use DataUpdate; -use DeferredUpdates; -use Job; -use JobQueueGroup; -use MediaWiki\HookContainer\ProtectedHookAccessorTrait; -use MediaWiki\Logger\LoggerFactory; -use MediaWiki\MediaWikiServices; -use MediaWiki\Page\PageIdentity; -use MediaWiki\Page\PageReference; -use MediaWiki\Page\PageReferenceValue; -use MediaWiki\Revision\RevisionRecord; -use MediaWiki\User\UserIdentity; -use MWException; -use ParserOutput; -use RefreshLinksJob; -use RuntimeException; -use Title; -use Wikimedia\Rdbms\IDatabase; -use Wikimedia\ScopedCallback; - -/** - * Class the manages updates of *_link tables as well as similar extension-managed tables - * - * @note: LinksUpdate is managed by DeferredUpdates::execute(). Do not run this in a transaction. - * - * See docs/deferred.txt - */ -class LinksUpdate extends DataUpdate { - use ProtectedHookAccessorTrait; - - // @todo make members protected, but make sure extensions don't break - - /** @var int Page ID of the article linked from */ - public $mId; - - /** @var Title Title object of the article linked from */ - public $mTitle; - - /** @var ParserOutput */ - public $mParserOutput; - - /** - * @var int[][] Map of title strings to IDs for the links in the document - * @phan-var array<int,array<string,int>> - */ - public $mLinks; - - /** @var array DB keys of the images used, in the array key only */ - public $mImages; - - /** @var array Map of title strings to IDs for the template references, including broken ones */ - public $mTemplates; - - /** @var array URLs of external links, array key only */ - public $mExternals; - - /** @var array Map of category names to sort keys */ - public $mCategories; - - /** @var array Map of language codes to titles */ - public $mInterlangs; - - /** @var array 2-D map of (prefix => DBK => 1) */ - public $mInterwikis; - - /** @var array Map of arbitrary name to value */ - public $mProperties; - - /** @var bool Whether to queue jobs for recursive updates */ - public $mRecursive; - - /** @var RevisionRecord Revision for which this update has been triggered */ - private $mRevisionRecord; - - /** - * @var UserIdentity|null - */ - private $user; - - /** @var IDatabase */ - private $db; - - /** @var LinksTableGroup */ - private $tableFactory; - - /** - * @param PageIdentity $page The page we're updating - * @param ParserOutput $parserOutput Output from a full parse of this page - * @param bool $recursive Queue jobs for recursive updates? - * - * @throws MWException - */ - public function __construct( PageIdentity $page, ParserOutput $parserOutput, $recursive = true ) { - parent::__construct(); - - // NOTE: mTitle is public and used in hooks. Will need careful deprecation. - $this->mTitle = Title::castFromPageIdentity( $page ); - $this->mParserOutput = $parserOutput; - - $this->mLinks = $parserOutput->getLinks(); - $this->mImages = $parserOutput->getImages(); - $this->mTemplates = $parserOutput->getTemplates(); - $this->mExternals = $parserOutput->getExternalLinks(); - $this->mCategories = $parserOutput->getCategories(); - $this->mProperties = $parserOutput->getPageProperties(); - $this->mInterwikis = $parserOutput->getInterwikiLinks(); - - # Convert the format of the interlanguage links - # I didn't want to change it in the ParserOutput, because that array is passed all - # the way back to the skin, so either a skin API break would be required, or an - # inefficient back-conversion. - $ill = $parserOutput->getLanguageLinks(); - $this->mInterlangs = []; - foreach ( $ill as $link ) { - list( $key, $title ) = explode( ':', $link, 2 ); - $this->mInterlangs[$key] = $title; - } - - foreach ( $this->mCategories as &$sortkey ) { - # If the sortkey is longer then 255 bytes, it is truncated by DB, and then doesn't match - # when comparing existing vs current categories, causing T27254. - $sortkey = mb_strcut( $sortkey, 0, 255 ); - } - - $this->mRecursive = $recursive; - - $services = MediaWikiServices::getInstance(); - $config = $services->getMainConfig(); - $this->tableFactory = new LinksTableGroup( - $services->getObjectFactory(), - $services->getDBLoadBalancerFactory(), - $services->getCollationFactory(), - $page, - $config->get( 'UpdateRowsPerQuery' ), - function ( $table, $rows ) { - $this->getHookRunner()->onLinksUpdateAfterInsert( $this, $table, $rows ); - }, - $config->get( 'TempCategoryCollations' ) - ); - // TODO: this does not have to be called in LinksDeletionUpdate - $this->tableFactory->setParserOutput( $parserOutput ); - - $this->getHookRunner()->onLinksUpdateConstructed( $this ); - } - - public function setTransactionTicket( $ticket ) { - parent::setTransactionTicket( $ticket ); - $this->tableFactory->setTransactionTicket( $ticket ); - } - - /** - * Notify LinksUpdate that a move has just been completed and set the - * original title - * - * @param PageReference $oldPage - */ - public function setMoveDetails( PageReference $oldPage ) { - $this->tableFactory->setMoveDetails( $oldPage ); - } - - /** - * Update link tables with outgoing links from an updated article - * - * @note this is managed by DeferredUpdates::execute(). Do not run this in a transaction. - */ - public function doUpdate() { - if ( !$this->mId ) { - // NOTE: subclasses may initialize mId directly! - $this->mId = $this->mTitle->getArticleID( Title::READ_LATEST ); - } - - if ( !$this->mId ) { - // Probably due to concurrent deletion or renaming of the page - $logger = LoggerFactory::getInstance( 'SecondaryDataUpdate' ); - $logger->notice( - 'LinksUpdate: The Title object yields no ID. Perhaps the page was deleted?', - [ - 'page_title' => $this->mTitle->getPrefixedDBkey(), - 'cause_action' => $this->getCauseAction(), - 'cause_agent' => $this->getCauseAgent() - ] - ); - - // nothing to do - return; - } - - // Do any setup that needs to be done prior to acquiring the lock - // Calling getAll() here has the side-effect of calling - // LinksUpdateBatch::setParserOutput() on all subclasses, allowing - // those methods to also do pre-lock operations. - foreach ( $this->tableFactory->getAll() as $table ) { - $table->beforeLock(); - } - - if ( $this->ticket ) { - // Make sure all links update threads see the changes of each other. - // This handles the case when updates have to batched into several COMMITs. - $scopedLock = self::acquirePageLock( $this->getDB(), $this->mId ); - if ( !$scopedLock ) { - throw new RuntimeException( "Could not acquire lock for page ID '{$this->mId}'." ); - } - } - - $this->getHookRunner()->onLinksUpdate( $this ); - $this->doIncrementalUpdate(); - - // Commit and release the lock (if set) - ScopedCallback::consume( $scopedLock ); - // Run post-commit hook handlers without DBO_TRX - DeferredUpdates::addUpdate( new AutoCommitUpdate( - $this->getDB(), - __METHOD__, - function () { - $this->getHookRunner()->onLinksUpdateComplete( $this, $this->ticket ); - } - ) ); - } - - /** - * Acquire a session-level lock for performing link table updates for a page on a DB - * - * @param IDatabase $dbw - * @param int $pageId - * @param string $why One of (job, atomicity) - * @return ScopedCallback|null - * @since 1.27 - */ - public static function acquirePageLock( IDatabase $dbw, $pageId, $why = 'atomicity' ) { - $key = "{$dbw->getDomainID()}:LinksUpdate:$why:pageid:$pageId"; // per-wiki - $scopedLock = $dbw->getScopedLockAndFlush( $key, __METHOD__, 15 ); - if ( !$scopedLock ) { - $logger = LoggerFactory::getInstance( 'SecondaryDataUpdate' ); - $logger->info( "Could not acquire lock '{key}' for page ID '{page_id}'.", [ - 'key' => $key, - 'page_id' => $pageId, - ] ); - return null; - } - - return $scopedLock; - } - - protected function doIncrementalUpdate() { - foreach ( $this->tableFactory->getAll() as $table ) { - $table->update(); - } - - # Refresh links of all pages including this page - # This will be in a separate transaction - if ( $this->mRecursive ) { - $this->queueRecursiveJobs(); - } - - # Update the links table freshness for this title - $this->updateLinksTimestamp(); - } - - /** - * Queue recursive jobs for this page - * - * Which means do LinksUpdate on all pages that include the current page, - * using the job queue. - */ - protected function queueRecursiveJobs() { - $backlinkCache = MediaWikiServices::getInstance()->getBacklinkCacheFactory() - ->getBacklinkCache( $this->mTitle ); - $action = $this->getCauseAction(); - $agent = $this->getCauseAgent(); - - self::queueRecursiveJobsForTable( - $this->mTitle, 'templatelinks', $action, $agent, $backlinkCache - ); - if ( $this->mTitle->getNamespace() === NS_FILE ) { - // Process imagelinks in case the title is or was a redirect - self::queueRecursiveJobsForTable( - $this->mTitle, 'imagelinks', $action, $agent, $backlinkCache - ); - } - - // Get jobs for cascade-protected backlinks for a high priority queue. - // If meta-templates change to using a new template, the new template - // should be implicitly protected as soon as possible, if applicable. - // These jobs duplicate a subset of the above ones, but can run sooner. - // Which ever runs first generally no-ops the other one. - $jobs = []; - foreach ( $backlinkCache->getCascadeProtectedLinkPages() as $page ) { - $jobs[] = RefreshLinksJob::newPrioritized( - $page, - [ - 'causeAction' => $action, - 'causeAgent' => $agent - ] - ); - } - JobQueueGroup::singleton()->push( $jobs ); - } - - /** - * Queue a RefreshLinks job for any table. - * - * @param PageIdentity $page Page to do job for - * @param string $table Table to use (e.g. 'templatelinks') - * @param string $action Triggering action - * @param string $userName Triggering user name - * @param BacklinkCache|null $backlinkCache - */ - public static function queueRecursiveJobsForTable( - PageIdentity $page, $table, $action = 'unknown', $userName = 'unknown', ?BacklinkCache $backlinkCache = null - ) { - $title = Title::castFromPageIdentity( $page ); - if ( !$backlinkCache ) { - wfDeprecatedMsg( __METHOD__ . " needs a BacklinkCache object, null passed", '1.37' ); - $backlinkCache = MediaWikiServices::getInstance()->getBacklinkCacheFactory() - ->getBacklinkCache( $title ); - } - if ( $backlinkCache->hasLinks( $table ) ) { - $job = new RefreshLinksJob( - $title, - [ - 'table' => $table, - 'recursive' => true, - ] + Job::newRootJobParams( // "overall" refresh links job info - "refreshlinks:{$table}:{$title->getPrefixedText()}" - ) + [ 'causeAction' => $action, 'causeAgent' => $userName ] - ); - - JobQueueGroup::singleton()->push( $job ); - } - } - - /** - * Omit conflict resolution options from the insert query so that testing - * can confirm that the incremental update logic was correct. - * - * @param bool $mode - */ - public function setStrictTestMode( $mode = true ) { - $this->tableFactory->setStrictTestMode( $mode ); - } - - /** - * Return the title object of the page being updated - * @return Title - */ - public function getTitle() { - return $this->mTitle; - } - - /** - * Get the page_id of the page being updated - * - * @since 1.38 - * @return int - */ - public function getPageId() { - if ( $this->mId ) { - return $this->mId; - } else { - return $this->mTitle->getArticleID(); - } - } - - /** - * Returns parser output - * @since 1.19 - * @return ParserOutput - */ - public function getParserOutput() { - return $this->mParserOutput; - } - - /** - * Return the list of images used as generated by the parser - * @return array - */ - public function getImages() { - return $this->mImages; - } - - /** - * Set the RevisionRecord corresponding to this LinksUpdate - * - * @since 1.35 - * @param RevisionRecord $revisionRecord - */ - public function setRevisionRecord( RevisionRecord $revisionRecord ) { - $this->mRevisionRecord = $revisionRecord; - $this->tableFactory->setRevision( $revisionRecord ); - } - - /** - * @since 1.35 - * @return RevisionRecord|null - */ - public function getRevisionRecord() { - return $this->mRevisionRecord; - } - - /** - * Set the user who triggered this LinksUpdate - * - * @since 1.27 - * @param UserIdentity $user - */ - public function setTriggeringUser( UserIdentity $user ) { - $this->user = $user; - } - - /** - * Get the user who triggered this LinksUpdate - * - * @since 1.27 - * @return UserIdentity|null - */ - public function getTriggeringUser(): ?UserIdentity { - return $this->user; - } - - /** - * @return PageLinksTable - */ - protected function getPageLinksTable(): PageLinksTable { - // @phan-suppress-next-line PhanTypeMismatchReturnSuperType - return $this->tableFactory->get( 'pagelinks' ); - } - - /** - * @return ExternalLinksTable - */ - protected function getExternalLinksTable(): ExternalLinksTable { - // @phan-suppress-next-line PhanTypeMismatchReturnSuperType - return $this->tableFactory->get( 'externallinks' ); - } - - /** - * @return PagePropsTable - */ - protected function getPagePropsTable(): PagePropsTable { - // @phan-suppress-next-line PhanTypeMismatchReturnSuperType - return $this->tableFactory->get( 'page_props' ); - } - - /** - * Fetch page links added by this LinksUpdate. Only available after the update is complete. - * - * @since 1.22 - * @deprecated since 1.38 use getPageReferenceIterator() or getPageReferenceArray() - * @return Title[] Array of Titles - */ - public function getAddedLinks() { - return $this->getPageLinksTable()->getTitleArray( LinksTable::INSERTED ); - } - - /** - * Fetch page links removed by this LinksUpdate. Only available after the update is complete. - * - * @since 1.22 - * @deprecated since 1.38 use getPageReferenceIterator() or getPageReferenceArray() - * @return Title[] Array of Titles - */ - public function getRemovedLinks() { - return $this->getPageLinksTable()->getTitleArray( LinksTable::DELETED ); - } - - /** - * Fetch external links added by this LinksUpdate. Only available after - * the update is complete. - * @since 1.33 - * @return null|array Array of Strings - */ - public function getAddedExternalLinks() { - return $this->getExternalLinksTable()->getStringArray( LinksTable::INSERTED ); - } - - /** - * Fetch external links removed by this LinksUpdate. Only available after - * the update is complete. - * @since 1.33 - * @return null|string[] - */ - public function getRemovedExternalLinks() { - return $this->getExternalLinksTable()->getStringArray( LinksTable::DELETED ); - } - - /** - * Fetch page properties added by this LinksUpdate. - * Only available after the update is complete. - * @since 1.28 - * @return null|array - */ - public function getAddedProperties() { - return $this->getPagePropsTable()->getAssocArray( LinksTable::INSERTED ); - } - - /** - * Fetch page properties removed by this LinksUpdate. - * Only available after the update is complete. - * @since 1.28 - * @return null|array - */ - public function getRemovedProperties() { - return $this->getPagePropsTable()->getAssocArray( LinksTable::DELETED ); - } - - /** - * Get an iterator over PageReferenceValue objects corresponding to a given set - * type in a given table. - * - * @since 1.38 - * @param string $tableName The name of any table that links to local titles - * @param int $setType One of: - * - LinksTable::INSERTED: The inserted links - * - LinksTable::DELETED: The deleted links - * - LinksTable::CHANGED: Both the inserted and deleted links - * - LinksTable::OLD: The old set of links, loaded before the update - * - LinksTable::NEW: The new set of links from the ParserOutput - * @return iterable<PageReferenceValue> - * @phan-return \Traversable - */ - public function getPageReferenceIterator( $tableName, $setType ) { - $table = $this->tableFactory->get( $tableName ); - if ( $table instanceof TitleLinksTable ) { - return $table->getPageReferenceIterator( $setType ); - } else { - throw new \InvalidArgumentException( - __METHOD__ . ": $tableName does not have a list of titles" ); - } - } - - /** - * Same as getPageReferenceIterator() but converted to an array for convenience - * (at the expense of additional time and memory usage) - * - * @since 1.38 - * @param string $tableName - * @param int $setType - * @return PageReferenceValue[] - */ - public function getPageReferenceArray( $tableName, $setType ) { - return iterator_to_array( $this->getPageReferenceIterator( $tableName, $setType ) ); - } - - /** - * Update links table freshness - */ - private function updateLinksTimestamp() { - if ( $this->mId ) { - // The link updates made here only reflect the freshness of the parser output - $timestamp = $this->mParserOutput->getCacheTime(); - $this->getDB()->update( 'page', - [ 'page_links_updated' => $this->getDB()->timestamp( $timestamp ) ], - [ 'page_id' => $this->mId ], - __METHOD__ - ); - } - } - - /** - * @return IDatabase - */ - protected function getDB() { - if ( !$this->db ) { - $this->db = wfGetDB( DB_PRIMARY ); - } - - return $this->db; - } - - /** - * Whether or not this LinksUpdate will also update pages which transclude the - * current page or otherwise depend on it. - * - * @return bool - */ - public function isRecursive() { - return $this->mRecursive; - } -} - -/** @deprecated since 1.38 */ -class_alias( LinksUpdate::class, 'LinksUpdate' ); diff --git a/includes/deferred/LinksUpdate/PageLinksTable.php b/includes/deferred/LinksUpdate/PageLinksTable.php deleted file mode 100644 index 15a21a55f0a1..000000000000 --- a/includes/deferred/LinksUpdate/PageLinksTable.php +++ /dev/null @@ -1,34 +0,0 @@ -<?php - -namespace MediaWiki\Deferred\LinksUpdate; - -use ParserOutput; - -/** - * pagelinks - */ -class PageLinksTable extends GenericPageLinksTable { - public function setParserOutput( ParserOutput $parserOutput ) { - $this->newLinks = $parserOutput->getLinks(); - } - - protected function getTableName() { - return 'pagelinks'; - } - - protected function getFromField() { - return 'pl_from'; - } - - protected function getNamespaceField() { - return 'pl_namespace'; - } - - protected function getTitleField() { - return 'pl_title'; - } - - protected function getFromNamespaceField() { - return 'pl_from_namespace'; - } -} diff --git a/includes/deferred/LinksUpdate/PagePropsTable.php b/includes/deferred/LinksUpdate/PagePropsTable.php deleted file mode 100644 index ca7097d16c2b..000000000000 --- a/includes/deferred/LinksUpdate/PagePropsTable.php +++ /dev/null @@ -1,190 +0,0 @@ -<?php - -namespace MediaWiki\Deferred\LinksUpdate; - -use HTMLCacheUpdateJob; -use JobQueueGroup; -use MediaWiki\Config\ServiceOptions; -use ParserOutput; - -/** - * page_props - * - * Link ID format: string[] - * 0: Property name (pp_propname) - * 1: Property value (pp_value) - * - * @since 1.38 - */ -class PagePropsTable extends LinksTable { - /** @var JobQueueGroup */ - private $jobQueueGroup; - - /** @var array */ - private $newProps = []; - - /** @var array|null */ - private $existingProps; - - /** - * The configured PagePropLinkInvalidations. An associative array where the - * key is the property name and the value is a string or array of strings - * giving the link table names which will be used for backlink cache - * invalidation. - * - * @var array - */ - private $linkInvalidations; - - public const CONSTRUCTOR_OPTIONS = [ 'PagePropLinkInvalidations' ]; - - public function __construct( - ServiceOptions $options, - JobQueueGroup $jobQueueGroup - ) { - $this->jobQueueGroup = $jobQueueGroup; - $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); - $this->linkInvalidations = $options->get( 'PagePropLinkInvalidations' ); - } - - public function setParserOutput( ParserOutput $parserOutput ) { - $this->newProps = $parserOutput->getPageProperties(); - } - - protected function getTableName() { - return 'page_props'; - } - - protected function getFromField() { - return 'pp_page'; - } - - protected function getExistingFields() { - return [ 'pp_propname', 'pp_value' ]; - } - - protected function getNewLinkIDs() { - foreach ( $this->newProps as $name => $value ) { - yield [ $name, $value ]; - } - } - - /** - * Get the existing page_props as an associative array - * - * @return array - */ - private function getExistingProps() { - if ( $this->existingProps === null ) { - $this->existingProps = []; - foreach ( $this->fetchExistingRows() as $row ) { - $this->existingProps[$row->pp_propname] = $row->pp_value; - } - } - return $this->existingProps; - } - - protected function getExistingLinkIDs() { - foreach ( $this->getExistingProps() as $name => $value ) { - yield [ $name, $value ]; - } - } - - protected function isExisting( $linkId ) { - $existing = $this->getExistingProps(); - [ $name, $value ] = $linkId; - return \array_key_exists( $name, $existing ) - && $existing[$name] === $value; - } - - protected function isInNewSet( $linkId ) { - [ $name, $value ] = $linkId; - return \array_key_exists( $name, $this->newProps ) - && $this->newProps[$name] === $value; - } - - protected function insertLink( $linkId ) { - [ $name, $value ] = $linkId; - $this->insertRow( [ - 'pp_propname' => $name, - 'pp_value' => $value, - 'pp_sortkey' => $this->getPropertySortKeyValue( $value ) - ] ); - } - - /** - * Determines the sort key for the given property value. - * This will return $value if it is a float or int, - * 1 or resp. 0 if it is a bool, and null otherwise. - * - * @note In the future, we may allow the sortkey to be specified explicitly - * in ParserOutput::setProperty. - * - * @param mixed $value - * - * @return float|null - */ - private function getPropertySortKeyValue( $value ) { - if ( is_int( $value ) || is_float( $value ) || is_bool( $value ) ) { - return floatval( $value ); - } - - return null; - } - - protected function deleteLink( $linkId ) { - $this->deleteRow( [ - 'pp_propname' => $linkId[0] - ] ); - } - - protected function finishUpdate() { - $changed = array_unique( array_merge( - array_column( $this->insertedLinks, 0 ), - array_column( $this->deletedLinks, 0 ) ) ); - $this->invalidateProperties( $changed ); - } - - /** - * Invalidate the properties given the list of changed property names - * - * @param string[] $changed - */ - private function invalidateProperties( array $changed ) { - $jobs = []; - foreach ( $changed as $name ) { - if ( isset( $this->linkInvalidations[$name] ) ) { - $inv = $this->linkInvalidations[$name]; - if ( !is_array( $inv ) ) { - $inv = [ $inv ]; - } - foreach ( $inv as $table ) { - $jobs[] = HTMLCacheUpdateJob::newForBacklinks( - $this->getSourcePage(), - $table, - [ 'causeAction' => 'page-props' ] - ); - } - } - } - - if ( $jobs ) { - $this->jobQueueGroup->lazyPush( $jobs ); - } - } - - /** - * Get the properties for a given link set as an associative array - * - * @param int $setType The set type as in LinksTable::getLinkIDs() - * @return array - */ - public function getAssocArray( $setType ) { - $props = []; - foreach ( $this->getLinkIDs( $setType ) as $linkId ) { - [ $name, $value ] = $linkId; - $props[$name] = $value; - } - return $props; - } -} diff --git a/includes/deferred/LinksUpdate/TemplateLinksTable.php b/includes/deferred/LinksUpdate/TemplateLinksTable.php deleted file mode 100644 index 0cc655fc517c..000000000000 --- a/includes/deferred/LinksUpdate/TemplateLinksTable.php +++ /dev/null @@ -1,36 +0,0 @@ -<?php - -namespace MediaWiki\Deferred\LinksUpdate; - -use ParserOutput; - -/** - * templatelinks - * - * @since 1.38 - */ -class TemplateLinksTable extends GenericPageLinksTable { - public function setParserOutput( ParserOutput $parserOutput ) { - $this->newLinks = $parserOutput->getTemplates(); - } - - protected function getTableName() { - return 'templatelinks'; - } - - protected function getFromField() { - return 'tl_from'; - } - - protected function getNamespaceField() { - return 'tl_namespace'; - } - - protected function getTitleField() { - return 'tl_title'; - } - - protected function getFromNamespaceField() { - return 'tl_from_namespace'; - } -} diff --git a/includes/deferred/LinksUpdate/TitleLinksTable.php b/includes/deferred/LinksUpdate/TitleLinksTable.php deleted file mode 100644 index 8a28638d8ced..000000000000 --- a/includes/deferred/LinksUpdate/TitleLinksTable.php +++ /dev/null @@ -1,88 +0,0 @@ -<?php - -namespace MediaWiki\Deferred\LinksUpdate; - -use MediaWiki\Page\PageReferenceValue; -use Title; - -/** - * An abstract base class for tables that link to local titles. - * - * @stable to extend - * @since 1.38 - */ -abstract class TitleLinksTable extends LinksTable { - /** - * Convert a link ID to a PageReferenceValue - * - * @param mixed $linkId - * @return PageReferenceValue - */ - abstract protected function makePageReferenceValue( $linkId ): PageReferenceValue; - - /** - * Convert a link ID to a Title - * - * @stable to override - * @param mixed $linkId - * @return Title - */ - protected function makeTitle( $linkId ): Title { - return Title::castFromPageReference( $this->makePageReferenceValue( $linkId ) ); - } - - /** - * Given an iterator over link IDs, remove links which go to the same - * title, leaving only one link per title. - * - * @param iterable<mixed> $linkIds - * @return iterable<mixed> - */ - abstract protected function deduplicateLinkIds( $linkIds ); - - /** - * Get link IDs for a given set type, filtering out duplicate links to the - * same title. - * - * @param int $setType - * @return iterable<mixed> - */ - protected function getDeduplicatedLinkIds( $setType ) { - $linkIds = $this->getLinkIDs( $setType ); - // Only the CHANGED set type should have duplicates - if ( $setType === self::CHANGED ) { - $linkIds = $this->deduplicateLinkIds( $linkIds ); - } - return $linkIds; - } - - /** - * Get a link set as an array of Title objects. This is memory-inefficient. - * - * @deprecated since 1.38 - * @param int $setType - * @return Title[] - */ - public function getTitleArray( $setType ) { - $linkIds = $this->getDeduplicatedLinkIds( $setType ); - $titles = []; - foreach ( $linkIds as $linkId ) { - $titles[] = $this->makeTitle( $linkId ); - } - return $titles; - } - - /** - * Get a link set as an iterator over PageReferenceValue objects. - * - * @param int $setType - * @return iterable<PageReferenceValue> - * @phan-return \Traversable - */ - public function getPageReferenceIterator( $setType ) { - $linkIds = $this->getDeduplicatedLinkIds( $setType ); - foreach ( $linkIds as $linkId ) { - yield $this->makePageReferenceValue( $linkId ); - } - } -} diff --git a/includes/deferred/RefreshSecondaryDataUpdate.php b/includes/deferred/RefreshSecondaryDataUpdate.php index 3858b3532c7c..97ca1d143a55 100644 --- a/includes/deferred/RefreshSecondaryDataUpdate.php +++ b/includes/deferred/RefreshSecondaryDataUpdate.php @@ -20,7 +20,6 @@ * @file */ -use MediaWiki\Deferred\LinksUpdate\LinksUpdate; use MediaWiki\Revision\RevisionRecord; use MediaWiki\Storage\DerivedPageDataUpdater; use MediaWiki\User\UserIdentity; diff --git a/includes/filerepo/file/LocalFile.php b/includes/filerepo/file/LocalFile.php index 388c26df3e2a..1f3c446f9518 100644 --- a/includes/filerepo/file/LocalFile.php +++ b/includes/filerepo/file/LocalFile.php @@ -21,7 +21,6 @@ * @ingroup FileAbstraction */ -use MediaWiki\Deferred\LinksUpdate\LinksUpdate; use MediaWiki\Logger\LoggerFactory; use MediaWiki\MediaWikiServices; use MediaWiki\Permissions\Authority; diff --git a/includes/jobqueue/jobs/DeleteLinksJob.php b/includes/jobqueue/jobs/DeleteLinksJob.php index 074a1386097f..d587d23e4204 100644 --- a/includes/jobqueue/jobs/DeleteLinksJob.php +++ b/includes/jobqueue/jobs/DeleteLinksJob.php @@ -21,8 +21,6 @@ * @ingroup JobQueue */ -use MediaWiki\Deferred\LinksUpdate\LinksDeletionUpdate; -use MediaWiki\Deferred\LinksUpdate\LinksUpdate; use MediaWiki\MediaWikiServices; /** diff --git a/includes/jobqueue/jobs/RefreshLinksJob.php b/includes/jobqueue/jobs/RefreshLinksJob.php index 459bf966c5cd..72f98e80c9cc 100644 --- a/includes/jobqueue/jobs/RefreshLinksJob.php +++ b/includes/jobqueue/jobs/RefreshLinksJob.php @@ -21,7 +21,6 @@ * @ingroup JobQueue */ use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface; -use MediaWiki\Deferred\LinksUpdate\LinksUpdate; use MediaWiki\Logger\LoggerFactory; use MediaWiki\MediaWikiServices; use MediaWiki\Page\PageIdentity; diff --git a/includes/page/DeletePage.php b/includes/page/DeletePage.php index aede631c6360..be56338ed1a2 100644 --- a/includes/page/DeletePage.php +++ b/includes/page/DeletePage.php @@ -12,12 +12,12 @@ use DeferredUpdates; use DeletePageJob; use Exception; use JobQueueGroup; +use LinksDeletionUpdate; +use LinksUpdate; use LogicException; use ManualLogEntry; use MediaWiki\Cache\BacklinkCacheFactory; use MediaWiki\Config\ServiceOptions; -use MediaWiki\Deferred\LinksUpdate\LinksDeletionUpdate; -use MediaWiki\Deferred\LinksUpdate\LinksUpdate; use MediaWiki\HookContainer\HookContainer; use MediaWiki\HookContainer\HookRunner; use MediaWiki\Logger\LoggerFactory; diff --git a/maintenance/namespaceDupes.php b/maintenance/namespaceDupes.php index ac7392d881cd..64624156eaba 100644 --- a/maintenance/namespaceDupes.php +++ b/maintenance/namespaceDupes.php @@ -26,7 +26,6 @@ require_once __DIR__ . '/Maintenance.php'; -use MediaWiki\Deferred\LinksUpdate\LinksDeletionUpdate; use MediaWiki\Linker\LinkTarget; use MediaWiki\MediaWikiServices; use Wikimedia\Rdbms\IDatabase; diff --git a/tests/phpunit/includes/Storage/DerivedPageDataUpdaterTest.php b/tests/phpunit/includes/Storage/DerivedPageDataUpdaterTest.php index b57333c3db68..cd39948d453a 100644 --- a/tests/phpunit/includes/Storage/DerivedPageDataUpdaterTest.php +++ b/tests/phpunit/includes/Storage/DerivedPageDataUpdaterTest.php @@ -9,8 +9,8 @@ use ContentHandler; use DeferredUpdates; use DummyContentHandlerForTesting; use JobQueueGroup; +use LinksUpdate; use MediaWiki\Config\ServiceOptions; -use MediaWiki\Deferred\LinksUpdate\LinksUpdate; use MediaWiki\MediaWikiServices; use MediaWiki\Revision\MutableRevisionRecord; use MediaWiki\Revision\MutableRevisionSlots; diff --git a/tests/phpunit/includes/content/WikitextContentTest.php b/tests/phpunit/includes/content/WikitextContentTest.php index de6dc0e43a44..7c39b38864bc 100644 --- a/tests/phpunit/includes/content/WikitextContentTest.php +++ b/tests/phpunit/includes/content/WikitextContentTest.php @@ -1,6 +1,5 @@ <?php -use MediaWiki\Deferred\LinksUpdate\LinksDeletionUpdate; use MediaWiki\MediaWikiServices; /** diff --git a/tests/phpunit/includes/deferred/LinksDeletionUpdateTest.php b/tests/phpunit/includes/deferred/LinksDeletionUpdateTest.php index 53b954b9c6e6..59c6cdeff91f 100644 --- a/tests/phpunit/includes/deferred/LinksDeletionUpdateTest.php +++ b/tests/phpunit/includes/deferred/LinksDeletionUpdateTest.php @@ -1,23 +1,8 @@ <?php -use MediaWiki\Deferred\LinksUpdate\LinksDeletionUpdate; -use MediaWiki\Deferred\LinksUpdate\LinksUpdate; - /** * @covers LinksDeletionUpdate * @covers LinksUpdate - * @covers \MediaWiki\Deferred\LinksUpdate\CategoryLinksTable - * @covers \MediaWiki\Deferred\LinksUpdate\ExternalLinksTable - * @covers \MediaWiki\Deferred\LinksUpdate\GenericPageLinksTable - * @covers \MediaWiki\Deferred\LinksUpdate\ImageLinksTable - * @covers \MediaWiki\Deferred\LinksUpdate\InterwikiLinksTable - * @covers \MediaWiki\Deferred\LinksUpdate\LangLinksTable - * @covers \MediaWiki\Deferred\LinksUpdate\LinksTable - * @covers \MediaWiki\Deferred\LinksUpdate\LinksTableGroup - * @covers \MediaWiki\Deferred\LinksUpdate\PageLinksTable - * @covers \MediaWiki\Deferred\LinksUpdate\PagePropsTable - * @covers \MediaWiki\Deferred\LinksUpdate\TemplateLinksTable - * @covers \MediaWiki\Deferred\LinksUpdate\TitleLinksTable * * @group LinksUpdate * @group Database diff --git a/tests/phpunit/includes/deferred/LinksUpdateTest.php b/tests/phpunit/includes/deferred/LinksUpdateTest.php index 3007011324f0..9d090824b4cc 100644 --- a/tests/phpunit/includes/deferred/LinksUpdateTest.php +++ b/tests/phpunit/includes/deferred/LinksUpdateTest.php @@ -1,24 +1,10 @@ <?php -use MediaWiki\Deferred\LinksUpdate\LinksUpdate; -use MediaWiki\Page\PageIdentityValue; use PHPUnit\Framework\MockObject\MockObject; use Wikimedia\TestingAccessWrapper; /** * @covers LinksUpdate - * @covers \MediaWiki\Deferred\LinksUpdate\CategoryLinksTable - * @covers \MediaWiki\Deferred\LinksUpdate\ExternalLinksTable - * @covers \MediaWiki\Deferred\LinksUpdate\GenericPageLinksTable - * @covers \MediaWiki\Deferred\LinksUpdate\ImageLinksTable - * @covers \MediaWiki\Deferred\LinksUpdate\InterwikiLinksTable - * @covers \MediaWiki\Deferred\LinksUpdate\LangLinksTable - * @covers \MediaWiki\Deferred\LinksUpdate\LinksTable - * @covers \MediaWiki\Deferred\LinksUpdate\LinksTableGroup - * @covers \MediaWiki\Deferred\LinksUpdate\PageLinksTable - * @covers \MediaWiki\Deferred\LinksUpdate\PagePropsTable - * @covers \MediaWiki\Deferred\LinksUpdate\TemplateLinksTable - * @covers \MediaWiki\Deferred\LinksUpdate\TitleLinksTable * * @group LinksUpdate * @group Database @@ -106,7 +92,8 @@ class LinksUpdateTest extends MediaWikiLangTestCase { $t, $po, 'pagelinks', - [ 'pl_namespace', 'pl_title' ], + 'pl_namespace, + pl_title', 'pl_from = ' . self::$testingPageId, [ [ NS_MAIN, 'Bar' ], @@ -129,7 +116,8 @@ class LinksUpdateTest extends MediaWikiLangTestCase { $t, $po, 'pagelinks', - [ 'pl_namespace', 'pl_title' ], + 'pl_namespace, + pl_title', 'pl_from = ' . self::$testingPageId, [ [ NS_MAIN, 'Bar' ], @@ -146,36 +134,6 @@ class LinksUpdateTest extends MediaWikiLangTestCase { ], $update->getRemovedLinks() ); } - public function testUpdate_pagelinks_move() { - list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", self::$testingPageId ); - - $po->addLink( Title::newFromText( "Foo" ) ); - $this->assertLinksUpdate( - $t, - $po, - 'pagelinks', - [ 'pl_namespace', 'pl_title', 'pl_from_namespace' ], - 'pl_from = ' . self::$testingPageId, - [ - [ NS_MAIN, 'Foo', NS_MAIN ], - ] - ); - - list( $t, $po ) = $this->makeTitleAndParserOutput( "User:Testing", self::$testingPageId ); - $po->addLink( Title::newFromText( "Foo" ) ); - $this->assertMoveLinksUpdate( - $t, - new PageIdentityValue( 2, 0, "Foo", false ), - $po, - 'pagelinks', - [ 'pl_namespace', 'pl_title', 'pl_from_namespace' ], - 'pl_from = ' . self::$testingPageId, - [ - [ NS_MAIN, 'Foo', NS_USER ], - ] - ); - } - /** * @covers ParserOutput::addExternalLink * @covers LinksUpdate::getAddedExternalLinks @@ -192,7 +150,7 @@ class LinksUpdateTest extends MediaWikiLangTestCase { $t, $po, 'externallinks', - [ 'el_to', 'el_index' ], + 'el_to, el_index', 'el_from = ' . self::$testingPageId, [ [ 'http://testing.com/wiki/Bar', 'http://com.testing./wiki/Bar' ], @@ -213,7 +171,7 @@ class LinksUpdateTest extends MediaWikiLangTestCase { $t, $po, 'externallinks', - [ 'el_to', 'el_index' ], + 'el_to, el_index', 'el_from = ' . self::$testingPageId, [ [ 'http://testing.com/wiki/Bar', 'http://com.testing./wiki/Bar' ], @@ -245,7 +203,7 @@ class LinksUpdateTest extends MediaWikiLangTestCase { $t, $po, 'categorylinks', - [ 'cl_to', 'cl_sortkey' ], + 'cl_to, cl_sortkey', 'cl_from = ' . self::$testingPageId, [ [ 'Bar', "BAR\nTESTING" ], @@ -272,7 +230,7 @@ class LinksUpdateTest extends MediaWikiLangTestCase { $t, $po, 'categorylinks', - [ 'cl_to', 'cl_sortkey' ], + 'cl_to, cl_sortkey', 'cl_from = ' . self::$testingPageId, [ [ 'Bar', "BAR\nTESTING" ], @@ -386,56 +344,6 @@ class LinksUpdateTest extends MediaWikiLangTestCase { ); } - public function testUpdate_categorylinks_move() { - $this->setMwGlobals( 'wgCategoryCollation', 'uppercase' ); - - /** @var ParserOutput $po */ - list( $t, $po ) = $this->makeTitleAndParserOutput( "Old", self::$testingPageId ); - - $po->addCategory( "Foo", "FOO" ); - - $this->assertLinksUpdate( - $t, - $po, - 'categorylinks', - [ 'cl_to', 'cl_sortkey' ], - 'cl_from = ' . self::$testingPageId, - [ - [ 'Foo', "FOO\nOLD" ] - ] - ); - - /** @var ParserOutput $po */ - list( $t, $po ) = $this->makeTitleAndParserOutput( "New", self::$testingPageId ); - - $po->addCategory( "Foo", "FOO" ); - - // An update to cl_sortkey is not expected if there was no move - $this->assertLinksUpdate( - $t, - $po, - 'categorylinks', - [ 'cl_to', 'cl_sortkey' ], - 'cl_from = ' . self::$testingPageId, - [ - [ 'Foo', "FOO\nOLD" ] - ] - ); - - // With move notification, update to cl_sortkey is expected - $this->assertMoveLinksUpdate( - $t, - new PageIdentityValue( 2, 0, "new", false ), - $po, - 'categorylinks', - [ 'cl_to', 'cl_sortkey' ], - 'cl_from = ' . self::$testingPageId, - [ - [ 'Foo', "FOO\nNEW" ] - ] - ); - } - /** * @covers ParserOutput::addInterwikiLink */ @@ -453,7 +361,7 @@ class LinksUpdateTest extends MediaWikiLangTestCase { $t, $po, 'iwlinks', - [ 'iwl_prefix', 'iwl_title' ], + 'iwl_prefix, iwl_title', 'iwl_from = ' . self::$testingPageId, [ [ 'linksupdatetest', 'T1' ], @@ -471,7 +379,7 @@ class LinksUpdateTest extends MediaWikiLangTestCase { $t, $po, 'iwlinks', - [ 'iwl_prefix', 'iwl_title' ], + 'iwl_prefix, iwl_title', 'iwl_from = ' . self::$testingPageId, [ [ 'linksupdatetest', 'T2' ], @@ -498,7 +406,8 @@ class LinksUpdateTest extends MediaWikiLangTestCase { $t, $po, 'templatelinks', - [ 'tl_namespace', 'tl_title' ], + 'tl_namespace, + tl_title', 'tl_from = ' . self::$testingPageId, [ [ NS_TEMPLATE, 'T1' ], @@ -516,7 +425,8 @@ class LinksUpdateTest extends MediaWikiLangTestCase { $t, $po, 'templatelinks', - [ 'tl_namespace', 'tl_title' ], + 'tl_namespace, + tl_title', 'tl_from = ' . self::$testingPageId, [ [ NS_TEMPLATE, 'T2' ], @@ -579,7 +489,7 @@ class LinksUpdateTest extends MediaWikiLangTestCase { $t, $po, 'langlinks', - [ 'll_lang', 'll_title' ], + 'll_lang, ll_title', 'll_from = ' . self::$testingPageId, [ [ 'De', '1' ], @@ -596,7 +506,7 @@ class LinksUpdateTest extends MediaWikiLangTestCase { $t, $po, 'langlinks', - [ 'll_lang', 'll_title' ], + 'll_lang, ll_title', 'll_from = ' . self::$testingPageId, [ [ 'En', '2' ], @@ -676,19 +586,8 @@ class LinksUpdateTest extends MediaWikiLangTestCase { protected function assertLinksUpdate( Title $title, ParserOutput $parserOutput, $table, $fields, $condition, array $expectedRows ) { - return $this->assertMoveLinksUpdate( $title, null, $parserOutput, - $table, $fields, $condition, $expectedRows ); - } - - protected function assertMoveLinksUpdate( - Title $title, ?PageIdentityValue $oldTitle, ParserOutput $parserOutput, - $table, $fields, $condition, array $expectedRows - ) { $update = new LinksUpdate( $title, $parserOutput ); $update->setStrictTestMode(); - if ( $oldTitle ) { - $update->setMoveDetails( $oldTitle ); - } $update->doUpdate(); @@ -701,7 +600,7 @@ class LinksUpdateTest extends MediaWikiLangTestCase { ) { $this->assertSelect( [ 'recentchanges', 'comment' ], - [ 'rc_title', 'comment_text' ], + 'rc_title, comment_text', [ 'rc_type' => RC_CATEGORIZE, 'rc_namespace' => NS_CATEGORY, diff --git a/tests/phpunit/includes/page/WikiPageDbTest.php b/tests/phpunit/includes/page/WikiPageDbTest.php index 27ca6558eb06..1ca1f8a22db4 100644 --- a/tests/phpunit/includes/page/WikiPageDbTest.php +++ b/tests/phpunit/includes/page/WikiPageDbTest.php @@ -1,7 +1,6 @@ <?php use MediaWiki\Content\Renderer\ContentRenderer; -use MediaWiki\Deferred\LinksUpdate\LinksDeletionUpdate; use MediaWiki\Edit\PreparedEdit; use MediaWiki\MediaWikiServices; use MediaWiki\Page\PageIdentity; |