]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/LinksUpdate.php
MediaWiki 1.11.0
[autoinstallsdev/mediawiki.git] / includes / LinksUpdate.php
1 <?php
2 /**
3  * See docs/deferred.txt
4  * 
5  * @todo document (e.g. one-sentence top-level class description).
6  */
7 class LinksUpdate {
8
9         /**@{{
10          * @private
11          */
12         var $mId,            //!< Page ID of the article linked from
13                 $mTitle,         //!< Title object of the article linked from
14                 $mLinks,         //!< Map of title strings to IDs for the links in the document
15                 $mImages,        //!< DB keys of the images used, in the array key only
16                 $mTemplates,     //!< Map of title strings to IDs for the template references, including broken ones
17                 $mExternals,     //!< URLs of external links, array key only
18                 $mCategories,    //!< Map of category names to sort keys
19                 $mInterlangs,    //!< Map of language codes to titles
20                 $mDb,            //!< Database connection reference
21                 $mOptions,       //!< SELECT options to be used (array)
22                 $mRecursive;     //!< Whether to queue jobs for recursive updates
23         /**@}}*/
24
25         /**
26          * Constructor
27          *
28          * @param Title $title Title of the page we're updating
29          * @param ParserOutput $parserOutput Output from a full parse of this page
30          * @param bool $recursive Queue jobs for recursive updates?
31          */
32         function LinksUpdate( $title, $parserOutput, $recursive = true ) {
33                 global $wgAntiLockFlags;
34
35                 if ( $wgAntiLockFlags & ALF_NO_LINK_LOCK ) {
36                         $this->mOptions = array();
37                 } else {
38                         $this->mOptions = array( 'FOR UPDATE' );
39                 }
40                 $this->mDb = wfGetDB( DB_MASTER );
41
42                 if ( !is_object( $title ) ) {
43                         throw new MWException( "The calling convention to LinksUpdate::LinksUpdate() has changed. " .
44                                 "Please see Article::editUpdates() for an invocation example.\n" );
45                 }
46                 $this->mTitle = $title;
47                 $this->mId = $title->getArticleID();
48
49                 $this->mLinks = $parserOutput->getLinks();
50                 $this->mImages = $parserOutput->getImages();
51                 $this->mTemplates = $parserOutput->getTemplates();
52                 $this->mExternals = $parserOutput->getExternalLinks();
53                 $this->mCategories = $parserOutput->getCategories();
54
55                 # Convert the format of the interlanguage links
56                 # I didn't want to change it in the ParserOutput, because that array is passed all 
57                 # the way back to the skin, so either a skin API break would be required, or an 
58                 # inefficient back-conversion.
59                 $ill = $parserOutput->getLanguageLinks();
60                 $this->mInterlangs = array();
61                 foreach ( $ill as $link ) {
62                         list( $key, $title ) = explode( ':', $link, 2 );
63                         $this->mInterlangs[$key] = $title;
64                 }
65
66                 $this->mRecursive = $recursive;
67                 
68                 wfRunHooks( 'LinksUpdateConstructed', array( &$this ) );
69         }
70
71         /**
72          * Update link tables with outgoing links from an updated article
73          */
74         function doUpdate() {
75                 global $wgUseDumbLinkUpdate;
76                 if ( $wgUseDumbLinkUpdate ) {
77                         $this->doDumbUpdate();
78                 } else {
79                         $this->doIncrementalUpdate();
80                 }
81         }
82
83         function doIncrementalUpdate() {
84                 $fname = 'LinksUpdate::doIncrementalUpdate';
85                 wfProfileIn( $fname );
86                 
87                 # Page links
88                 $existing = $this->getExistingLinks();
89                 $this->incrTableUpdate( 'pagelinks', 'pl', $this->getLinkDeletions( $existing ),
90                         $this->getLinkInsertions( $existing ) );
91
92                 # Image links
93                 $existing = $this->getExistingImages();
94                 $this->incrTableUpdate( 'imagelinks', 'il', $this->getImageDeletions( $existing ),
95                         $this->getImageInsertions( $existing ) );
96
97                 # Invalidate all image description pages which had links added or removed
98                 $imageUpdates = array_diff_key( $existing, $this->mImages ) + array_diff_key( $this->mImages, $existing );
99                 $this->invalidateImageDescriptions( $imageUpdates );
100
101                 # External links
102                 $existing = $this->getExistingExternals();
103                 $this->incrTableUpdate( 'externallinks', 'el', $this->getExternalDeletions( $existing ),
104                 $this->getExternalInsertions( $existing ) );
105
106                 # Language links
107                 $existing = $this->getExistingInterlangs();
108                 $this->incrTableUpdate( 'langlinks', 'll', $this->getInterlangDeletions( $existing ),
109                         $this->getInterlangInsertions( $existing ) );
110
111                 # Template links
112                 $existing = $this->getExistingTemplates();
113                 $this->incrTableUpdate( 'templatelinks', 'tl', $this->getTemplateDeletions( $existing ),
114                         $this->getTemplateInsertions( $existing ) );
115
116                 # Category links
117                 $existing = $this->getExistingCategories();
118                 $this->incrTableUpdate( 'categorylinks', 'cl', $this->getCategoryDeletions( $existing ),
119                         $this->getCategoryInsertions( $existing ) );
120
121                 # Invalidate all categories which were added, deleted or changed (set symmetric difference)
122                 $categoryUpdates = array_diff_assoc( $existing, $this->mCategories ) + array_diff_assoc( $this->mCategories, $existing );
123                 $this->invalidateCategories( $categoryUpdates );
124
125                 # Refresh links of all pages including this page
126                 # This will be in a separate transaction
127                 if ( $this->mRecursive ) {
128                         $this->queueRecursiveJobs();
129                 }
130                 
131                 wfProfileOut( $fname );
132         }
133
134         /**
135          * Link update which clears the previous entries and inserts new ones
136          * May be slower or faster depending on level of lock contention and write speed of DB
137          * Also useful where link table corruption needs to be repaired, e.g. in refreshLinks.php
138          */
139         function doDumbUpdate() {
140                 $fname = 'LinksUpdate::doDumbUpdate';
141                 wfProfileIn( $fname );
142
143                 # Refresh category pages and image description pages
144                 $existing = $this->getExistingCategories();
145                 $categoryUpdates = array_diff_assoc( $existing, $this->mCategories ) + array_diff_assoc( $this->mCategories, $existing );
146                 $existing = $this->getExistingImages();
147                 $imageUpdates = array_diff_key( $existing, $this->mImages ) + array_diff_key( $this->mImages, $existing );
148
149                 $this->dumbTableUpdate( 'pagelinks',     $this->getLinkInsertions(),     'pl_from' );
150                 $this->dumbTableUpdate( 'imagelinks',    $this->getImageInsertions(),    'il_from' );
151                 $this->dumbTableUpdate( 'categorylinks', $this->getCategoryInsertions(), 'cl_from' );
152                 $this->dumbTableUpdate( 'templatelinks', $this->getTemplateInsertions(), 'tl_from' );
153                 $this->dumbTableUpdate( 'externallinks', $this->getExternalInsertions(), 'el_from' );
154                 $this->dumbTableUpdate( 'langlinks',     $this->getInterlangInsertions(), 'll_from' );
155
156                 # Update the cache of all the category pages and image description pages which were changed
157                 $this->invalidateCategories( $categoryUpdates );
158                 $this->invalidateImageDescriptions( $imageUpdates );
159
160                 # Refresh links of all pages including this page
161                 # This will be in a separate transaction
162                 if ( $this->mRecursive ) {
163                         $this->queueRecursiveJobs();
164                 }
165
166                 wfProfileOut( $fname );
167         }
168
169         function queueRecursiveJobs() {
170                 wfProfileIn( __METHOD__ );
171                 
172                 $batchSize = 100;
173                 $dbr = wfGetDB( DB_SLAVE );
174                 $res = $dbr->select( array( 'templatelinks', 'page' ), 
175                         array( 'page_namespace', 'page_title' ),
176                         array( 
177                                 'page_id=tl_from', 
178                                 'tl_namespace' => $this->mTitle->getNamespace(),
179                                 'tl_title' => $this->mTitle->getDBkey()
180                         ), __METHOD__
181                 );
182
183                 $done = false;
184                 while ( !$done ) {
185                         $jobs = array();
186                         for ( $i = 0; $i < $batchSize; $i++ ) {
187                                 $row = $dbr->fetchObject( $res );
188                                 if ( !$row ) {
189                                         $done = true;
190                                         break;
191                                 }
192                                 $title = Title::makeTitle( $row->page_namespace, $row->page_title );
193                                 $jobs[] = new RefreshLinksJob( $title, '' );
194                         }
195                         Job::batchInsert( $jobs );
196                 }
197                 $dbr->freeResult( $res );
198                 wfProfileOut( __METHOD__ );
199         }
200         
201         /**
202          * Invalidate the cache of a list of pages from a single namespace
203          *
204          * @param integer $namespace
205          * @param array $dbkeys
206          */
207         function invalidatePages( $namespace, $dbkeys ) {
208                 $fname = 'LinksUpdate::invalidatePages';
209                 
210                 if ( !count( $dbkeys ) ) {
211                         return;
212                 }
213                 
214                 /**
215                  * Determine which pages need to be updated
216                  * This is necessary to prevent the job queue from smashing the DB with
217                  * large numbers of concurrent invalidations of the same page
218                  */
219                 $now = $this->mDb->timestamp();
220                 $ids = array();
221                 $res = $this->mDb->select( 'page', array( 'page_id' ), 
222                         array( 
223                                 'page_namespace' => $namespace,
224                                 'page_title IN (' . $this->mDb->makeList( $dbkeys ) . ')',
225                                 'page_touched < ' . $this->mDb->addQuotes( $now )
226                         ), $fname
227                 );
228                 while ( $row = $this->mDb->fetchObject( $res ) ) {
229                         $ids[] = $row->page_id;
230                 }
231                 if ( !count( $ids ) ) {
232                         return;
233                 }
234                 
235                 /**
236                  * Do the update
237                  * We still need the page_touched condition, in case the row has changed since 
238                  * the non-locking select above.
239                  */
240                 $this->mDb->update( 'page', array( 'page_touched' => $now ), 
241                         array( 
242                                 'page_id IN (' . $this->mDb->makeList( $ids ) . ')',
243                                 'page_touched < ' . $this->mDb->addQuotes( $now )
244                         ), $fname
245                 );
246         }
247
248         function invalidateCategories( $cats ) {
249                 $this->invalidatePages( NS_CATEGORY, array_keys( $cats ) );
250         }
251
252         function invalidateImageDescriptions( $images ) {
253                 $this->invalidatePages( NS_IMAGE, array_keys( $images ) );
254         }
255
256         function dumbTableUpdate( $table, $insertions, $fromField ) {
257                 $fname = 'LinksUpdate::dumbTableUpdate';
258                 $this->mDb->delete( $table, array( $fromField => $this->mId ), $fname );
259                 if ( count( $insertions ) ) {
260                         # The link array was constructed without FOR UPDATE, so there may be collisions
261                         # This may cause minor link table inconsistencies, which is better than
262                         # crippling the site with lock contention.
263                         $this->mDb->insert( $table, $insertions, $fname, array( 'IGNORE' ) );
264                 }
265         }
266
267         /**
268          * Make a WHERE clause from a 2-d NS/dbkey array
269          *
270          * @param array $arr 2-d array indexed by namespace and DB key
271          * @param string $prefix Field name prefix, without the underscore
272          */
273         function makeWhereFrom2d( &$arr, $prefix ) {
274                 $lb = new LinkBatch;
275                 $lb->setArray( $arr );
276                 return $lb->constructSet( $prefix, $this->mDb );
277         }
278
279         /**
280          * Update a table by doing a delete query then an insert query
281          * @private
282          */
283         function incrTableUpdate( $table, $prefix, $deletions, $insertions ) {
284                 $fname = 'LinksUpdate::incrTableUpdate';
285                 $where = array( "{$prefix}_from" => $this->mId );
286                 if ( $table == 'pagelinks' || $table == 'templatelinks' ) {
287                         $clause = $this->makeWhereFrom2d( $deletions, $prefix );
288                         if ( $clause ) {
289                                 $where[] = $clause;
290                         } else {
291                                 $where = false;
292                         }
293                 } else {
294                         if ( $table == 'langlinks' ) {
295                                 $toField = 'll_lang';
296                         } else {
297                                 $toField = $prefix . '_to';
298                         }
299                         if ( count( $deletions ) ) {
300                                 $where[] = "$toField IN (" . $this->mDb->makeList( array_keys( $deletions ) ) . ')';
301                         } else {
302                                 $where = false;
303                         }
304                 }
305                 if ( $where ) {
306                         $this->mDb->delete( $table, $where, $fname );
307                 }
308                 if ( count( $insertions ) ) {
309                         $this->mDb->insert( $table, $insertions, $fname, 'IGNORE' );
310                 }
311         }
312
313
314         /**
315          * Get an array of pagelinks insertions for passing to the DB
316          * Skips the titles specified by the 2-D array $existing
317          * @private
318          */
319         function getLinkInsertions( $existing = array() ) {
320                 $arr = array();
321                 foreach( $this->mLinks as $ns => $dbkeys ) {
322                         # array_diff_key() was introduced in PHP 5.1, there is a compatibility function
323                         # in GlobalFunctions.php
324                         $diffs = isset( $existing[$ns] ) ? array_diff_key( $dbkeys, $existing[$ns] ) : $dbkeys;
325                         foreach ( $diffs as $dbk => $id ) {
326                                 $arr[] = array(
327                                         'pl_from'      => $this->mId,
328                                         'pl_namespace' => $ns,
329                                         'pl_title'     => $dbk
330                                 );
331                         }
332                 }
333                 return $arr;
334         }
335
336         /**
337          * Get an array of template insertions. Like getLinkInsertions()
338          * @private
339          */
340         function getTemplateInsertions( $existing = array() ) {
341                 $arr = array();
342                 foreach( $this->mTemplates as $ns => $dbkeys ) {
343                         $diffs = isset( $existing[$ns] ) ? array_diff_key( $dbkeys, $existing[$ns] ) : $dbkeys;
344                         foreach ( $diffs as $dbk => $id ) {
345                                 $arr[] = array(
346                                         'tl_from'      => $this->mId,
347                                         'tl_namespace' => $ns,
348                                         'tl_title'     => $dbk
349                                 );
350                         }
351                 }
352                 return $arr;
353         }
354
355         /**
356          * Get an array of image insertions
357          * Skips the names specified in $existing
358          * @private
359          */
360         function getImageInsertions( $existing = array() ) {
361                 $arr = array();
362                 $diffs = array_diff_key( $this->mImages, $existing );
363                 foreach( $diffs as $iname => $dummy ) {
364                         $arr[] = array(
365                                 'il_from' => $this->mId,
366                                 'il_to'   => $iname
367                         );
368                 }
369                 return $arr;
370         }
371
372         /**
373          * Get an array of externallinks insertions. Skips the names specified in $existing
374          * @private
375          */
376         function getExternalInsertions( $existing = array() ) {
377                 $arr = array();
378                 $diffs = array_diff_key( $this->mExternals, $existing );
379                 foreach( $diffs as $url => $dummy ) {
380                         $arr[] = array(
381                                 'el_from'   => $this->mId,
382                                 'el_to'     => $url,
383                                 'el_index'  => wfMakeUrlIndex( $url ),
384                         );
385                 }
386                 return $arr;
387         }
388
389         /**
390          * Get an array of category insertions
391          * @param array $existing Array mapping existing category names to sort keys. If both
392          * match a link in $this, the link will be omitted from the output
393          * @private
394          */
395         function getCategoryInsertions( $existing = array() ) {
396                 $diffs = array_diff_assoc( $this->mCategories, $existing );
397                 $arr = array();
398                 foreach ( $diffs as $name => $sortkey ) {
399                         $arr[] = array(
400                                 'cl_from'    => $this->mId,
401                                 'cl_to'      => $name,
402                                 'cl_sortkey' => $sortkey,
403                                 'cl_timestamp' => $this->mDb->timestamp()
404                         );
405                 }
406                 return $arr;
407         }
408
409         /**
410          * Get an array of interlanguage link insertions
411          * @param array $existing Array mapping existing language codes to titles        
412          * @private
413          */
414         function getInterlangInsertions( $existing = array() ) {
415             $diffs = array_diff_assoc( $this->mInterlangs, $existing );
416             $arr = array();
417             foreach( $diffs as $lang => $title ) {
418                 $arr[] = array(
419                     'll_from'  => $this->mId,
420                     'll_lang'  => $lang,
421                     'll_title' => $title
422                 );
423             }
424             return $arr;
425         }
426
427         /**
428          * Given an array of existing links, returns those links which are not in $this
429          * and thus should be deleted.
430          * @private
431          */
432         function getLinkDeletions( $existing ) {
433                 $del = array();
434                 foreach ( $existing as $ns => $dbkeys ) {
435                         if ( isset( $this->mLinks[$ns] ) ) {
436                                 $del[$ns] = array_diff_key( $existing[$ns], $this->mLinks[$ns] );
437                         } else {
438                                 $del[$ns] = $existing[$ns];
439                         }
440                 }
441                 return $del;
442         }
443
444         /**
445          * Given an array of existing templates, returns those templates which are not in $this
446          * and thus should be deleted.
447          * @private
448          */
449         function getTemplateDeletions( $existing ) {
450                 $del = array();
451                 foreach ( $existing as $ns => $dbkeys ) {
452                         if ( isset( $this->mTemplates[$ns] ) ) {
453                                 $del[$ns] = array_diff_key( $existing[$ns], $this->mTemplates[$ns] );
454                         } else {
455                                 $del[$ns] = $existing[$ns];
456                         }
457                 }
458                 return $del;
459         }
460
461         /**
462          * Given an array of existing images, returns those images which are not in $this
463          * and thus should be deleted.
464          * @private
465          */
466         function getImageDeletions( $existing ) {
467                 return array_diff_key( $existing, $this->mImages );
468         }
469
470         /** 
471          * Given an array of existing external links, returns those links which are not
472          * in $this and thus should be deleted.
473          * @private
474          */
475         function getExternalDeletions( $existing ) {
476                 return array_diff_key( $existing, $this->mExternals );
477         }
478
479         /**
480          * Given an array of existing categories, returns those categories which are not in $this
481          * and thus should be deleted.
482          * @private
483          */
484         function getCategoryDeletions( $existing ) {
485                 return array_diff_assoc( $existing, $this->mCategories );
486         }
487
488         /** 
489          * Given an array of existing interlanguage links, returns those links which are not
490          * in $this and thus should be deleted.
491          * @private
492          */
493         function getInterlangDeletions( $existing ) {
494             return array_diff_assoc( $existing, $this->mInterlangs );
495         }
496
497         /**
498          * Get an array of existing links, as a 2-D array
499          * @private
500          */
501         function getExistingLinks() {
502                 $fname = 'LinksUpdate::getExistingLinks';
503                 $res = $this->mDb->select( 'pagelinks', array( 'pl_namespace', 'pl_title' ),
504                         array( 'pl_from' => $this->mId ), $fname, $this->mOptions );
505                 $arr = array();
506                 while ( $row = $this->mDb->fetchObject( $res ) ) {
507                         if ( !isset( $arr[$row->pl_namespace] ) ) {
508                                 $arr[$row->pl_namespace] = array();
509                         }
510                         $arr[$row->pl_namespace][$row->pl_title] = 1;
511                 }
512                 $this->mDb->freeResult( $res );
513                 return $arr;
514         }
515
516         /**
517          * Get an array of existing templates, as a 2-D array
518          * @private
519          */
520         function getExistingTemplates() {
521                 $fname = 'LinksUpdate::getExistingTemplates';
522                 $res = $this->mDb->select( 'templatelinks', array( 'tl_namespace', 'tl_title' ),
523                         array( 'tl_from' => $this->mId ), $fname, $this->mOptions );
524                 $arr = array();
525                 while ( $row = $this->mDb->fetchObject( $res ) ) {
526                         if ( !isset( $arr[$row->tl_namespace] ) ) {
527                                 $arr[$row->tl_namespace] = array();
528                         }
529                         $arr[$row->tl_namespace][$row->tl_title] = 1;
530                 }
531                 $this->mDb->freeResult( $res );
532                 return $arr;
533         }
534
535         /**
536          * Get an array of existing images, image names in the keys
537          * @private
538          */
539         function getExistingImages() {
540                 $fname = 'LinksUpdate::getExistingImages';
541                 $res = $this->mDb->select( 'imagelinks', array( 'il_to' ),
542                         array( 'il_from' => $this->mId ), $fname, $this->mOptions );
543                 $arr = array();
544                 while ( $row = $this->mDb->fetchObject( $res ) ) {
545                         $arr[$row->il_to] = 1;
546                 }
547                 $this->mDb->freeResult( $res );
548                 return $arr;
549         }
550
551         /**
552          * Get an array of existing external links, URLs in the keys
553          * @private
554          */
555         function getExistingExternals() {
556                 $fname = 'LinksUpdate::getExistingExternals';
557                 $res = $this->mDb->select( 'externallinks', array( 'el_to' ),
558                         array( 'el_from' => $this->mId ), $fname, $this->mOptions );
559                 $arr = array();
560                 while ( $row = $this->mDb->fetchObject( $res ) ) {
561                         $arr[$row->el_to] = 1;
562                 }
563                 $this->mDb->freeResult( $res );
564                 return $arr;
565         }
566
567         /**
568          * Get an array of existing categories, with the name in the key and sort key in the value.
569          * @private
570          */
571         function getExistingCategories() {
572                 $fname = 'LinksUpdate::getExistingCategories';
573                 $res = $this->mDb->select( 'categorylinks', array( 'cl_to', 'cl_sortkey' ),
574                         array( 'cl_from' => $this->mId ), $fname, $this->mOptions );
575                 $arr = array();
576                 while ( $row = $this->mDb->fetchObject( $res ) ) {
577                         $arr[$row->cl_to] = $row->cl_sortkey;
578                 }
579                 $this->mDb->freeResult( $res );
580                 return $arr;
581         }
582
583         /**
584          * Get an array of existing interlanguage links, with the language code in the key and the 
585          * title in the value.
586          * @private
587          */
588         function getExistingInterlangs() {
589                 $fname = 'LinksUpdate::getExistingInterlangs';
590                 $res = $this->mDb->select( 'langlinks', array( 'll_lang', 'll_title' ), 
591                         array( 'll_from' => $this->mId ), $fname, $this->mOptions );
592                 $arr = array();
593                 while ( $row = $this->mDb->fetchObject( $res ) ) {
594                         $arr[$row->ll_lang] = $row->ll_title;
595                 }
596                 return $arr;
597         }
598 }
599