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 }