26 use Wikimedia\ScopedCallback;
122 parent::__construct();
128 $this->mId =
$title->getArticleID( Title::READ_LATEST );
132 throw new InvalidArgumentException(
133 "The Title object yields no ID. Perhaps the page doesn't exist?"
137 $this->mParserOutput = $parserOutput;
139 $this->mLinks = $parserOutput->
getLinks();
140 $this->mImages = $parserOutput->
getImages();
147 # Convert the format of the interlanguage links
148 # I didn't want to change it in the ParserOutput, because that array is passed all
149 # the way back to the skin, so either a skin API break would be required, or an
150 # inefficient back-conversion.
152 $this->mInterlangs = [];
153 foreach ( $ill as $link ) {
154 list( $key,
$title ) = explode(
':', $link, 2 );
155 $this->mInterlangs[$key] =
$title;
158 foreach ( $this->mCategories as &$sortkey ) {
159 # If the sortkey is longer then 255 bytes, it is truncated by DB, and then doesn't match
160 # when comparing existing vs current categories, causing T27254.
161 $sortkey = mb_strcut( $sortkey, 0, 255 );
164 $this->mRecursive = $recursive;
167 $linksUpdate = $this;
168 Hooks::run(
'LinksUpdateConstructed', [ &$linksUpdate ] );
177 if ( $this->ticket ) {
181 if ( !$scopedLock ) {
182 throw new RuntimeException(
"Could not acquire lock for page ID '{$this->mId}'." );
187 $linksUpdate = $this;
188 Hooks::run(
'LinksUpdate', [ &$linksUpdate ] );
192 ScopedCallback::consume( $scopedLock );
199 $linksUpdate = $this;
200 Hooks::run(
'LinksUpdateComplete', [ &$linksUpdate, $this->ticket ] );
215 $key =
"{$dbw->getDomainID()}:LinksUpdate:$why:pageid:$pageId";
217 if ( !$scopedLock ) {
218 $logger = LoggerFactory::getInstance(
'SecondaryDataUpdate' );
219 $logger->info(
"Could not acquire lock '{key}' for page ID '{page_id}'.", [
221 'page_id' => $pageId,
234 $this->
incrTableUpdate(
'pagelinks',
'pl', $this->linkDeletions, $this->linkInsertions );
245 # Invalidate all image description pages which had links added or removed
246 $imageUpdates = $imageDeletes + array_diff_key( $this->mImages, $existingIL );
257 $this->externalLinkDeletions,
258 $this->externalLinkInsertions );
268 # Inline interwiki links
292 $categoryInserts = array_diff_assoc( $this->mCategories, $existingCL );
293 $categoryUpdates = $categoryInserts + $categoryDeletes;
301 $this->propertyDeletions,
304 # Invalidate the necessary pages
305 $this->propertyInsertions = array_diff_assoc( $this->mProperties, $existingPP );
309 # Invalidate all categories which were added, deleted or changed (set symmetric difference)
313 # Refresh links of all pages including this page
314 # This will be in a separate transaction
315 if ( $this->mRecursive ) {
319 # Update the links table freshness for this title
334 if ( $this->mTitle->getNamespace() ==
NS_FILE ) {
339 $bc = $this->mTitle->getBacklinkCache();
346 foreach ( $bc->getCascadeProtectedLinks() as
$title ) {
350 'causeAction' => $action,
351 'causeAgent' => $agent
367 Title $title, $table, $action =
'unknown', $userName =
'unknown'
369 if (
$title->getBacklinkCache()->hasLinks( $table ) ) {
376 "refreshlinks:{$table}:{$title->getPrefixedText()}"
377 ) + [
'causeAction' => $action,
'causeAgent' => $userName ]
399 if ( !$added && !$deleted ) {
403 $domainId = $this->
getDB()->getDomainID();
405 $lbf = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
407 $lbf->commitAndWaitForReplication( __METHOD__, $this->ticket, [
'domain' => $domainId ] );
410 $wp->updateCategoryCounts( $addBatch, [], $this->mId );
411 $lbf->commitAndWaitForReplication(
412 __METHOD__, $this->ticket, [
'domain' => $domainId ] );
416 $wp->updateCategoryCounts( [], $deleteBatch, $this->mId );
417 $lbf->commitAndWaitForReplication(
418 __METHOD__, $this->ticket, [
'domain' => $domainId ] );
437 $services = MediaWikiServices::getInstance();
438 $bSize = $services->getMainConfig()->get(
'UpdateRowsPerQuery' );
439 $lbf = $services->getDBLoadBalancerFactory();
441 if ( $table ===
'page_props' ) {
442 $fromField =
'pp_page';
444 $fromField =
"{$prefix}_from";
448 if ( $table ===
'pagelinks' || $table ===
'templatelinks' || $table ===
'iwlinks' ) {
449 $baseKey = ( $table ===
'iwlinks' ) ?
'iwl_prefix' :
"{$prefix}_namespace";
452 $curDeletionBatch = [];
453 $deletionBatches = [];
454 foreach ( $deletions as $ns => $dbKeys ) {
455 foreach ( $dbKeys as $dbKey => $unused ) {
456 $curDeletionBatch[$ns][$dbKey] = 1;
457 if ( ++$curBatchSize >= $bSize ) {
458 $deletionBatches[] = $curDeletionBatch;
459 $curDeletionBatch = [];
464 if ( $curDeletionBatch ) {
465 $deletionBatches[] = $curDeletionBatch;
468 foreach ( $deletionBatches as $deletionBatch ) {
471 $this->
getDB()->makeWhereFrom2d( $deletionBatch, $baseKey,
"{$prefix}_title" )
475 if ( $table ===
'langlinks' ) {
476 $toField =
'll_lang';
477 } elseif ( $table ===
'page_props' ) {
478 $toField =
'pp_propname';
480 $toField = $prefix .
'_to';
483 $deletionBatches = array_chunk( array_keys( $deletions ), $bSize );
484 foreach ( $deletionBatches as $deletionBatch ) {
485 $deleteWheres[] = [ $fromField =>
$this->mId, $toField => $deletionBatch ];
489 $domainId = $this->
getDB()->getDomainID();
491 foreach ( $deleteWheres as $deleteWhere ) {
492 $this->
getDB()->delete( $table, $deleteWhere, __METHOD__ );
493 $lbf->commitAndWaitForReplication(
494 __METHOD__, $this->ticket, [
'domain' => $domainId ]
498 $insertBatches = array_chunk( $insertions, $bSize );
499 foreach ( $insertBatches as $insertBatch ) {
500 $this->
getDB()->insert( $table, $insertBatch, __METHOD__, [
'IGNORE' ] );
501 $lbf->commitAndWaitForReplication(
502 __METHOD__, $this->ticket, [
'domain' => $domainId ]
506 if ( count( $insertions ) ) {
507 Hooks::run(
'LinksUpdateAfterInsert', [ $this, $table, $insertions ] );
519 foreach ( $this->mLinks as $ns => $dbkeys ) {
520 $diffs = isset( $existing[$ns] )
521 ? array_diff_key( $dbkeys, $existing[$ns] )
523 foreach ( $diffs as $dbk => $id ) {
526 'pl_from_namespace' => $this->mTitle->getNamespace(),
527 'pl_namespace' => $ns,
543 foreach ( $this->mTemplates as $ns => $dbkeys ) {
544 $diffs = isset( $existing[$ns] ) ? array_diff_key( $dbkeys, $existing[$ns] ) : $dbkeys;
545 foreach ( $diffs as $dbk => $id ) {
548 'tl_from_namespace' => $this->mTitle->getNamespace(),
549 'tl_namespace' => $ns,
566 $diffs = array_diff_key( $this->mImages, $existing );
567 foreach ( $diffs as $iname => $dummy ) {
570 'il_from_namespace' => $this->mTitle->getNamespace(),
585 $diffs = array_diff_key( $this->mExternals, $existing );
586 foreach ( $diffs as $url => $dummy ) {
591 'el_index' => $index,
592 'el_index_60' => substr( $index, 0, 60 ),
610 $diffs = array_diff_assoc( $this->mCategories, $existing );
612 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
614 foreach ( $diffs as $name => $prefix ) {
616 $contLang->findVariantLink( $name, $nt,
true );
618 $type = MediaWikiServices::getInstance()->getNamespaceInfo()->
619 getCategoryLinkType( $this->mTitle->getNamespace() );
621 # Treat custom sortkeys as a prefix, so that if multiple
622 # things are forced to sort as '*' or something, they'll
623 # sort properly in the category rather than in page_id
625 $sortkey = $collation->getSortKey( $this->mTitle->getCategorySortkey( $prefix ) );
630 'cl_sortkey' => $sortkey,
631 'cl_timestamp' => $this->
getDB()->timestamp(),
632 'cl_sortkey_prefix' => $prefix,
649 $diffs = array_diff_assoc( $this->mInterlangs, $existing );
668 $diffs = array_diff_assoc( $this->mProperties, $existing );
671 foreach ( array_keys( $diffs ) as $name ) {
697 $value = $this->mProperties[$prop];
701 'pp_propname' => $prop,
702 'pp_value' => $value,
725 if ( is_int( $value ) || is_float( $value ) || is_bool( $value ) ) {
726 return floatval( $value );
740 foreach ( $this->mInterwikis as $prefix => $dbkeys ) {
741 $diffs = isset( $existing[$prefix] )
742 ? array_diff_key( $dbkeys, $existing[$prefix] )
745 foreach ( $diffs as $dbk => $id ) {
748 'iwl_prefix' => $prefix,
765 foreach ( $existing as $ns => $dbkeys ) {
766 if ( isset( $this->mLinks[$ns] ) ) {
767 $del[$ns] = array_diff_key( $existing[$ns], $this->mLinks[$ns] );
769 $del[$ns] = $existing[$ns];
784 foreach ( $existing as $ns => $dbkeys ) {
785 if ( isset( $this->mTemplates[$ns] ) ) {
786 $del[$ns] = array_diff_key( $existing[$ns], $this->mTemplates[$ns] );
788 $del[$ns] = $existing[$ns];
802 return array_diff_key( $existing, $this->mImages );
812 return array_diff_key( $existing, $this->mExternals );
822 return array_diff_assoc( $existing, $this->mCategories );
832 return array_diff_assoc( $existing, $this->mInterlangs );
841 return array_diff_assoc( $existing, $this->mProperties );
852 foreach ( $existing as $prefix => $dbkeys ) {
853 if ( isset( $this->mInterwikis[$prefix] ) ) {
854 $del[$prefix] = array_diff_key( $existing[$prefix], $this->mInterwikis[$prefix] );
856 $del[$prefix] = $existing[$prefix];
869 $res = $this->
getDB()->select(
'pagelinks', [
'pl_namespace',
'pl_title' ],
870 [
'pl_from' => $this->mId ], __METHOD__ );
872 foreach (
$res as $row ) {
873 if ( !isset( $arr[$row->pl_namespace] ) ) {
874 $arr[$row->pl_namespace] = [];
876 $arr[$row->pl_namespace][$row->pl_title] = 1;
888 $res = $this->
getDB()->select(
'templatelinks', [
'tl_namespace',
'tl_title' ],
889 [
'tl_from' => $this->mId ], __METHOD__ );
891 foreach (
$res as $row ) {
892 if ( !isset( $arr[$row->tl_namespace] ) ) {
893 $arr[$row->tl_namespace] = [];
895 $arr[$row->tl_namespace][$row->tl_title] = 1;
907 $res = $this->
getDB()->select(
'imagelinks', [
'il_to' ],
908 [
'il_from' => $this->mId ], __METHOD__ );
910 foreach (
$res as $row ) {
911 $arr[$row->il_to] = 1;
923 $res = $this->
getDB()->select(
'externallinks', [
'el_to' ],
924 [
'el_from' => $this->mId ], __METHOD__ );
926 foreach (
$res as $row ) {
927 $arr[$row->el_to] = 1;
939 $res = $this->
getDB()->select(
'categorylinks', [
'cl_to',
'cl_sortkey_prefix' ],
940 [
'cl_from' => $this->mId ], __METHOD__ );
942 foreach (
$res as $row ) {
943 $arr[$row->cl_to] = $row->cl_sortkey_prefix;
956 $res = $this->
getDB()->select(
'langlinks', [
'll_lang',
'll_title' ],
957 [
'll_from' => $this->mId ], __METHOD__ );
959 foreach (
$res as $row ) {
960 $arr[$row->ll_lang] = $row->ll_title;
971 $res = $this->
getDB()->select(
'iwlinks', [
'iwl_prefix',
'iwl_title' ],
972 [
'iwl_from' => $this->mId ], __METHOD__ );
974 foreach (
$res as $row ) {
975 if ( !isset( $arr[$row->iwl_prefix] ) ) {
976 $arr[$row->iwl_prefix] = [];
978 $arr[$row->iwl_prefix][$row->iwl_title] = 1;
990 $res = $this->
getDB()->select(
'page_props', [
'pp_propname',
'pp_value' ],
991 [
'pp_page' => $this->mId ], __METHOD__ );
993 foreach (
$res as $row ) {
994 $arr[$row->pp_propname] = $row->pp_value;
1033 $this->mRevision = $revision;
1051 $this->user =
$user;
1070 foreach ( $changed as $name => $value ) {
1073 if ( !is_array( $inv ) ) {
1076 foreach ( $inv as $table ) {
1080 [
'causeAction' =>
'page-props' ]
1095 if ( $this->linkInsertions ===
null ) {
1099 foreach ( $this->linkInsertions as $insertion ) {
1100 $result[] =
Title::makeTitle( $insertion[
'pl_namespace'], $insertion[
'pl_title'] );
1112 if ( $this->linkDeletions ===
null ) {
1116 foreach ( $this->linkDeletions as $ns => $titles ) {
1117 foreach ( $titles as
$title => $unused ) {
1132 if ( $this->externalLinkInsertions ===
null ) {
1136 foreach ( $this->externalLinkInsertions as $key => $value ) {
1137 $result[] = $value[
'el_to'];
1149 if ( $this->externalLinkDeletions ===
null ) {
1152 return array_keys( $this->externalLinkDeletions );
1181 $timestamp = $this->mParserOutput->getCacheTime();
1182 $this->
getDB()->update(
'page',
1183 [
'page_links_updated' => $this->
getDB()->timestamp( $timestamp ) ],
1184 [
'page_id' => $this->mId ],