MediaWiki  master
LinksUpdate.php
Go to the documentation of this file.
1 <?php
27 
35 class LinksUpdate extends DataUpdate {
36  // @todo make members protected, but make sure extensions don't break
37 
39  public $mId;
40 
42  public $mTitle;
43 
46 
51  public $mLinks;
52 
54  public $mImages;
55 
57  public $mTemplates;
58 
60  public $mExternals;
61 
63  public $mCategories;
64 
66  public $mInterlangs;
67 
69  public $mInterwikis;
70 
72  public $mProperties;
73 
75  public $mRecursive;
76 
78  private $mRevision;
79 
84  private $linkInsertions = null;
85 
89  private $linkDeletions = null;
90 
94  private $externalLinkInsertions = null;
95 
99  private $externalLinkDeletions = null;
100 
104  private $propertyInsertions = null;
105 
109  private $propertyDeletions = null;
110 
114  private $user;
115 
117  private $db;
118 
125  public function __construct( Title $title, ParserOutput $parserOutput, $recursive = true ) {
126  parent::__construct();
127 
128  $this->mTitle = $title;
129 
130  if ( !$this->mId ) {
131  // NOTE: subclasses may initialize mId before calling this constructor!
132  $this->mId = $title->getArticleID( Title::READ_LATEST );
133  }
134 
135  if ( !$this->mId ) {
136  throw new InvalidArgumentException(
137  "The Title object yields no ID. Perhaps the page doesn't exist?"
138  );
139  }
140 
141  $this->mParserOutput = $parserOutput;
142 
143  $this->mLinks = $parserOutput->getLinks();
144  $this->mImages = $parserOutput->getImages();
145  $this->mTemplates = $parserOutput->getTemplates();
146  $this->mExternals = $parserOutput->getExternalLinks();
147  $this->mCategories = $parserOutput->getCategories();
148  $this->mProperties = $parserOutput->getProperties();
149  $this->mInterwikis = $parserOutput->getInterwikiLinks();
150 
151  # Convert the format of the interlanguage links
152  # I didn't want to change it in the ParserOutput, because that array is passed all
153  # the way back to the skin, so either a skin API break would be required, or an
154  # inefficient back-conversion.
155  $ill = $parserOutput->getLanguageLinks();
156  $this->mInterlangs = [];
157  foreach ( $ill as $link ) {
158  list( $key, $title ) = explode( ':', $link, 2 );
159  $this->mInterlangs[$key] = $title;
160  }
161 
162  foreach ( $this->mCategories as &$sortkey ) {
163  # If the sortkey is longer then 255 bytes, it is truncated by DB, and then doesn't match
164  # when comparing existing vs current categories, causing T27254.
165  $sortkey = mb_strcut( $sortkey, 0, 255 );
166  }
167 
168  $this->mRecursive = $recursive;
169 
170  // Avoid PHP 7.1 warning from passing $this by reference
171  $linksUpdate = $this;
172  Hooks::run( 'LinksUpdateConstructed', [ &$linksUpdate ] );
173  }
174 
180  public function doUpdate() {
181  if ( $this->ticket ) {
182  // Make sure all links update threads see the changes of each other.
183  // This handles the case when updates have to batched into several COMMITs.
184  $scopedLock = self::acquirePageLock( $this->getDB(), $this->mId );
185  if ( !$scopedLock ) {
186  throw new RuntimeException( "Could not acquire lock for page ID '{$this->mId}'." );
187  }
188  }
189 
190  // Avoid PHP 7.1 warning from passing $this by reference
191  $linksUpdate = $this;
192  Hooks::run( 'LinksUpdate', [ &$linksUpdate ] );
193  $this->doIncrementalUpdate();
194 
195  // Commit and release the lock (if set)
196  ScopedCallback::consume( $scopedLock );
197  // Run post-commit hook handlers without DBO_TRX
199  $this->getDB(),
200  __METHOD__,
201  function () {
202  // Avoid PHP 7.1 warning from passing $this by reference
203  $linksUpdate = $this;
204  Hooks::run( 'LinksUpdateComplete', [ &$linksUpdate, $this->ticket ] );
205  }
206  ) );
207  }
208 
218  public static function acquirePageLock( IDatabase $dbw, $pageId, $why = 'atomicity' ) {
219  $key = "{$dbw->getDomainID()}:LinksUpdate:$why:pageid:$pageId"; // per-wiki
220  $scopedLock = $dbw->getScopedLockAndFlush( $key, __METHOD__, 15 );
221  if ( !$scopedLock ) {
222  $logger = LoggerFactory::getInstance( 'SecondaryDataUpdate' );
223  $logger->info( "Could not acquire lock '{key}' for page ID '{page_id}'.", [
224  'key' => $key,
225  'page_id' => $pageId,
226  ] );
227  return null;
228  }
229 
230  return $scopedLock;
231  }
232 
233  protected function doIncrementalUpdate() {
234  # Page links
235  $existingPL = $this->getExistingLinks();
236  $this->linkDeletions = $this->getLinkDeletions( $existingPL );
237  $this->linkInsertions = $this->getLinkInsertions( $existingPL );
238  $this->incrTableUpdate( 'pagelinks', 'pl', $this->linkDeletions, $this->linkInsertions );
239 
240  # Image links
241  $existingIL = $this->getExistingImages();
242  $imageDeletes = $this->getImageDeletions( $existingIL );
243  $this->incrTableUpdate(
244  'imagelinks',
245  'il',
246  $imageDeletes,
247  $this->getImageInsertions( $existingIL ) );
248 
249  # Invalidate all image description pages which had links added or removed
250  $imageUpdates = $imageDeletes + array_diff_key( $this->mImages, $existingIL );
251  $this->invalidateImageDescriptions( $imageUpdates );
252 
253  # External links
254  $existingEL = $this->getExistingExternals();
255  $this->externalLinkDeletions = $this->getExternalDeletions( $existingEL );
256  $this->externalLinkInsertions = $this->getExternalInsertions(
257  $existingEL );
258  $this->incrTableUpdate(
259  'externallinks',
260  'el',
261  $this->externalLinkDeletions,
262  $this->externalLinkInsertions );
263 
264  # Language links
265  $existingLL = $this->getExistingInterlangs();
266  $this->incrTableUpdate(
267  'langlinks',
268  'll',
269  $this->getInterlangDeletions( $existingLL ),
270  $this->getInterlangInsertions( $existingLL ) );
271 
272  # Inline interwiki links
273  $existingIW = $this->getExistingInterwikis();
274  $this->incrTableUpdate(
275  'iwlinks',
276  'iwl',
277  $this->getInterwikiDeletions( $existingIW ),
278  $this->getInterwikiInsertions( $existingIW ) );
279 
280  # Template links
281  $existingTL = $this->getExistingTemplates();
282  $this->incrTableUpdate(
283  'templatelinks',
284  'tl',
285  $this->getTemplateDeletions( $existingTL ),
286  $this->getTemplateInsertions( $existingTL ) );
287 
288  # Category links
289  $existingCL = $this->getExistingCategories();
290  $categoryDeletes = $this->getCategoryDeletions( $existingCL );
291  $this->incrTableUpdate(
292  'categorylinks',
293  'cl',
294  $categoryDeletes,
295  $this->getCategoryInsertions( $existingCL ) );
296  $categoryInserts = array_diff_assoc( $this->mCategories, $existingCL );
297  $categoryUpdates = $categoryInserts + $categoryDeletes;
298 
299  # Page properties
300  $existingPP = $this->getExistingProperties();
301  $this->propertyDeletions = $this->getPropertyDeletions( $existingPP );
302  $this->incrTableUpdate(
303  'page_props',
304  'pp',
305  $this->propertyDeletions,
306  $this->getPropertyInsertions( $existingPP ) );
307 
308  # Invalidate the necessary pages
309  $this->propertyInsertions = array_diff_assoc( $this->mProperties, $existingPP );
310  $changed = $this->propertyDeletions + $this->propertyInsertions;
311  $this->invalidateProperties( $changed );
312 
313  # Invalidate all categories which were added, deleted or changed (set symmetric difference)
314  $this->invalidateCategories( $categoryUpdates );
315  $this->updateCategoryCounts( $categoryInserts, $categoryDeletes );
316 
317  # Refresh links of all pages including this page
318  # This will be in a separate transaction
319  if ( $this->mRecursive ) {
320  $this->queueRecursiveJobs();
321  }
322 
323  # Update the links table freshness for this title
324  $this->updateLinksTimestamp();
325  }
326 
333  protected function queueRecursiveJobs() {
334  $action = $this->getCauseAction();
335  $agent = $this->getCauseAgent();
336 
337  self::queueRecursiveJobsForTable( $this->mTitle, 'templatelinks', $action, $agent );
338  if ( $this->mTitle->getNamespace() == NS_FILE ) {
339  // Process imagelinks in case the title is or was a redirect
340  self::queueRecursiveJobsForTable( $this->mTitle, 'imagelinks', $action, $agent );
341  }
342 
343  $bc = $this->mTitle->getBacklinkCache();
344  // Get jobs for cascade-protected backlinks for a high priority queue.
345  // If meta-templates change to using a new template, the new template
346  // should be implicitly protected as soon as possible, if applicable.
347  // These jobs duplicate a subset of the above ones, but can run sooner.
348  // Which ever runs first generally no-ops the other one.
349  $jobs = [];
350  foreach ( $bc->getCascadeProtectedLinks() as $title ) {
352  $title,
353  [
354  'causeAction' => $action,
355  'causeAgent' => $agent
356  ]
357  );
358  }
359  JobQueueGroup::singleton()->push( $jobs );
360  }
361 
370  public static function queueRecursiveJobsForTable(
371  Title $title, $table, $action = 'unknown', $userName = 'unknown'
372  ) {
373  if ( $title->getBacklinkCache()->hasLinks( $table ) ) {
374  $job = new RefreshLinksJob(
375  $title,
376  [
377  'table' => $table,
378  'recursive' => true,
379  ] + Job::newRootJobParams( // "overall" refresh links job info
380  "refreshlinks:{$table}:{$title->getPrefixedText()}"
381  ) + [ 'causeAction' => $action, 'causeAgent' => $userName ]
382  );
383 
384  JobQueueGroup::singleton()->push( $job );
385  }
386  }
387 
391  private function invalidateCategories( $cats ) {
392  PurgeJobUtils::invalidatePages( $this->getDB(), NS_CATEGORY, array_keys( $cats ) );
393  }
394 
400  private function updateCategoryCounts( array $added, array $deleted ) {
401  global $wgUpdateRowsPerQuery;
402 
403  if ( !$added && !$deleted ) {
404  return;
405  }
406 
407  $domainId = $this->getDB()->getDomainID();
408  $wp = WikiPage::factory( $this->mTitle );
409  $lbf = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
410  // T163801: try to release any row locks to reduce contention
411  $lbf->commitAndWaitForReplication( __METHOD__, $this->ticket, [ 'domain' => $domainId ] );
412 
413  foreach ( array_chunk( array_keys( $added ), $wgUpdateRowsPerQuery ) as $addBatch ) {
414  $wp->updateCategoryCounts( $addBatch, [], $this->mId );
415  $lbf->commitAndWaitForReplication(
416  __METHOD__, $this->ticket, [ 'domain' => $domainId ] );
417  }
418 
419  foreach ( array_chunk( array_keys( $deleted ), $wgUpdateRowsPerQuery ) as $deleteBatch ) {
420  $wp->updateCategoryCounts( [], $deleteBatch, $this->mId );
421  $lbf->commitAndWaitForReplication(
422  __METHOD__, $this->ticket, [ 'domain' => $domainId ] );
423  }
424  }
425 
429  private function invalidateImageDescriptions( array $images ) {
430  PurgeJobUtils::invalidatePages( $this->getDB(), NS_FILE, array_keys( $images ) );
431  }
432 
440  private function incrTableUpdate( $table, $prefix, $deletions, $insertions ) {
441  $services = MediaWikiServices::getInstance();
442  $bSize = $services->getMainConfig()->get( 'UpdateRowsPerQuery' );
443  $lbf = $services->getDBLoadBalancerFactory();
444 
445  if ( $table === 'page_props' ) {
446  $fromField = 'pp_page';
447  } else {
448  $fromField = "{$prefix}_from";
449  }
450 
451  $deleteWheres = []; // list of WHERE clause arrays for each DB delete() call
452  if ( $table === 'pagelinks' || $table === 'templatelinks' || $table === 'iwlinks' ) {
453  $baseKey = ( $table === 'iwlinks' ) ? 'iwl_prefix' : "{$prefix}_namespace";
454 
455  $curBatchSize = 0;
456  $curDeletionBatch = [];
457  $deletionBatches = [];
458  foreach ( $deletions as $ns => $dbKeys ) {
459  foreach ( $dbKeys as $dbKey => $unused ) {
460  $curDeletionBatch[$ns][$dbKey] = 1;
461  if ( ++$curBatchSize >= $bSize ) {
462  $deletionBatches[] = $curDeletionBatch;
463  $curDeletionBatch = [];
464  $curBatchSize = 0;
465  }
466  }
467  }
468  if ( $curDeletionBatch ) {
469  $deletionBatches[] = $curDeletionBatch;
470  }
471 
472  foreach ( $deletionBatches as $deletionBatch ) {
473  $deleteWheres[] = [
474  $fromField => $this->mId,
475  $this->getDB()->makeWhereFrom2d( $deletionBatch, $baseKey, "{$prefix}_title" )
476  ];
477  }
478  } else {
479  if ( $table === 'langlinks' ) {
480  $toField = 'll_lang';
481  } elseif ( $table === 'page_props' ) {
482  $toField = 'pp_propname';
483  } else {
484  $toField = $prefix . '_to';
485  }
486 
487  $deletionBatches = array_chunk( array_keys( $deletions ), $bSize );
488  foreach ( $deletionBatches as $deletionBatch ) {
489  $deleteWheres[] = [ $fromField => $this->mId, $toField => $deletionBatch ];
490  }
491  }
492 
493  $domainId = $this->getDB()->getDomainID();
494 
495  foreach ( $deleteWheres as $deleteWhere ) {
496  $this->getDB()->delete( $table, $deleteWhere, __METHOD__ );
497  $lbf->commitAndWaitForReplication(
498  __METHOD__, $this->ticket, [ 'domain' => $domainId ]
499  );
500  }
501 
502  $insertBatches = array_chunk( $insertions, $bSize );
503  foreach ( $insertBatches as $insertBatch ) {
504  $this->getDB()->insert( $table, $insertBatch, __METHOD__, [ 'IGNORE' ] );
505  $lbf->commitAndWaitForReplication(
506  __METHOD__, $this->ticket, [ 'domain' => $domainId ]
507  );
508  }
509 
510  if ( count( $insertions ) ) {
511  Hooks::run( 'LinksUpdateAfterInsert', [ $this, $table, $insertions ] );
512  }
513  }
514 
522  private function getLinkInsertions( $existing = [] ) {
523  $arr = [];
524  foreach ( $this->mLinks as $ns => $dbkeys ) {
525  $diffs = isset( $existing[$ns] )
526  ? array_diff_key( $dbkeys, $existing[$ns] )
527  : $dbkeys;
528  foreach ( $diffs as $dbk => $id ) {
529  $arr[] = [
530  'pl_from' => $this->mId,
531  'pl_from_namespace' => $this->mTitle->getNamespace(),
532  'pl_namespace' => $ns,
533  'pl_title' => $dbk
534  ];
535  }
536  }
537 
538  return $arr;
539  }
540 
546  private function getTemplateInsertions( $existing = [] ) {
547  $arr = [];
548  foreach ( $this->mTemplates as $ns => $dbkeys ) {
549  $diffs = isset( $existing[$ns] ) ? array_diff_key( $dbkeys, $existing[$ns] ) : $dbkeys;
550  foreach ( $diffs as $dbk => $id ) {
551  $arr[] = [
552  'tl_from' => $this->mId,
553  'tl_from_namespace' => $this->mTitle->getNamespace(),
554  'tl_namespace' => $ns,
555  'tl_title' => $dbk
556  ];
557  }
558  }
559 
560  return $arr;
561  }
562 
569  private function getImageInsertions( $existing = [] ) {
570  $arr = [];
571  $diffs = array_diff_key( $this->mImages, $existing );
572  foreach ( $diffs as $iname => $dummy ) {
573  $arr[] = [
574  'il_from' => $this->mId,
575  'il_from_namespace' => $this->mTitle->getNamespace(),
576  'il_to' => $iname
577  ];
578  }
579 
580  return $arr;
581  }
582 
588  private function getExternalInsertions( $existing = [] ) {
589  $arr = [];
590  $diffs = array_diff_key( $this->mExternals, $existing );
591  foreach ( $diffs as $url => $dummy ) {
592  foreach ( LinkFilter::makeIndexes( $url ) as $index ) {
593  $arr[] = [
594  'el_from' => $this->mId,
595  'el_to' => $url,
596  'el_index' => $index,
597  'el_index_60' => substr( $index, 0, 60 ),
598  ];
599  }
600  }
601 
602  return $arr;
603  }
604 
613  private function getCategoryInsertions( $existing = [] ) {
614  global $wgCategoryCollation;
615  $diffs = array_diff_assoc( $this->mCategories, $existing );
616  $arr = [];
617  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
618  $collation = Collation::singleton();
619  foreach ( $diffs as $name => $prefix ) {
620  $nt = Title::makeTitleSafe( NS_CATEGORY, $name );
621  $contLang->findVariantLink( $name, $nt, true );
622 
623  $type = MediaWikiServices::getInstance()->getNamespaceInfo()->
624  getCategoryLinkType( $this->mTitle->getNamespace() );
625 
626  # Treat custom sortkeys as a prefix, so that if multiple
627  # things are forced to sort as '*' or something, they'll
628  # sort properly in the category rather than in page_id
629  # order or such.
630  $sortkey = $collation->getSortKey( $this->mTitle->getCategorySortkey( $prefix ) );
631 
632  $arr[] = [
633  'cl_from' => $this->mId,
634  'cl_to' => $name,
635  'cl_sortkey' => $sortkey,
636  'cl_timestamp' => $this->getDB()->timestamp(),
637  'cl_sortkey_prefix' => $prefix,
638  'cl_collation' => $wgCategoryCollation,
639  'cl_type' => $type,
640  ];
641  }
642 
643  return $arr;
644  }
645 
653  private function getInterlangInsertions( $existing = [] ) {
654  $diffs = array_diff_assoc( $this->mInterlangs, $existing );
655  $arr = [];
656  foreach ( $diffs as $lang => $title ) {
657  $arr[] = [
658  'll_from' => $this->mId,
659  'll_lang' => $lang,
660  'll_title' => $title
661  ];
662  }
663 
664  return $arr;
665  }
666 
672  function getPropertyInsertions( $existing = [] ) {
673  $diffs = array_diff_assoc( $this->mProperties, $existing );
674 
675  $arr = [];
676  foreach ( array_keys( $diffs ) as $name ) {
677  $arr[] = $this->getPagePropRowData( $name );
678  }
679 
680  return $arr;
681  }
682 
699  private function getPagePropRowData( $prop ) {
701 
702  $value = $this->mProperties[$prop];
703 
704  $row = [
705  'pp_page' => $this->mId,
706  'pp_propname' => $prop,
707  'pp_value' => $value,
708  ];
709 
710  if ( $wgPagePropsHaveSortkey ) {
711  $row['pp_sortkey'] = $this->getPropertySortKeyValue( $value );
712  }
713 
714  return $row;
715  }
716 
729  private function getPropertySortKeyValue( $value ) {
730  if ( is_int( $value ) || is_float( $value ) || is_bool( $value ) ) {
731  return floatval( $value );
732  }
733 
734  return null;
735  }
736 
743  private function getInterwikiInsertions( $existing = [] ) {
744  $arr = [];
745  foreach ( $this->mInterwikis as $prefix => $dbkeys ) {
746  $diffs = isset( $existing[$prefix] )
747  ? array_diff_key( $dbkeys, $existing[$prefix] )
748  : $dbkeys;
749 
750  foreach ( $diffs as $dbk => $id ) {
751  $arr[] = [
752  'iwl_from' => $this->mId,
753  'iwl_prefix' => $prefix,
754  'iwl_title' => $dbk
755  ];
756  }
757  }
758 
759  return $arr;
760  }
761 
768  private function getLinkDeletions( $existing ) {
769  $del = [];
770  foreach ( $existing as $ns => $dbkeys ) {
771  if ( isset( $this->mLinks[$ns] ) ) {
772  $del[$ns] = array_diff_key( $existing[$ns], $this->mLinks[$ns] );
773  } else {
774  $del[$ns] = $existing[$ns];
775  }
776  }
777 
778  return $del;
779  }
780 
787  private function getTemplateDeletions( $existing ) {
788  $del = [];
789  foreach ( $existing as $ns => $dbkeys ) {
790  if ( isset( $this->mTemplates[$ns] ) ) {
791  $del[$ns] = array_diff_key( $existing[$ns], $this->mTemplates[$ns] );
792  } else {
793  $del[$ns] = $existing[$ns];
794  }
795  }
796 
797  return $del;
798  }
799 
806  private function getImageDeletions( $existing ) {
807  return array_diff_key( $existing, $this->mImages );
808  }
809 
816  private function getExternalDeletions( $existing ) {
817  return array_diff_key( $existing, $this->mExternals );
818  }
819 
826  private function getCategoryDeletions( $existing ) {
827  return array_diff_assoc( $existing, $this->mCategories );
828  }
829 
836  private function getInterlangDeletions( $existing ) {
837  return array_diff_assoc( $existing, $this->mInterlangs );
838  }
839 
845  private function getPropertyDeletions( $existing ) {
846  return array_diff_assoc( $existing, $this->mProperties );
847  }
848 
855  private function getInterwikiDeletions( $existing ) {
856  $del = [];
857  foreach ( $existing as $prefix => $dbkeys ) {
858  if ( isset( $this->mInterwikis[$prefix] ) ) {
859  $del[$prefix] = array_diff_key( $existing[$prefix], $this->mInterwikis[$prefix] );
860  } else {
861  $del[$prefix] = $existing[$prefix];
862  }
863  }
864 
865  return $del;
866  }
867 
873  private function getExistingLinks() {
874  $res = $this->getDB()->select( 'pagelinks', [ 'pl_namespace', 'pl_title' ],
875  [ 'pl_from' => $this->mId ], __METHOD__ );
876  $arr = [];
877  foreach ( $res as $row ) {
878  if ( !isset( $arr[$row->pl_namespace] ) ) {
879  $arr[$row->pl_namespace] = [];
880  }
881  $arr[$row->pl_namespace][$row->pl_title] = 1;
882  }
883 
884  return $arr;
885  }
886 
892  private function getExistingTemplates() {
893  $res = $this->getDB()->select( 'templatelinks', [ 'tl_namespace', 'tl_title' ],
894  [ 'tl_from' => $this->mId ], __METHOD__ );
895  $arr = [];
896  foreach ( $res as $row ) {
897  if ( !isset( $arr[$row->tl_namespace] ) ) {
898  $arr[$row->tl_namespace] = [];
899  }
900  $arr[$row->tl_namespace][$row->tl_title] = 1;
901  }
902 
903  return $arr;
904  }
905 
911  private function getExistingImages() {
912  $res = $this->getDB()->select( 'imagelinks', [ 'il_to' ],
913  [ 'il_from' => $this->mId ], __METHOD__ );
914  $arr = [];
915  foreach ( $res as $row ) {
916  $arr[$row->il_to] = 1;
917  }
918 
919  return $arr;
920  }
921 
927  private function getExistingExternals() {
928  $res = $this->getDB()->select( 'externallinks', [ 'el_to' ],
929  [ 'el_from' => $this->mId ], __METHOD__ );
930  $arr = [];
931  foreach ( $res as $row ) {
932  $arr[$row->el_to] = 1;
933  }
934 
935  return $arr;
936  }
937 
943  private function getExistingCategories() {
944  $res = $this->getDB()->select( 'categorylinks', [ 'cl_to', 'cl_sortkey_prefix' ],
945  [ 'cl_from' => $this->mId ], __METHOD__ );
946  $arr = [];
947  foreach ( $res as $row ) {
948  $arr[$row->cl_to] = $row->cl_sortkey_prefix;
949  }
950 
951  return $arr;
952  }
953 
960  private function getExistingInterlangs() {
961  $res = $this->getDB()->select( 'langlinks', [ 'll_lang', 'll_title' ],
962  [ 'll_from' => $this->mId ], __METHOD__ );
963  $arr = [];
964  foreach ( $res as $row ) {
965  $arr[$row->ll_lang] = $row->ll_title;
966  }
967 
968  return $arr;
969  }
970 
975  private function getExistingInterwikis() {
976  $res = $this->getDB()->select( 'iwlinks', [ 'iwl_prefix', 'iwl_title' ],
977  [ 'iwl_from' => $this->mId ], __METHOD__ );
978  $arr = [];
979  foreach ( $res as $row ) {
980  if ( !isset( $arr[$row->iwl_prefix] ) ) {
981  $arr[$row->iwl_prefix] = [];
982  }
983  $arr[$row->iwl_prefix][$row->iwl_title] = 1;
984  }
985 
986  return $arr;
987  }
988 
994  private function getExistingProperties() {
995  $res = $this->getDB()->select( 'page_props', [ 'pp_propname', 'pp_value' ],
996  [ 'pp_page' => $this->mId ], __METHOD__ );
997  $arr = [];
998  foreach ( $res as $row ) {
999  $arr[$row->pp_propname] = $row->pp_value;
1000  }
1001 
1002  return $arr;
1003  }
1004 
1009  public function getTitle() {
1010  return $this->mTitle;
1011  }
1012 
1018  public function getParserOutput() {
1019  return $this->mParserOutput;
1020  }
1021 
1026  public function getImages() {
1027  return $this->mImages;
1028  }
1029 
1037  public function setRevision( Revision $revision ) {
1038  $this->mRevision = $revision;
1039  }
1040 
1045  public function getRevision() {
1046  return $this->mRevision;
1047  }
1048 
1055  public function setTriggeringUser( User $user ) {
1056  $this->user = $user;
1057  }
1058 
1063  public function getTriggeringUser() {
1064  return $this->user;
1065  }
1066 
1071  private function invalidateProperties( $changed ) {
1073 
1074  $jobs = [];
1075  foreach ( $changed as $name => $value ) {
1076  if ( isset( $wgPagePropLinkInvalidations[$name] ) ) {
1077  $inv = $wgPagePropLinkInvalidations[$name];
1078  if ( !is_array( $inv ) ) {
1079  $inv = [ $inv ];
1080  }
1081  foreach ( $inv as $table ) {
1083  $this->mTitle,
1084  $table,
1085  [ 'causeAction' => 'page-props' ]
1086  );
1087  }
1088  }
1089  }
1090 
1091  JobQueueGroup::singleton()->lazyPush( $jobs );
1092  }
1093 
1099  public function getAddedLinks() {
1100  if ( $this->linkInsertions === null ) {
1101  return null;
1102  }
1103  $result = [];
1104  foreach ( $this->linkInsertions as $insertion ) {
1105  $result[] = Title::makeTitle( $insertion['pl_namespace'], $insertion['pl_title'] );
1106  }
1107 
1108  return $result;
1109  }
1110 
1116  public function getRemovedLinks() {
1117  if ( $this->linkDeletions === null ) {
1118  return null;
1119  }
1120  $result = [];
1121  foreach ( $this->linkDeletions as $ns => $titles ) {
1122  foreach ( $titles as $title => $unused ) {
1123  $result[] = Title::makeTitle( $ns, $title );
1124  }
1125  }
1126 
1127  return $result;
1128  }
1129 
1136  public function getAddedExternalLinks() {
1137  if ( $this->externalLinkInsertions === null ) {
1138  return null;
1139  }
1140  $result = [];
1141  foreach ( $this->externalLinkInsertions as $key => $value ) {
1142  $result[] = $value['el_to'];
1143  }
1144  return $result;
1145  }
1146 
1153  public function getRemovedExternalLinks() {
1154  if ( $this->externalLinkDeletions === null ) {
1155  return null;
1156  }
1157  return array_keys( $this->externalLinkDeletions );
1158  }
1159 
1166  public function getAddedProperties() {
1168  }
1169 
1176  public function getRemovedProperties() {
1177  return $this->propertyDeletions;
1178  }
1179 
1183  private function updateLinksTimestamp() {
1184  if ( $this->mId ) {
1185  // The link updates made here only reflect the freshness of the parser output
1186  $timestamp = $this->mParserOutput->getCacheTime();
1187  $this->getDB()->update( 'page',
1188  [ 'page_links_updated' => $this->getDB()->timestamp( $timestamp ) ],
1189  [ 'page_id' => $this->mId ],
1190  __METHOD__
1191  );
1192  }
1193  }
1194 
1198  protected function getDB() {
1199  if ( !$this->db ) {
1200  $this->db = wfGetDB( DB_MASTER );
1201  }
1202 
1203  return $this->db;
1204  }
1205 
1212  public function isRecursive() {
1213  return $this->mRecursive;
1214  }
1215 }
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:142
setRevision(Revision $revision)
Set the revision corresponding to this LinksUpdate.
updateLinksTimestamp()
Update links table freshness.
null array $externalLinkDeletions
Deleted external links if calculated.
Definition: LinksUpdate.php:99
getArticleID( $flags=0)
Get the article ID for this Title from the link cache, adding it if necessary.
Definition: Title.php:3161
getExternalDeletions( $existing)
Given an array of existing external links, returns those links which are not in $this and thus should...
$wgPagePropsHaveSortkey
Whether the page_props table has a pp_sortkey column.
queueRecursiveJobs()
Queue recursive jobs for this page.
getExternalInsertions( $existing=[])
Get an array of externallinks insertions.
bool $mRecursive
Whether to queue jobs for recursive updates.
Definition: LinksUpdate.php:75
static invalidatePages(IDatabase $dbw, $namespace, array $dbkeys)
Invalidate the cache of a list of pages from a single namespace.
getImageDeletions( $existing)
Given an array of existing images, returns those images which are not in $this and thus should be del...
getCategoryInsertions( $existing=[])
Get an array of category insertions.
getExistingInterwikis()
Get an array of existing inline interwiki links, as a 2-D array.
array $mProperties
Map of arbitrary name to value.
Definition: LinksUpdate.php:72
getInterlangInsertions( $existing=[])
Get an array of interlanguage link insertions.
getTemplateInsertions( $existing=[])
Get an array of template insertions.
static singleton()
Definition: Collation.php:36
if(!isset( $args[0])) $lang
invalidateImageDescriptions(array $images)
incrTableUpdate( $table, $prefix, $deletions, $insertions)
Update a table by doing a delete query then an insert query.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Revision $mRevision
Revision for which this update has been triggered.
Definition: LinksUpdate.php:78
getExistingProperties()
Get an array of existing categories, with the name in the key and sort key in the value...
Title $mTitle
Title object of the article linked from.
Definition: LinksUpdate.php:42
getBacklinkCache()
Get a backlink cache object.
Definition: Title.php:4506
getInterlangDeletions( $existing)
Given an array of existing interlanguage links, returns those links which are not in $this and thus s...
getInterwikiDeletions( $existing)
Given an array of existing interwiki links, returns those links which are not in $this and thus shoul...
$wgCategoryCollation
Specify how category names should be sorted, when listed on a category page.
getCategoryDeletions( $existing)
Given an array of existing categories, returns those categories which are not in $this and thus shoul...
const DB_MASTER
Definition: defines.php:26
__construct(Title $title, ParserOutput $parserOutput, $recursive=true)
getExistingInterlangs()
Get an array of existing interlanguage links, with the language code in the key and the title in the ...
static newRootJobParams( $key)
Get "root job" parameters for a task.
Definition: Job.php:308
ParserOutput $mParserOutput
Definition: LinksUpdate.php:45
array [] null $linkInsertions
Added links if calculated.
Definition: LinksUpdate.php:84
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:51
$wgPagePropLinkInvalidations
Page property link table invalidation lists.
static acquirePageLock(IDatabase $dbw, $pageId, $why='atomicity')
Acquire a session-level lock for performing link table updates for a page on a DB.
isRecursive()
Whether or not this LinksUpdate will also update pages which transclude the current page or otherwise...
getRemovedExternalLinks()
Fetch external links removed by this LinksUpdate.
getRemovedProperties()
Fetch page properties removed by this LinksUpdate.
getTitle()
Return the title object of the page being updated.
getScopedLockAndFlush( $lockKey, $fname, $timeout)
Acquire a named lock, flush any transaction, and return an RAII style unlocker object.
getImageInsertions( $existing=[])
Get an array of image insertions Skips the names specified in $existing.
getExistingCategories()
Get an array of existing categories, with the name in the key and sort key in the value...
array $mImages
DB keys of the images used, in the array key only.
Definition: LinksUpdate.php:54
getExistingExternals()
Get an array of existing external links, URLs in the keys.
getPropertyDeletions( $existing)
Get array of properties which should be deleted.
getParserOutput()
Returns parser output.
getCauseAction()
Definition: DataUpdate.php:60
getAddedLinks()
Fetch page links added by this LinksUpdate.
null array $propertyInsertions
Added properties if calculated.
static newForBacklinks(Title $title, $table, $params=[])
setTriggeringUser(User $user)
Set the User who triggered this LinksUpdate.
getRemovedLinks()
Fetch page links removed by this LinksUpdate.
const NS_CATEGORY
Definition: Defines.php:74
array $mExternals
URLs of external links, array key only.
Definition: LinksUpdate.php:60
getPagePropRowData( $prop)
Returns an associative array to be used for inserting a row into the page_props table.
getPropertyInsertions( $existing=[])
Get an array of page property insertions.
array $mTemplates
Map of title strings to IDs for the template references, including broken ones.
Definition: LinksUpdate.php:57
updateCategoryCounts(array $added, array $deleted)
Update all the appropriate counts in the category table.
invalidateCategories( $cats)
getExistingImages()
Get an array of existing images, image names in the keys.
const NS_FILE
Definition: Defines.php:66
getLinkInsertions( $existing=[])
Get an array of pagelinks insertions for passing to the DB Skips the titles specified by the 2-D arra...
getExistingTemplates()
Get an array of existing templates, as a 2-D array.
getExistingLinks()
Get an array of existing links, as a 2-D array.
Job to update link tables for pages.
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:610
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:584
User null $user
getAddedExternalLinks()
Fetch external links added by this LinksUpdate.
null array $propertyDeletions
Deleted properties if calculated.
static queueRecursiveJobsForTable(Title $title, $table, $action='unknown', $userName='unknown')
Queue a RefreshLinks job for any table.
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the deferred list to be run later by execute()
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
invalidateProperties( $changed)
Invalidate any necessary link lists related to page property changes.
getInterwikiInsertions( $existing=[])
Get an array of interwiki insertions for passing to the DB Skips the titles specified by the 2-D arra...
getAddedProperties()
Fetch page properties added by this LinksUpdate.
if(count( $args)< 1) $job
getTemplateDeletions( $existing)
Given an array of existing templates, returns those templates which are not in $this and thus should ...
$wgUpdateRowsPerQuery
Number of rows to update per query.
static newPrioritized(Title $title, array $params)
null array [] $externalLinkInsertions
Added external links if calculated.
Definition: LinksUpdate.php:94
static makeIndexes( $url)
Converts a URL into a format for el_index.
Definition: LinkFilter.php:171
getPropertySortKeyValue( $value)
Determines the sort key for the given property value.
getLinkDeletions( $existing)
Given an array of existing links, returns those links which are not in $this and thus should be delet...
doUpdate()
Update link tables with outgoing links from an updated article.
static singleton( $domain=false)
null array $linkDeletions
Deleted links if calculated.
Definition: LinksUpdate.php:89
int [][] $mLinks
Map of title strings to IDs for the links in the document -var array<int,array<string,int>>
Definition: LinksUpdate.php:51
getImages()
Return the list of images used as generated by the parser.
array $mInterlangs
Map of language codes to titles.
Definition: LinksUpdate.php:66
int $mId
Page ID of the article linked from.
Definition: LinksUpdate.php:39
array $mCategories
Map of category names to sort keys.
Definition: LinksUpdate.php:63
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
array $mInterwikis
2-D map of (prefix => DBK => 1)
Definition: LinksUpdate.php:69
IDatabase $db