23 use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
28 use Wikimedia\ScopedCallback;
38 use ProtectedHookAccessorTrait;
130 parent::__construct();
136 $this->mId =
$title->getArticleID( Title::READ_LATEST );
140 throw new InvalidArgumentException(
141 "The Title object yields no ID. "
142 .
"Perhaps the page [[{$title->getPrefixedDBkey()}]] doesn't exist?"
146 $this->mParserOutput = $parserOutput;
148 $this->mLinks = $parserOutput->
getLinks();
149 $this->mImages = $parserOutput->
getImages();
156 # Convert the format of the interlanguage links
157 # I didn't want to change it in the ParserOutput, because that array is passed all
158 # the way back to the skin, so either a skin API break would be required, or an
159 # inefficient back-conversion.
161 $this->mInterlangs = [];
162 foreach ( $ill as $link ) {
163 list( $key,
$title ) = explode(
':', $link, 2 );
164 $this->mInterlangs[$key] =
$title;
167 foreach ( $this->mCategories as &$sortkey ) {
168 # If the sortkey is longer then 255 bytes, it is truncated by DB, and then doesn't match
169 # when comparing existing vs current categories, causing T27254.
170 $sortkey = mb_strcut( $sortkey, 0, 255 );
173 $this->mRecursive = $recursive;
175 $this->getHookRunner()->onLinksUpdateConstructed( $this );
184 if ( $this->ticket ) {
188 if ( !$scopedLock ) {
189 throw new RuntimeException(
"Could not acquire lock for page ID '{$this->mId}'." );
193 $this->getHookRunner()->onLinksUpdate( $this );
197 ScopedCallback::consume( $scopedLock );
203 $this->getHookRunner()->onLinksUpdateComplete( $this, $this->ticket );
218 $key =
"{$dbw->getDomainID()}:LinksUpdate:$why:pageid:$pageId";
220 if ( !$scopedLock ) {
221 $logger = LoggerFactory::getInstance(
'SecondaryDataUpdate' );
222 $logger->info(
"Could not acquire lock '{key}' for page ID '{page_id}'.", [
224 'page_id' => $pageId,
237 $this->
incrTableUpdate(
'pagelinks',
'pl', $this->linkDeletions, $this->linkInsertions );
248 # Invalidate all image description pages which had links added or removed
249 $imageUpdates = $imageDeletes + array_diff_key( $this->mImages, $existingIL );
260 $this->externalLinkDeletions,
261 $this->externalLinkInsertions );
271 # Inline interwiki links
295 $categoryInserts = array_diff_assoc( $this->mCategories, $existingCL );
296 $categoryUpdates = $categoryInserts + $categoryDeletes;
304 $this->propertyDeletions,
307 # Invalidate the necessary pages
308 $this->propertyInsertions = array_diff_assoc( $this->mProperties, $existingPP );
312 # Invalidate all categories which were added, deleted or changed (set symmetric difference)
316 # Refresh links of all pages including this page
317 # This will be in a separate transaction
318 if ( $this->mRecursive ) {
322 # Update the links table freshness for this title
337 if ( $this->mTitle->getNamespace() ===
NS_FILE ) {
342 $bc = $this->mTitle->getBacklinkCache();
349 foreach ( $bc->getCascadeProtectedLinks() as
$title ) {
353 'causeAction' => $action,
354 'causeAgent' => $agent
370 Title $title, $table, $action =
'unknown', $userName =
'unknown'
372 if (
$title->getBacklinkCache()->hasLinks( $table ) ) {
379 "refreshlinks:{$table}:{$title->getPrefixedText()}"
380 ) + [
'causeAction' => $action,
'causeAgent' => $userName ]
404 if ( !$added && !$deleted ) {
408 $domainId = $this->
getDB()->getDomainID();
409 $services = MediaWikiServices::getInstance();
410 $wp = $services->getWikiPageFactory()->newFromTitle( $this->mTitle );
411 $lbf = $services->getDBLoadBalancerFactory();
413 $lbf->commitAndWaitForReplication( __METHOD__, $this->ticket, [
'domain' => $domainId ] );
416 $wp->updateCategoryCounts( array_map(
'strval', $addBatch ), [], $this->mId );
417 $lbf->commitAndWaitForReplication(
418 __METHOD__, $this->ticket, [
'domain' => $domainId ] );
422 $wp->updateCategoryCounts( [], array_map(
'strval', $deleteBatch ), $this->mId );
423 $lbf->commitAndWaitForReplication(
424 __METHOD__, $this->ticket, [
'domain' => $domainId ] );
433 $this->
getDB(),
NS_FILE, array_map(
'strval', array_keys( $images ) )
445 $services = MediaWikiServices::getInstance();
446 $bSize = $services->getMainConfig()->get(
'UpdateRowsPerQuery' );
447 $lbf = $services->getDBLoadBalancerFactory();
449 if ( $table ===
'page_props' ) {
450 $fromField =
'pp_page';
452 $fromField =
"{$prefix}_from";
456 if ( $table ===
'pagelinks' || $table ===
'templatelinks' || $table ===
'iwlinks' ) {
457 $baseKey = ( $table ===
'iwlinks' ) ?
'iwl_prefix' :
"{$prefix}_namespace";
460 $curDeletionBatch = [];
461 $deletionBatches = [];
462 foreach ( $deletions as $ns => $dbKeys ) {
463 foreach ( $dbKeys as $dbKey => $unused ) {
464 $curDeletionBatch[$ns][$dbKey] = 1;
465 if ( ++$curBatchSize >= $bSize ) {
466 $deletionBatches[] = $curDeletionBatch;
467 $curDeletionBatch = [];
472 if ( $curDeletionBatch ) {
473 $deletionBatches[] = $curDeletionBatch;
476 foreach ( $deletionBatches as $deletionBatch ) {
479 $this->
getDB()->makeWhereFrom2d( $deletionBatch, $baseKey,
"{$prefix}_title" )
483 if ( $table ===
'langlinks' ) {
484 $toField =
'll_lang';
485 } elseif ( $table ===
'page_props' ) {
486 $toField =
'pp_propname';
488 $toField = $prefix .
'_to';
491 $deletionBatches = array_chunk( array_keys( $deletions ), $bSize );
492 foreach ( $deletionBatches as $deletionBatch ) {
495 $toField => array_map(
'strval', $deletionBatch )
500 $domainId = $this->
getDB()->getDomainID();
502 foreach ( $deleteWheres as $deleteWhere ) {
503 $this->
getDB()->delete( $table, $deleteWhere, __METHOD__ );
504 $lbf->commitAndWaitForReplication(
505 __METHOD__, $this->ticket, [
'domain' => $domainId ]
509 $insertBatches = array_chunk( $insertions, $bSize );
510 foreach ( $insertBatches as $insertBatch ) {
511 $this->
getDB()->insert( $table, $insertBatch, __METHOD__, [
'IGNORE' ] );
512 $lbf->commitAndWaitForReplication(
513 __METHOD__, $this->ticket, [
'domain' => $domainId ]
517 if ( count( $insertions ) ) {
518 $this->getHookRunner()->onLinksUpdateAfterInsert( $this, $table, $insertions );
531 foreach ( $this->mLinks as $ns => $dbkeys ) {
532 $diffs = isset( $existing[$ns] )
533 ? array_diff_key( $dbkeys, $existing[$ns] )
535 foreach ( $diffs as $dbk => $id ) {
538 'pl_from_namespace' => $this->mTitle->getNamespace(),
539 'pl_namespace' => $ns,
555 foreach ( $this->mTemplates as $ns => $dbkeys ) {
556 $diffs = isset( $existing[$ns] ) ? array_diff_key( $dbkeys, $existing[$ns] ) : $dbkeys;
557 foreach ( $diffs as $dbk => $id ) {
560 'tl_from_namespace' => $this->mTitle->getNamespace(),
561 'tl_namespace' => $ns,
578 $diffs = array_diff_key( $this->mImages, $existing );
579 foreach ( $diffs as $iname => $dummy ) {
582 'il_from_namespace' => $this->mTitle->getNamespace(),
597 $diffs = array_diff_key( $this->mExternals, $existing );
598 foreach ( $diffs as $url => $dummy ) {
603 'el_index' => $index,
604 'el_index_60' => substr( $index, 0, 60 ),
622 $diffs = array_diff_assoc( $this->mCategories, $existing );
625 $languageConverter = MediaWikiServices::getInstance()->getLanguageConverterFactory()
626 ->getLanguageConverter();
629 foreach ( $diffs as $name => $prefix ) {
631 $languageConverter->findVariantLink( $name, $nt,
true );
633 $type = MediaWikiServices::getInstance()->getNamespaceInfo()->
634 getCategoryLinkType( $this->mTitle->getNamespace() );
636 # Treat custom sortkeys as a prefix, so that if multiple
637 # things are forced to sort as '*' or something, they'll
638 # sort properly in the category rather than in page_id
640 $sortkey = $collation->getSortKey( $this->mTitle->getCategorySortkey( $prefix ) );
645 'cl_sortkey' => $sortkey,
646 'cl_timestamp' => $this->
getDB()->timestamp(),
647 'cl_sortkey_prefix' => $prefix,
664 $diffs = array_diff_assoc( $this->mInterlangs, $existing );
683 $diffs = array_diff_assoc( $this->mProperties, $existing );
686 foreach ( array_keys( $diffs ) as $name ) {
709 $value = $this->mProperties[$prop];
713 'pp_propname' => $prop,
714 'pp_value' => $value,
732 if ( is_int( $value ) || is_float( $value ) || is_bool( $value ) ) {
733 return floatval( $value );
747 foreach ( $this->mInterwikis as $prefix => $dbkeys ) {
748 $diffs = isset( $existing[$prefix] )
749 ? array_diff_key( $dbkeys, $existing[$prefix] )
752 foreach ( $diffs as $dbk => $id ) {
755 'iwl_prefix' => $prefix,
772 foreach ( $existing as $ns => $dbkeys ) {
773 if ( isset( $this->mLinks[$ns] ) ) {
774 $del[$ns] = array_diff_key( $dbkeys, $this->mLinks[$ns] );
791 foreach ( $existing as $ns => $dbkeys ) {
792 if ( isset( $this->mTemplates[$ns] ) ) {
793 $del[$ns] = array_diff_key( $dbkeys, $this->mTemplates[$ns] );
809 return array_diff_key( $existing, $this->mImages );
819 return array_diff_key( $existing, $this->mExternals );
829 return array_diff_assoc( $existing, $this->mCategories );
839 return array_diff_assoc( $existing, $this->mInterlangs );
848 return array_diff_assoc( $existing, $this->mProperties );
859 foreach ( $existing as $prefix => $dbkeys ) {
860 if ( isset( $this->mInterwikis[$prefix] ) ) {
861 $del[$prefix] = array_diff_key( $dbkeys, $this->mInterwikis[$prefix] );
863 $del[$prefix] = $dbkeys;
876 $res = $this->
getDB()->select(
'pagelinks', [
'pl_namespace',
'pl_title' ],
877 [
'pl_from' => $this->mId ], __METHOD__ );
879 foreach (
$res as $row ) {
880 if ( !isset( $arr[$row->pl_namespace] ) ) {
881 $arr[$row->pl_namespace] = [];
883 $arr[$row->pl_namespace][$row->pl_title] = 1;
895 $res = $this->
getDB()->select(
'templatelinks', [
'tl_namespace',
'tl_title' ],
896 [
'tl_from' => $this->mId ], __METHOD__ );
898 foreach (
$res as $row ) {
899 if ( !isset( $arr[$row->tl_namespace] ) ) {
900 $arr[$row->tl_namespace] = [];
902 $arr[$row->tl_namespace][$row->tl_title] = 1;
914 $res = $this->
getDB()->select(
'imagelinks', [
'il_to' ],
915 [
'il_from' => $this->mId ], __METHOD__ );
917 foreach (
$res as $row ) {
918 $arr[$row->il_to] = 1;
930 $res = $this->
getDB()->select(
'externallinks', [
'el_to' ],
931 [
'el_from' => $this->mId ], __METHOD__ );
933 foreach (
$res as $row ) {
934 $arr[$row->el_to] = 1;
946 $res = $this->
getDB()->select(
'categorylinks', [
'cl_to',
'cl_sortkey_prefix' ],
947 [
'cl_from' => $this->mId ], __METHOD__ );
949 foreach (
$res as $row ) {
950 $arr[$row->cl_to] = $row->cl_sortkey_prefix;
963 $res = $this->
getDB()->select(
'langlinks', [
'll_lang',
'll_title' ],
964 [
'll_from' => $this->mId ], __METHOD__ );
966 foreach (
$res as $row ) {
967 $arr[$row->ll_lang] = $row->ll_title;
978 $res = $this->
getDB()->select(
'iwlinks', [
'iwl_prefix',
'iwl_title' ],
979 [
'iwl_from' => $this->mId ], __METHOD__ );
981 foreach (
$res as $row ) {
982 if ( !isset( $arr[$row->iwl_prefix] ) ) {
983 $arr[$row->iwl_prefix] = [];
985 $arr[$row->iwl_prefix][$row->iwl_title] = 1;
997 $res = $this->
getDB()->select(
'page_props', [
'pp_propname',
'pp_value' ],
998 [
'pp_page' => $this->mId ], __METHOD__ );
1000 foreach (
$res as $row ) {
1001 $arr[$row->pp_propname] = $row->pp_value;
1051 $this->mRevisionRecord = $revisionRecord;
1062 return $revRecord ?
new Revision( $revRecord ) :
null;
1080 $this->user =
$user;
1099 foreach ( $changed as $name => $value ) {
1102 if ( !is_array( $inv ) ) {
1105 foreach ( $inv as $table ) {
1109 [
'causeAction' =>
'page-props' ]
1124 if ( $this->linkInsertions ===
null ) {
1128 foreach ( $this->linkInsertions as $insertion ) {
1129 $result[] =
Title::makeTitle( $insertion[
'pl_namespace'], $insertion[
'pl_title'] );
1141 if ( $this->linkDeletions ===
null ) {
1145 foreach ( $this->linkDeletions as $ns => $titles ) {
1146 foreach ( $titles as
$title => $unused ) {
1161 if ( $this->externalLinkInsertions ===
null ) {
1164 return array_column( $this->externalLinkInsertions,
'el_to' );
1174 if ( $this->externalLinkDeletions ===
null ) {
1177 return array_keys( $this->externalLinkDeletions );
1206 $timestamp = $this->mParserOutput->getCacheTime();
1207 $this->
getDB()->update(
'page',
1208 [
'page_links_updated' => $this->
getDB()->timestamp( $timestamp ) ],
1209 [
'page_id' => $this->mId ],