summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKunal Mehta <legoktm@debian.org>2022-01-14 15:35:37 -0800
committerKunal Mehta <legoktm@debian.org>2022-01-14 15:38:18 -0800
commit6971d100e1ad851fde84664b1cd895250917855e (patch)
tree18b77499abe801759edac13a5db5af5ef3ae0509
parent758e39cb3d8d54a326e19355869ad1c016f7acad (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
-rw-r--r--autoload.php4
-rw-r--r--includes/AutoLoader.php1
-rw-r--r--includes/DefaultSettings.php11
-rw-r--r--includes/MovePage.php49
-rw-r--r--includes/Storage/DerivedPageDataUpdater.php7
-rw-r--r--includes/collation/CollationFactory.php6
-rw-r--r--includes/deferred/Hook/LinksUpdateAfterInsertHook.php2
-rw-r--r--includes/deferred/Hook/LinksUpdateCompleteHook.php2
-rw-r--r--includes/deferred/Hook/LinksUpdateConstructedHook.php2
-rw-r--r--includes/deferred/Hook/LinksUpdateHook.php2
-rw-r--r--includes/deferred/LinksDeletionUpdate.php (renamed from includes/deferred/LinksUpdate/LinksDeletionUpdate.php)14
-rw-r--r--includes/deferred/LinksUpdate.php1298
-rw-r--r--includes/deferred/LinksUpdate/CategoryLinksTable.php317
-rw-r--r--includes/deferred/LinksUpdate/ExternalLinksTable.php99
-rw-r--r--includes/deferred/LinksUpdate/GenericPageLinksTable.php140
-rw-r--r--includes/deferred/LinksUpdate/ImageLinksTable.php142
-rw-r--r--includes/deferred/LinksUpdate/InterwikiLinksTable.php97
-rw-r--r--includes/deferred/LinksUpdate/LangLinksTable.php99
-rw-r--r--includes/deferred/LinksUpdate/LinksTable.php536
-rw-r--r--includes/deferred/LinksUpdate/LinksTableGroup.php293
-rw-r--r--includes/deferred/LinksUpdate/LinksUpdate.php608
-rw-r--r--includes/deferred/LinksUpdate/PageLinksTable.php34
-rw-r--r--includes/deferred/LinksUpdate/PagePropsTable.php190
-rw-r--r--includes/deferred/LinksUpdate/TemplateLinksTable.php36
-rw-r--r--includes/deferred/LinksUpdate/TitleLinksTable.php88
-rw-r--r--includes/deferred/RefreshSecondaryDataUpdate.php1
-rw-r--r--includes/filerepo/file/LocalFile.php1
-rw-r--r--includes/jobqueue/jobs/DeleteLinksJob.php2
-rw-r--r--includes/jobqueue/jobs/RefreshLinksJob.php1
-rw-r--r--includes/page/DeletePage.php4
-rw-r--r--maintenance/namespaceDupes.php1
-rw-r--r--tests/phpunit/includes/Storage/DerivedPageDataUpdaterTest.php2
-rw-r--r--tests/phpunit/includes/content/WikitextContentTest.php1
-rw-r--r--tests/phpunit/includes/deferred/LinksDeletionUpdateTest.php15
-rw-r--r--tests/phpunit/includes/deferred/LinksUpdateTest.php135
-rw-r--r--tests/phpunit/includes/page/WikiPageDbTest.php1
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;