00001 <?php
00007 class LinksUpdate {
00008
00012 var $mId,
00013 $mTitle,
00014 $mLinks,
00015 $mImages,
00016 $mTemplates,
00017 $mExternals,
00018 $mCategories,
00019 $mInterlangs,
00020 $mProperties,
00021 $mDb,
00022 $mOptions,
00023 $mRecursive;
00024
00033 function LinksUpdate( $title, $parserOutput, $recursive = true ) {
00034 global $wgAntiLockFlags;
00035
00036 if ( $wgAntiLockFlags & ALF_NO_LINK_LOCK ) {
00037 $this->mOptions = array();
00038 } else {
00039 $this->mOptions = array( 'FOR UPDATE' );
00040 }
00041 $this->mDb = wfGetDB( DB_MASTER );
00042
00043 if ( !is_object( $title ) ) {
00044 throw new MWException( "The calling convention to LinksUpdate::LinksUpdate() has changed. " .
00045 "Please see Article::editUpdates() for an invocation example.\n" );
00046 }
00047 $this->mTitle = $title;
00048 $this->mId = $title->getArticleID();
00049
00050 $this->mParserOutput = $parserOutput;
00051 $this->mLinks = $parserOutput->getLinks();
00052 $this->mImages = $parserOutput->getImages();
00053 $this->mTemplates = $parserOutput->getTemplates();
00054 $this->mExternals = $parserOutput->getExternalLinks();
00055 $this->mCategories = $parserOutput->getCategories();
00056 $this->mProperties = $parserOutput->getProperties();
00057
00058 # Convert the format of the interlanguage links
00059 # I didn't want to change it in the ParserOutput, because that array is passed all
00060 # the way back to the skin, so either a skin API break would be required, or an
00061 # inefficient back-conversion.
00062 $ill = $parserOutput->getLanguageLinks();
00063 $this->mInterlangs = array();
00064 foreach ( $ill as $link ) {
00065 list( $key, $title ) = explode( ':', $link, 2 );
00066 $this->mInterlangs[$key] = $title;
00067 }
00068
00069 $this->mRecursive = $recursive;
00070 $this->mTouchTmplLinks = false;
00071
00072 wfRunHooks( 'LinksUpdateConstructed', array( &$this ) );
00073 }
00074
00078 public function doUpdate() {
00079 global $wgUseDumbLinkUpdate;
00080
00081 wfRunHooks( 'LinksUpdate', array( &$this ) );
00082 if ( $wgUseDumbLinkUpdate ) {
00083 $this->doDumbUpdate();
00084 } else {
00085 $this->doIncrementalUpdate();
00086 }
00087 wfRunHooks( 'LinksUpdateComplete', array( &$this ) );
00088 }
00089
00090 protected function doIncrementalUpdate() {
00091 wfProfileIn( __METHOD__ );
00092
00093 # Page links
00094 $existing = $this->getExistingLinks();
00095 $this->incrTableUpdate( 'pagelinks', 'pl', $this->getLinkDeletions( $existing ),
00096 $this->getLinkInsertions( $existing ) );
00097
00098 # Image links
00099 $existing = $this->getExistingImages();
00100
00101 $imageDeletes = $this->getImageDeletions( $existing );
00102 $this->incrTableUpdate( 'imagelinks', 'il', $imageDeletes, $this->getImageInsertions( $existing ) );
00103
00104 # Invalidate all image description pages which had links added or removed
00105 $imageUpdates = $imageDeletes + array_diff_key( $this->mImages, $existing );
00106 $this->invalidateImageDescriptions( $imageUpdates );
00107
00108 # External links
00109 $existing = $this->getExistingExternals();
00110 $this->incrTableUpdate( 'externallinks', 'el', $this->getExternalDeletions( $existing ),
00111 $this->getExternalInsertions( $existing ) );
00112
00113 # Language links
00114 $existing = $this->getExistingInterlangs();
00115 $this->incrTableUpdate( 'langlinks', 'll', $this->getInterlangDeletions( $existing ),
00116 $this->getInterlangInsertions( $existing ) );
00117
00118 # Template links
00119 $existing = $this->getExistingTemplates();
00120 $this->incrTableUpdate( 'templatelinks', 'tl', $this->getTemplateDeletions( $existing ),
00121 $this->getTemplateInsertions( $existing ) );
00122
00123 # Category links
00124 $existing = $this->getExistingCategories();
00125
00126 $categoryDeletes = $this->getCategoryDeletions( $existing );
00127
00128 $this->incrTableUpdate( 'categorylinks', 'cl', $categoryDeletes, $this->getCategoryInsertions( $existing ) );
00129
00130 # Invalidate all categories which were added, deleted or changed (set symmetric difference)
00131 $categoryInserts = array_diff_assoc( $this->mCategories, $existing );
00132 $categoryUpdates = $categoryInserts + $categoryDeletes;
00133 $this->invalidateCategories( $categoryUpdates );
00134 $this->updateCategoryCounts( $categoryInserts, $categoryDeletes );
00135
00136 # Page properties
00137 $existing = $this->getExistingProperties();
00138
00139 $propertiesDeletes = $this->getPropertyDeletions( $existing );
00140
00141 $this->incrTableUpdate( 'page_props', 'pp', $propertiesDeletes, $this->getPropertyInsertions( $existing ) );
00142
00143 # Invalidate the necessary pages
00144 $changed = $propertiesDeletes + array_diff_assoc( $this->mProperties, $existing );
00145 $this->invalidateProperties( $changed );
00146
00147 # Refresh links of all pages including this page
00148 # This will be in a separate transaction
00149 if ( $this->mRecursive ) {
00150 $this->queueRecursiveJobs();
00151 }
00152
00153 wfProfileOut( __METHOD__ );
00154 }
00155
00161 protected function doDumbUpdate() {
00162 wfProfileIn( __METHOD__ );
00163
00164 # Refresh category pages and image description pages
00165 $existing = $this->getExistingCategories();
00166 $categoryInserts = array_diff_assoc( $this->mCategories, $existing );
00167 $categoryDeletes = array_diff_assoc( $existing, $this->mCategories );
00168 $categoryUpdates = $categoryInserts + $categoryDeletes;
00169 $existing = $this->getExistingImages();
00170 $imageUpdates = array_diff_key( $existing, $this->mImages ) + array_diff_key( $this->mImages, $existing );
00171
00172 $this->dumbTableUpdate( 'pagelinks', $this->getLinkInsertions(), 'pl_from' );
00173 $this->dumbTableUpdate( 'imagelinks', $this->getImageInsertions(), 'il_from' );
00174 $this->dumbTableUpdate( 'categorylinks', $this->getCategoryInsertions(), 'cl_from' );
00175 $this->dumbTableUpdate( 'templatelinks', $this->getTemplateInsertions(), 'tl_from' );
00176 $this->dumbTableUpdate( 'externallinks', $this->getExternalInsertions(), 'el_from' );
00177 $this->dumbTableUpdate( 'langlinks', $this->getInterlangInsertions(),'ll_from' );
00178 $this->dumbTableUpdate( 'page_props', $this->getPropertyInsertions(), 'pp_page' );
00179
00180 # Update the cache of all the category pages and image description
00181 # pages which were changed, and fix the category table count
00182 $this->invalidateCategories( $categoryUpdates );
00183 $this->updateCategoryCounts( $categoryInserts, $categoryDeletes );
00184 $this->invalidateImageDescriptions( $imageUpdates );
00185
00186 # Refresh links of all pages including this page
00187 # This will be in a separate transaction
00188 if ( $this->mRecursive ) {
00189 $this->queueRecursiveJobs();
00190 }
00191
00192 wfProfileOut( __METHOD__ );
00193 }
00194
00195 function queueRecursiveJobs() {
00196 global $wgUpdateRowsPerJob;
00197 wfProfileIn( __METHOD__ );
00198
00199 $cache = $this->mTitle->getBacklinkCache();
00200 $batches = $cache->partition( 'templatelinks', $wgUpdateRowsPerJob );
00201 if ( !$batches ) {
00202 wfProfileOut( __METHOD__ );
00203 return;
00204 }
00205 $jobs = array();
00206 foreach ( $batches as $batch ) {
00207 list( $start, $end ) = $batch;
00208 $params = array(
00209 'start' => $start,
00210 'end' => $end,
00211 );
00212 $jobs[] = new RefreshLinksJob2( $this->mTitle, $params );
00213 }
00214 Job::batchInsert( $jobs );
00215
00216 wfProfileOut( __METHOD__ );
00217 }
00218
00225 function invalidatePages( $namespace, $dbkeys ) {
00226 if ( !count( $dbkeys ) ) {
00227 return;
00228 }
00229
00235 $now = $this->mDb->timestamp();
00236 $ids = array();
00237 $res = $this->mDb->select( 'page', array( 'page_id' ),
00238 array(
00239 'page_namespace' => $namespace,
00240 'page_title IN (' . $this->mDb->makeList( $dbkeys ) . ')',
00241 'page_touched < ' . $this->mDb->addQuotes( $now )
00242 ), __METHOD__
00243 );
00244 while ( $row = $this->mDb->fetchObject( $res ) ) {
00245 $ids[] = $row->page_id;
00246 }
00247 if ( !count( $ids ) ) {
00248 return;
00249 }
00250
00256 $this->mDb->update( 'page', array( 'page_touched' => $now ),
00257 array(
00258 'page_id IN (' . $this->mDb->makeList( $ids ) . ')',
00259 'page_touched < ' . $this->mDb->addQuotes( $now )
00260 ), __METHOD__
00261 );
00262 }
00263
00264 function invalidateCategories( $cats ) {
00265 $this->invalidatePages( NS_CATEGORY, array_keys( $cats ) );
00266 }
00267
00273 function updateCategoryCounts( $added, $deleted ) {
00274 $a = new Article($this->mTitle);
00275 $a->updateCategoryCounts(
00276 array_keys( $added ), array_keys( $deleted )
00277 );
00278 }
00279
00280 function invalidateImageDescriptions( $images ) {
00281 $this->invalidatePages( NS_FILE, array_keys( $images ) );
00282 }
00283
00284 function dumbTableUpdate( $table, $insertions, $fromField ) {
00285 $this->mDb->delete( $table, array( $fromField => $this->mId ), __METHOD__ );
00286 if ( count( $insertions ) ) {
00287 # The link array was constructed without FOR UPDATE, so there may
00288 # be collisions. This may cause minor link table inconsistencies,
00289 # which is better than crippling the site with lock contention.
00290 $this->mDb->insert( $table, $insertions, __METHOD__, array( 'IGNORE' ) );
00291 }
00292 }
00293
00300 function makeWhereFrom2d( &$arr, $prefix ) {
00301 $lb = new LinkBatch;
00302 $lb->setArray( $arr );
00303 return $lb->constructSet( $prefix, $this->mDb );
00304 }
00305
00310 function incrTableUpdate( $table, $prefix, $deletions, $insertions ) {
00311 if ( $table == 'page_props' ) {
00312 $fromField = 'pp_page';
00313 } else {
00314 $fromField = "{$prefix}_from";
00315 }
00316 $where = array( $fromField => $this->mId );
00317 if ( $table == 'pagelinks' || $table == 'templatelinks' ) {
00318 $clause = $this->makeWhereFrom2d( $deletions, $prefix );
00319 if ( $clause ) {
00320 $where[] = $clause;
00321 } else {
00322 $where = false;
00323 }
00324 } else {
00325 if ( $table == 'langlinks' ) {
00326 $toField = 'll_lang';
00327 } elseif ( $table == 'page_props' ) {
00328 $toField = 'pp_propname';
00329 } else {
00330 $toField = $prefix . '_to';
00331 }
00332 if ( count( $deletions ) ) {
00333 $where[] = "$toField IN (" . $this->mDb->makeList( array_keys( $deletions ) ) . ')';
00334 } else {
00335 $where = false;
00336 }
00337 }
00338 if ( $where ) {
00339 $this->mDb->delete( $table, $where, __METHOD__ );
00340 }
00341 if ( count( $insertions ) ) {
00342 $this->mDb->insert( $table, $insertions, __METHOD__, 'IGNORE' );
00343 }
00344 }
00345
00346
00352 function getLinkInsertions( $existing = array() ) {
00353 $arr = array();
00354 foreach( $this->mLinks as $ns => $dbkeys ) {
00355 # array_diff_key() was introduced in PHP 5.1, there is a compatibility function
00356 # in GlobalFunctions.php
00357 $diffs = isset( $existing[$ns] ) ? array_diff_key( $dbkeys, $existing[$ns] ) : $dbkeys;
00358 foreach ( $diffs as $dbk => $id ) {
00359 $arr[] = array(
00360 'pl_from' => $this->mId,
00361 'pl_namespace' => $ns,
00362 'pl_title' => $dbk
00363 );
00364 }
00365 }
00366 return $arr;
00367 }
00368
00373 function getTemplateInsertions( $existing = array() ) {
00374 $arr = array();
00375 foreach( $this->mTemplates as $ns => $dbkeys ) {
00376 $diffs = isset( $existing[$ns] ) ? array_diff_key( $dbkeys, $existing[$ns] ) : $dbkeys;
00377 foreach ( $diffs as $dbk => $id ) {
00378 $arr[] = array(
00379 'tl_from' => $this->mId,
00380 'tl_namespace' => $ns,
00381 'tl_title' => $dbk
00382 );
00383 }
00384 }
00385 return $arr;
00386 }
00387
00393 function getImageInsertions( $existing = array() ) {
00394 $arr = array();
00395 $diffs = array_diff_key( $this->mImages, $existing );
00396 foreach( $diffs as $iname => $dummy ) {
00397 $arr[] = array(
00398 'il_from' => $this->mId,
00399 'il_to' => $iname
00400 );
00401 }
00402 return $arr;
00403 }
00404
00409 function getExternalInsertions( $existing = array() ) {
00410 $arr = array();
00411 $diffs = array_diff_key( $this->mExternals, $existing );
00412 foreach( $diffs as $url => $dummy ) {
00413 $arr[] = array(
00414 'el_from' => $this->mId,
00415 'el_to' => $url,
00416 'el_index' => wfMakeUrlIndex( $url ),
00417 );
00418 }
00419 return $arr;
00420 }
00421
00428 function getCategoryInsertions( $existing = array() ) {
00429 global $wgContLang;
00430 $diffs = array_diff_assoc( $this->mCategories, $existing );
00431 $arr = array();
00432 foreach ( $diffs as $name => $sortkey ) {
00433 $nt = Title::makeTitleSafe( NS_CATEGORY, $name );
00434 $wgContLang->findVariantLink( $name, $nt, true );
00435 $arr[] = array(
00436 'cl_from' => $this->mId,
00437 'cl_to' => $name,
00438 'cl_sortkey' => $sortkey,
00439 'cl_timestamp' => $this->mDb->timestamp()
00440 );
00441 }
00442 return $arr;
00443 }
00444
00450 function getInterlangInsertions( $existing = array() ) {
00451 $diffs = array_diff_assoc( $this->mInterlangs, $existing );
00452 $arr = array();
00453 foreach( $diffs as $lang => $title ) {
00454 $arr[] = array(
00455 'll_from' => $this->mId,
00456 'll_lang' => $lang,
00457 'll_title' => $title
00458 );
00459 }
00460 return $arr;
00461 }
00462
00466 function getPropertyInsertions( $existing = array() ) {
00467 $diffs = array_diff_assoc( $this->mProperties, $existing );
00468 $arr = array();
00469 foreach ( $diffs as $name => $value ) {
00470 $arr[] = array(
00471 'pp_page' => $this->mId,
00472 'pp_propname' => $name,
00473 'pp_value' => $value,
00474 );
00475 }
00476 return $arr;
00477 }
00478
00479
00485 function getLinkDeletions( $existing ) {
00486 $del = array();
00487 foreach ( $existing as $ns => $dbkeys ) {
00488 if ( isset( $this->mLinks[$ns] ) ) {
00489 $del[$ns] = array_diff_key( $existing[$ns], $this->mLinks[$ns] );
00490 } else {
00491 $del[$ns] = $existing[$ns];
00492 }
00493 }
00494 return $del;
00495 }
00496
00502 function getTemplateDeletions( $existing ) {
00503 $del = array();
00504 foreach ( $existing as $ns => $dbkeys ) {
00505 if ( isset( $this->mTemplates[$ns] ) ) {
00506 $del[$ns] = array_diff_key( $existing[$ns], $this->mTemplates[$ns] );
00507 } else {
00508 $del[$ns] = $existing[$ns];
00509 }
00510 }
00511 return $del;
00512 }
00513
00519 function getImageDeletions( $existing ) {
00520 return array_diff_key( $existing, $this->mImages );
00521 }
00522
00528 function getExternalDeletions( $existing ) {
00529 return array_diff_key( $existing, $this->mExternals );
00530 }
00531
00537 function getCategoryDeletions( $existing ) {
00538 return array_diff_assoc( $existing, $this->mCategories );
00539 }
00540
00546 function getInterlangDeletions( $existing ) {
00547 return array_diff_assoc( $existing, $this->mInterlangs );
00548 }
00549
00554 function getPropertyDeletions( $existing ) {
00555 return array_diff_assoc( $existing, $this->mProperties );
00556 }
00557
00562 function getExistingLinks() {
00563 $res = $this->mDb->select( 'pagelinks', array( 'pl_namespace', 'pl_title' ),
00564 array( 'pl_from' => $this->mId ), __METHOD__, $this->mOptions );
00565 $arr = array();
00566 while ( $row = $this->mDb->fetchObject( $res ) ) {
00567 if ( !isset( $arr[$row->pl_namespace] ) ) {
00568 $arr[$row->pl_namespace] = array();
00569 }
00570 $arr[$row->pl_namespace][$row->pl_title] = 1;
00571 }
00572 $this->mDb->freeResult( $res );
00573 return $arr;
00574 }
00575
00580 function getExistingTemplates() {
00581 $res = $this->mDb->select( 'templatelinks', array( 'tl_namespace', 'tl_title' ),
00582 array( 'tl_from' => $this->mId ), __METHOD__, $this->mOptions );
00583 $arr = array();
00584 while ( $row = $this->mDb->fetchObject( $res ) ) {
00585 if ( !isset( $arr[$row->tl_namespace] ) ) {
00586 $arr[$row->tl_namespace] = array();
00587 }
00588 $arr[$row->tl_namespace][$row->tl_title] = 1;
00589 }
00590 $this->mDb->freeResult( $res );
00591 return $arr;
00592 }
00593
00598 function getExistingImages() {
00599 $res = $this->mDb->select( 'imagelinks', array( 'il_to' ),
00600 array( 'il_from' => $this->mId ), __METHOD__, $this->mOptions );
00601 $arr = array();
00602 while ( $row = $this->mDb->fetchObject( $res ) ) {
00603 $arr[$row->il_to] = 1;
00604 }
00605 $this->mDb->freeResult( $res );
00606 return $arr;
00607 }
00608
00613 function getExistingExternals() {
00614 $res = $this->mDb->select( 'externallinks', array( 'el_to' ),
00615 array( 'el_from' => $this->mId ), __METHOD__, $this->mOptions );
00616 $arr = array();
00617 while ( $row = $this->mDb->fetchObject( $res ) ) {
00618 $arr[$row->el_to] = 1;
00619 }
00620 $this->mDb->freeResult( $res );
00621 return $arr;
00622 }
00623
00628 function getExistingCategories() {
00629 $res = $this->mDb->select( 'categorylinks', array( 'cl_to', 'cl_sortkey' ),
00630 array( 'cl_from' => $this->mId ), __METHOD__, $this->mOptions );
00631 $arr = array();
00632 while ( $row = $this->mDb->fetchObject( $res ) ) {
00633 $arr[$row->cl_to] = $row->cl_sortkey;
00634 }
00635 $this->mDb->freeResult( $res );
00636 return $arr;
00637 }
00638
00644 function getExistingInterlangs() {
00645 $res = $this->mDb->select( 'langlinks', array( 'll_lang', 'll_title' ),
00646 array( 'll_from' => $this->mId ), __METHOD__, $this->mOptions );
00647 $arr = array();
00648 while ( $row = $this->mDb->fetchObject( $res ) ) {
00649 $arr[$row->ll_lang] = $row->ll_title;
00650 }
00651 return $arr;
00652 }
00653
00658 function getExistingProperties() {
00659 $res = $this->mDb->select( 'page_props', array( 'pp_propname', 'pp_value' ),
00660 array( 'pp_page' => $this->mId ), __METHOD__, $this->mOptions );
00661 $arr = array();
00662 while ( $row = $this->mDb->fetchObject( $res ) ) {
00663 $arr[$row->pp_propname] = $row->pp_value;
00664 }
00665 $this->mDb->freeResult( $res );
00666 return $arr;
00667 }
00668
00669
00673 function getTitle() {
00674 return $this->mTitle;
00675 }
00676
00680 function invalidateProperties( $changed ) {
00681 global $wgPagePropLinkInvalidations;
00682
00683 foreach ( $changed as $name => $value ) {
00684 if ( isset( $wgPagePropLinkInvalidations[$name] ) ) {
00685 $inv = $wgPagePropLinkInvalidations[$name];
00686 if ( !is_array( $inv ) ) {
00687 $inv = array( $inv );
00688 }
00689 foreach ( $inv as $table ) {
00690 $update = new HTMLCacheUpdate( $this->mTitle, $table );
00691 $update->doUpdate();
00692 }
00693 }
00694 }
00695 }
00696 }