(bug 13588) Experimentally track link changes if $wgTrackLinkChanges is set to true...
[lhc/web/wiklou.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 $mExistingLinks,
16 $mImages, //!< DB keys of the images used, in the array key only
17 $mExistingImages,
18 $mTemplates, //!< Map of title strings to IDs for the template references, including broken ones
19 $mExistingTemplates,
20 $mExternals, //!< URLs of external links, array key only
21 $mExistingExternals,
22 $mCategories, //!< Map of category names to sort keys
23 $mExistingCategories,
24 $mInterlangs, //!< Map of language codes to titles
25 $mExistingInterlangs,
26 $mProperties, //!< Map of arbitrary name to value
27 $mExistingProperties,
28 $mDb, //!< Database connection reference
29 $mOptions, //!< SELECT options to be used (array)
30 $mRecursive; //!< Whether to queue jobs for recursive updates
31 /**@}}*/
32
33 /**
34 * Constructor
35 *
36 * @param Title $title Title of the page we're updating
37 * @param ParserOutput $parserOutput Output from a full parse of this page
38 * @param bool $recursive Queue jobs for recursive updates?
39 */
40 function LinksUpdate( $title, $parserOutput, $recursive = true ) {
41 global $wgAntiLockFlags;
42
43 if ( $wgAntiLockFlags & ALF_NO_LINK_LOCK ) {
44 $this->mOptions = array();
45 } else {
46 $this->mOptions = array( 'FOR UPDATE' );
47 }
48 $this->mDb = wfGetDB( DB_MASTER );
49
50 if ( !is_object( $title ) ) {
51 throw new MWException( "The calling convention to LinksUpdate::LinksUpdate() has changed. " .
52 "Please see Article::editUpdates() for an invocation example.\n" );
53 }
54 $this->mTitle = $title;
55 $this->mId = $title->getArticleID();
56
57 $this->mParserOutput = $parserOutput;
58 $this->mLinks = $parserOutput->getLinks();
59 $this->mImages = $parserOutput->getImages();
60 $this->mTemplates = $parserOutput->getTemplates();
61 $this->mExternals = $parserOutput->getExternalLinks();
62 $this->mCategories = $parserOutput->getCategories();
63 $this->mProperties = $parserOutput->getProperties();
64
65 # Convert the format of the interlanguage links
66 # I didn't want to change it in the ParserOutput, because that array is passed all
67 # the way back to the skin, so either a skin API break would be required, or an
68 # inefficient back-conversion.
69 $ill = $parserOutput->getLanguageLinks();
70 $this->mInterlangs = array();
71 foreach ( $ill as $link ) {
72 list( $key, $title ) = explode( ':', $link, 2 );
73 $this->mInterlangs[$key] = $title;
74 }
75
76 $this->mRecursive = $recursive;
77
78 wfRunHooks( 'LinksUpdateConstructed', array( &$this ) );
79 }
80
81 /**
82 * Update link tables with outgoing links from an updated article
83 */
84 function doUpdate() {
85 global $wgUseDumbLinkUpdate, $wgTrackLinkChanges;
86
87 wfRunHooks( 'LinksUpdate', array( &$this ) );
88 if ( $wgUseDumbLinkUpdate ) {
89 $this->doDumbUpdate();
90 } else {
91 $this->doIncrementalUpdate();
92 }
93 if ( $wgTrackLinkChanges )
94 $this->makeRecentlinkchanges();
95 wfRunHooks( 'LinksUpdateComplete', array( &$this ) );
96
97 }
98
99 function doIncrementalUpdate() {
100 wfProfileIn( __METHOD__ );
101
102 # Page links
103 $existing = $this->getExistingLinks();
104 $this->incrTableUpdate( 'pagelinks', 'pl', $this->getLinkDeletions( $existing ),
105 $this->getLinkInsertions( $existing ) );
106
107 # Image links
108 $existing = $this->getExistingImages();
109
110 $imageDeletes = $this->getImageDeletions( $existing );
111 $this->incrTableUpdate( 'imagelinks', 'il', $imageDeletes, $this->getImageInsertions( $existing ) );
112
113 # Invalidate all image description pages which had links added or removed
114 $imageUpdates = $imageDeletes + array_diff_key( $this->mImages, $existing );
115 $this->invalidateImageDescriptions( $imageUpdates );
116
117 # External links
118 $existing = $this->getExistingExternals();
119 $this->incrTableUpdate( 'externallinks', 'el', $this->getExternalDeletions( $existing ),
120 $this->getExternalInsertions( $existing ) );
121
122 # Language links
123 $existing = $this->getExistingInterlangs();
124 $this->incrTableUpdate( 'langlinks', 'll', $this->getInterlangDeletions( $existing ),
125 $this->getInterlangInsertions( $existing ) );
126
127 # Template links
128 $existing = $this->getExistingTemplates();
129 $this->incrTableUpdate( 'templatelinks', 'tl', $this->getTemplateDeletions( $existing ),
130 $this->getTemplateInsertions( $existing ) );
131
132 # Category links
133 $existing = $this->getExistingCategories();
134
135 $categoryDeletes = $this->getCategoryDeletions( $existing );
136
137 $this->incrTableUpdate( 'categorylinks', 'cl', $categoryDeletes, $this->getCategoryInsertions( $existing ) );
138
139 # Invalidate all categories which were added, deleted or changed (set symmetric difference)
140 $categoryInserts = array_diff_assoc( $this->mCategories, $existing );
141 $categoryUpdates = $categoryInserts + $categoryDeletes;
142 $this->invalidateCategories( $categoryUpdates );
143 $this->updateCategoryCounts( $categoryInserts, $categoryDeletes );
144
145 # Page properties
146 $existing = $this->getExistingProperties();
147
148 $propertiesDeletes = $this->getPropertyDeletions( $existing );
149
150 $this->incrTableUpdate( 'page_props', 'pp', $propertiesDeletes, $this->getPropertyInsertions( $existing ) );
151
152 # Invalidate the necessary pages
153 $changed = $propertiesDeletes + array_diff_assoc( $this->mProperties, $existing );
154 $this->invalidateProperties( $changed );
155
156 # Refresh links of all pages including this page
157 # This will be in a separate transaction
158 if ( $this->mRecursive ) {
159 $this->queueRecursiveJobs();
160 }
161
162 wfProfileOut( __METHOD__ );
163 }
164
165 /**
166 * Link update which clears the previous entries and inserts new ones
167 * May be slower or faster depending on level of lock contention and write speed of DB
168 * Also useful where link table corruption needs to be repaired, e.g. in refreshLinks.php
169 */
170 function doDumbUpdate() {
171 wfProfileIn( __METHOD__ );
172
173 # Refresh category pages and image description pages
174 $existing = $this->getExistingCategories();
175 $categoryInserts = array_diff_assoc( $this->mCategories, $existing );
176 $categoryDeletes = array_diff_assoc( $existing, $this->mCategories );
177 $categoryUpdates = $categoryInserts + $categoryDeletes;
178 $existing = $this->getExistingImages();
179 $imageUpdates = array_diff_key( $existing, $this->mImages ) + array_diff_key( $this->mImages, $existing );
180
181 $this->dumbTableUpdate( 'pagelinks', $this->getLinkInsertions(), 'pl_from' );
182 $this->dumbTableUpdate( 'imagelinks', $this->getImageInsertions(), 'il_from' );
183 $this->dumbTableUpdate( 'categorylinks', $this->getCategoryInsertions(), 'cl_from' );
184 $this->dumbTableUpdate( 'templatelinks', $this->getTemplateInsertions(), 'tl_from' );
185 $this->dumbTableUpdate( 'externallinks', $this->getExternalInsertions(), 'el_from' );
186 $this->dumbTableUpdate( 'langlinks', $this->getInterlangInsertions(),'ll_from' );
187 $this->dumbTableUpdate( 'page_props', $this->getPropertyInsertions(), 'pp_page' );
188
189 # Update the cache of all the category pages and image description
190 # pages which were changed, and fix the category table count
191 $this->invalidateCategories( $categoryUpdates );
192 $this->updateCategoryCounts( $categoryInserts, $categoryDeletes );
193 $this->invalidateImageDescriptions( $imageUpdates );
194
195 # Refresh links of all pages including this page
196 # This will be in a separate transaction
197 if ( $this->mRecursive ) {
198 $this->queueRecursiveJobs();
199 }
200
201 wfProfileOut( __METHOD__ );
202 }
203
204 function queueRecursiveJobs() {
205 wfProfileIn( __METHOD__ );
206
207 $batchSize = 100;
208 $dbr = wfGetDB( DB_SLAVE );
209 $res = $dbr->select( array( 'templatelinks', 'page' ),
210 array( 'page_namespace', 'page_title' ),
211 array(
212 'page_id=tl_from',
213 'tl_namespace' => $this->mTitle->getNamespace(),
214 'tl_title' => $this->mTitle->getDBkey()
215 ), __METHOD__
216 );
217
218 $done = false;
219 while ( !$done ) {
220 $jobs = array();
221 for ( $i = 0; $i < $batchSize; $i++ ) {
222 $row = $dbr->fetchObject( $res );
223 if ( !$row ) {
224 $done = true;
225 break;
226 }
227 $title = Title::makeTitle( $row->page_namespace, $row->page_title );
228 $jobs[] = new RefreshLinksJob( $title, '' );
229 }
230 Job::batchInsert( $jobs );
231 }
232 $dbr->freeResult( $res );
233 wfProfileOut( __METHOD__ );
234 }
235
236 /**
237 * Invalidate the cache of a list of pages from a single namespace
238 *
239 * @param integer $namespace
240 * @param array $dbkeys
241 */
242 function invalidatePages( $namespace, $dbkeys ) {
243 if ( !count( $dbkeys ) ) {
244 return;
245 }
246
247 /**
248 * Determine which pages need to be updated
249 * This is necessary to prevent the job queue from smashing the DB with
250 * large numbers of concurrent invalidations of the same page
251 */
252 $now = $this->mDb->timestamp();
253 $ids = array();
254 $res = $this->mDb->select( 'page', array( 'page_id' ),
255 array(
256 'page_namespace' => $namespace,
257 'page_title IN (' . $this->mDb->makeList( $dbkeys ) . ')',
258 'page_touched < ' . $this->mDb->addQuotes( $now )
259 ), __METHOD__
260 );
261 while ( $row = $this->mDb->fetchObject( $res ) ) {
262 $ids[] = $row->page_id;
263 }
264 if ( !count( $ids ) ) {
265 return;
266 }
267
268 /**
269 * Do the update
270 * We still need the page_touched condition, in case the row has changed since
271 * the non-locking select above.
272 */
273 $this->mDb->update( 'page', array( 'page_touched' => $now ),
274 array(
275 'page_id IN (' . $this->mDb->makeList( $ids ) . ')',
276 'page_touched < ' . $this->mDb->addQuotes( $now )
277 ), __METHOD__
278 );
279 }
280
281 function invalidateCategories( $cats ) {
282 $this->invalidatePages( NS_CATEGORY, array_keys( $cats ) );
283 }
284
285 /**
286 * Update all the appropriate counts in the category table.
287 * @param $added associative array of category name => sort key
288 * @param $deleted associative array of category name => sort key
289 */
290 function updateCategoryCounts( $added, $deleted ) {
291 $a = new Article($this->mTitle);
292 $a->updateCategoryCounts(
293 array_keys( $added ), array_keys( $deleted )
294 );
295 }
296
297 function invalidateImageDescriptions( $images ) {
298 $this->invalidatePages( NS_IMAGE, array_keys( $images ) );
299 }
300
301 function dumbTableUpdate( $table, $insertions, $fromField ) {
302 $this->mDb->delete( $table, array( $fromField => $this->mId ), __METHOD__ );
303 if ( count( $insertions ) ) {
304 # The link array was constructed without FOR UPDATE, so there may
305 # be collisions. This may cause minor link table inconsistencies,
306 # which is better than crippling the site with lock contention.
307 $this->mDb->insert( $table, $insertions, __METHOD__, array( 'IGNORE' ) );
308 }
309 }
310
311 /**
312 * Make a WHERE clause from a 2-d NS/dbkey array
313 *
314 * @param array $arr 2-d array indexed by namespace and DB key
315 * @param string $prefix Field name prefix, without the underscore
316 */
317 function makeWhereFrom2d( &$arr, $prefix ) {
318 $lb = new LinkBatch;
319 $lb->setArray( $arr );
320 return $lb->constructSet( $prefix, $this->mDb );
321 }
322
323 /**
324 * Update a table by doing a delete query then an insert query
325 * @private
326 */
327 function incrTableUpdate( $table, $prefix, $deletions, $insertions ) {
328 if ( $table == 'page_props' ) {
329 $fromField = 'pp_page';
330 } else {
331 $fromField = "{$prefix}_from";
332 }
333 $where = array( $fromField => $this->mId );
334 if ( $table == 'pagelinks' || $table == 'templatelinks' ) {
335 $clause = $this->makeWhereFrom2d( $deletions, $prefix );
336 if ( $clause ) {
337 $where[] = $clause;
338 } else {
339 $where = false;
340 }
341 } else {
342 if ( $table == 'langlinks' ) {
343 $toField = 'll_lang';
344 } elseif ( $table == 'page_props' ) {
345 $toField = 'pp_propname';
346 } else {
347 $toField = $prefix . '_to';
348 }
349 if ( count( $deletions ) ) {
350 $where[] = "$toField IN (" . $this->mDb->makeList( array_keys( $deletions ) ) . ')';
351 } else {
352 $where = false;
353 }
354 }
355 if ( $where ) {
356 $this->mDb->delete( $table, $where, __METHOD__ );
357 }
358 if ( count( $insertions ) ) {
359 $this->mDb->insert( $table, $insertions, __METHOD__, 'IGNORE' );
360 }
361 }
362
363
364 /**
365 * Get an array of pagelinks insertions for passing to the DB
366 * Skips the titles specified by the 2-D array $existing
367 * @private
368 */
369 function getLinkInsertions( $existing = array() ) {
370 $arr = array();
371 foreach( $this->mLinks as $ns => $dbkeys ) {
372 # array_diff_key() was introduced in PHP 5.1, there is a compatibility function
373 # in GlobalFunctions.php
374 $diffs = isset( $existing[$ns] ) ? array_diff_key( $dbkeys, $existing[$ns] ) : $dbkeys;
375 foreach ( $diffs as $dbk => $id ) {
376 $arr[] = array(
377 'pl_from' => $this->mId,
378 'pl_namespace' => $ns,
379 'pl_title' => $dbk
380 );
381 }
382 }
383 return $arr;
384 }
385
386 /**
387 * Get an array of template insertions. Like getLinkInsertions()
388 * @private
389 */
390 function getTemplateInsertions( $existing = array() ) {
391 $arr = array();
392 foreach( $this->mTemplates as $ns => $dbkeys ) {
393 $diffs = isset( $existing[$ns] ) ? array_diff_key( $dbkeys, $existing[$ns] ) : $dbkeys;
394 foreach ( $diffs as $dbk => $id ) {
395 $arr[] = array(
396 'tl_from' => $this->mId,
397 'tl_namespace' => $ns,
398 'tl_title' => $dbk
399 );
400 }
401 }
402 return $arr;
403 }
404
405 /**
406 * Get an array of image insertions
407 * Skips the names specified in $existing
408 * @private
409 */
410 function getImageInsertions( $existing = array() ) {
411 $arr = array();
412 $diffs = array_diff_key( $this->mImages, $existing );
413 foreach( $diffs as $iname => $dummy ) {
414 $arr[] = array(
415 'il_from' => $this->mId,
416 'il_to' => $iname
417 );
418 }
419 return $arr;
420 }
421
422 /**
423 * Get an array of externallinks insertions. Skips the names specified in $existing
424 * @private
425 */
426 function getExternalInsertions( $existing = array() ) {
427 $arr = array();
428 $diffs = array_diff_key( $this->mExternals, $existing );
429 foreach( $diffs as $url => $dummy ) {
430 $arr[] = array(
431 'el_from' => $this->mId,
432 'el_to' => $url,
433 'el_index' => wfMakeUrlIndex( $url ),
434 );
435 }
436 return $arr;
437 }
438
439 /**
440 * Get an array of category insertions
441 * @param array $existing Array mapping existing category names to sort keys. If both
442 * match a link in $this, the link will be omitted from the output
443 * @private
444 */
445 function getCategoryInsertions( $existing = array() ) {
446 $diffs = array_diff_assoc( $this->mCategories, $existing );
447 $arr = array();
448 foreach ( $diffs as $name => $sortkey ) {
449 $arr[] = array(
450 'cl_from' => $this->mId,
451 'cl_to' => $name,
452 'cl_sortkey' => $sortkey,
453 'cl_timestamp' => $this->mDb->timestamp()
454 );
455 }
456 return $arr;
457 }
458
459 /**
460 * Get an array of interlanguage link insertions
461 * @param array $existing Array mapping existing language codes to titles
462 * @private
463 */
464 function getInterlangInsertions( $existing = array() ) {
465 $diffs = array_diff_assoc( $this->mInterlangs, $existing );
466 $arr = array();
467 foreach( $diffs as $lang => $title ) {
468 $arr[] = array(
469 'll_from' => $this->mId,
470 'll_lang' => $lang,
471 'll_title' => $title
472 );
473 }
474 return $arr;
475 }
476
477 /**
478 * Get an array of page property insertions
479 */
480 function getPropertyInsertions( $existing = array() ) {
481 $diffs = array_diff_assoc( $this->mProperties, $existing );
482 $arr = array();
483 foreach ( $diffs as $name => $value ) {
484 $arr[] = array(
485 'pp_page' => $this->mId,
486 'pp_propname' => $name,
487 'pp_value' => $value,
488 );
489 }
490 return $arr;
491 }
492
493
494 /**
495 * Given an array of existing links, returns those links which are not in $this
496 * and thus should be deleted.
497 * @private
498 */
499 function getLinkDeletions( $existing ) {
500 $del = array();
501 foreach ( $existing as $ns => $dbkeys ) {
502 if ( isset( $this->mLinks[$ns] ) ) {
503 $del[$ns] = array_diff_key( $existing[$ns], $this->mLinks[$ns] );
504 } else {
505 $del[$ns] = $existing[$ns];
506 }
507 }
508 return $del;
509 }
510
511 /**
512 * Given an array of existing templates, returns those templates which are not in $this
513 * and thus should be deleted.
514 * @private
515 */
516 function getTemplateDeletions( $existing ) {
517 $del = array();
518 foreach ( $existing as $ns => $dbkeys ) {
519 if ( isset( $this->mTemplates[$ns] ) ) {
520 $del[$ns] = array_diff_key( $existing[$ns], $this->mTemplates[$ns] );
521 } else {
522 $del[$ns] = $existing[$ns];
523 }
524 }
525 return $del;
526 }
527
528 /**
529 * Given an array of existing images, returns those images which are not in $this
530 * and thus should be deleted.
531 * @private
532 */
533 function getImageDeletions( $existing ) {
534 return array_diff_key( $existing, $this->mImages );
535 }
536
537 /**
538 * Given an array of existing external links, returns those links which are not
539 * in $this and thus should be deleted.
540 * @private
541 */
542 function getExternalDeletions( $existing ) {
543 return array_diff_key( $existing, $this->mExternals );
544 }
545
546 /**
547 * Given an array of existing categories, returns those categories which are not in $this
548 * and thus should be deleted.
549 * @private
550 */
551 function getCategoryDeletions( $existing ) {
552 return array_diff_assoc( $existing, $this->mCategories );
553 }
554
555 /**
556 * Given an array of existing interlanguage links, returns those links which are not
557 * in $this and thus should be deleted.
558 * @private
559 */
560 function getInterlangDeletions( $existing ) {
561 return array_diff_assoc( $existing, $this->mInterlangs );
562 }
563
564 /**
565 * Get array of properties which should be deleted.
566 * @private
567 */
568 function getPropertyDeletions( $existing ) {
569 return array_diff_assoc( $existing, $this->mProperties );
570 }
571
572 /**
573 * Get an array of existing links, as a 2-D array
574 * @private
575 */
576 function getExistingLinks() {
577 if ( is_array( $this->mExistingLinks ) )
578 return $this->mExistingLinks;
579
580 $res = $this->mDb->select( 'pagelinks', array( 'pl_namespace', 'pl_title' ),
581 array( 'pl_from' => $this->mId ), __METHOD__, $this->mOptions );
582 $arr = array();
583 while ( $row = $this->mDb->fetchObject( $res ) ) {
584 if ( !isset( $arr[$row->pl_namespace] ) ) {
585 $arr[$row->pl_namespace] = array();
586 }
587 $arr[$row->pl_namespace][$row->pl_title] = 1;
588 }
589 $this->mDb->freeResult( $res );
590 return $this->mExistingLinks = $arr;
591 }
592
593 /**
594 * Get an array of existing templates, as a 2-D array
595 * @private
596 */
597 function getExistingTemplates() {
598 if ( is_array( $this->mExistingTemplates ) )
599 return $this->mExistingTemplates;
600
601 $res = $this->mDb->select( 'templatelinks', array( 'tl_namespace', 'tl_title' ),
602 array( 'tl_from' => $this->mId ), __METHOD__, $this->mOptions );
603 $arr = array();
604 while ( $row = $this->mDb->fetchObject( $res ) ) {
605 if ( !isset( $arr[$row->tl_namespace] ) ) {
606 $arr[$row->tl_namespace] = array();
607 }
608 $arr[$row->tl_namespace][$row->tl_title] = 1;
609 }
610 $this->mDb->freeResult( $res );
611 return $this->mExistingTemplates = $arr;
612 }
613
614 /**
615 * Get an array of existing images, image names in the keys
616 * @private
617 */
618 function getExistingImages() {
619 if ( is_array( $this->mExistingImages ) )
620 return $this->mExistingImages;
621
622 $res = $this->mDb->select( 'imagelinks', array( 'il_to' ),
623 array( 'il_from' => $this->mId ), __METHOD__, $this->mOptions );
624 $arr = array();
625 while ( $row = $this->mDb->fetchObject( $res ) ) {
626 $arr[$row->il_to] = 1;
627 }
628 $this->mDb->freeResult( $res );
629 return $this->mExistingImages = $arr;
630 }
631
632 /**
633 * Get an array of existing external links, URLs in the keys
634 * @private
635 */
636 function getExistingExternals() {
637 if ( is_array( $this->mExistingExternals ) )
638 return $this->mExistingExternals;
639
640 $res = $this->mDb->select( 'externallinks', array( 'el_to' ),
641 array( 'el_from' => $this->mId ), __METHOD__, $this->mOptions );
642 $arr = array();
643 while ( $row = $this->mDb->fetchObject( $res ) ) {
644 $arr[$row->el_to] = 1;
645 }
646 $this->mDb->freeResult( $res );
647 return $this->mExistingExternals = $arr;
648 }
649
650 /**
651 * Get an array of existing categories, with the name in the key and sort key in the value.
652 * @private
653 */
654 function getExistingCategories() {
655 if ( is_array( $this->mExistingCategories ) )
656 return $this->mExistingCategories;
657
658 $res = $this->mDb->select( 'categorylinks', array( 'cl_to', 'cl_sortkey' ),
659 array( 'cl_from' => $this->mId ), __METHOD__, $this->mOptions );
660 $arr = array();
661 while ( $row = $this->mDb->fetchObject( $res ) ) {
662 $arr[$row->cl_to] = $row->cl_sortkey;
663 }
664 $this->mDb->freeResult( $res );
665 return $this->mExistingCategories = $arr;
666 }
667
668 /**
669 * Get an array of existing interlanguage links, with the language code in the key and the
670 * title in the value.
671 * @private
672 */
673 function getExistingInterlangs() {
674 if ( is_array( $this->mExistingInterlangs ) )
675 return $this->mExistingInterlangs;
676
677 $res = $this->mDb->select( 'langlinks', array( 'll_lang', 'll_title' ),
678 array( 'll_from' => $this->mId ), __METHOD__, $this->mOptions );
679 $arr = array();
680 while ( $row = $this->mDb->fetchObject( $res ) ) {
681 $arr[$row->ll_lang] = $row->ll_title;
682 }
683 return $this->mExistingInterlangs = $arr;
684 }
685
686 /**
687 * Get an array of existing categories, with the name in the key and sort key in the value.
688 * @private
689 */
690 function getExistingProperties() {
691 if ( is_array( $this->mExistingProperties ) )
692 return $this->mExistingProperties;
693
694 $res = $this->mDb->select( 'page_props', array( 'pp_propname', 'pp_value' ),
695 array( 'pp_page' => $this->mId ), __METHOD__, $this->mOptions );
696 $arr = array();
697 while ( $row = $this->mDb->fetchObject( $res ) ) {
698 $arr[$row->pp_propname] = $row->pp_value;
699 }
700 $this->mDb->freeResult( $res );
701 return $this->mExistingProperties = $arr;
702 }
703
704
705 /**
706 * Return the title object of the page being updated
707 */
708 function getTitle() {
709 return $this->mTitle;
710 }
711
712 /**
713 * Invalidate any necessary link lists related to page property changes
714 */
715 function invalidateProperties( $changed ) {
716 global $wgPagePropLinkInvalidations;
717
718 foreach ( $changed as $name => $value ) {
719 if ( isset( $wgPagePropLinkInvalidations[$name] ) ) {
720 $inv = $wgPagePropLinkInvalidations[$name];
721 if ( !is_array( $inv ) ) {
722 $inv = array( $inv );
723 }
724 foreach ( $inv as $table ) {
725 $update = new HTMLCacheUpdate( $this->mTitle, $table );
726 $update->doUpdate();
727 }
728 }
729 }
730 }
731
732 // Recentlinkchanges constants
733 const INSERTION = 1;
734 const DELETION = 2;
735 private static $rlcFields = array(
736 'rlc_type',
737 'rlc_timestamp',
738 'rlc_action',
739 'rlc_from',
740 'rlc_to_namespace',
741 'rlc_to_title',
742 'rlc_to_blob'
743 );
744 /*
745 * Insert items to recentlinkchanges
746 */
747 function makeRecentlinkchanges() {
748 $insert = array();
749 $now = $this->mDb->timestamp();
750
751 // Category changes
752 $existing = array_keys( $this->getExistingCategories() );
753 $current = array_keys( $this->mCategories );
754 $this->simpleAddToLinkchanges( $insert, 'category', $now, $existing, $current, NS_CATEGORY );
755
756 // External links
757 $existing = array_keys( $this->getExistingExternals() );
758 $current = array_keys( $this->mExternals );
759 $insertions = array_diff( $current, $existing );
760 foreach ( $insertions as $item )
761 $insert[] = array(
762 'external', $now, self::INSERTION,
763 $this->mId, null, null, $item );
764 $deletions = array_diff( $existing, $current );
765 foreach ( $deletions as $item )
766 $insert[] = array(
767 'external', $now, self::DELETION,
768 $this->mId, null, null, $item );
769
770 // Image changes
771 $existing = array_keys( $this->getExistingImages() );
772 $current = array_keys( $this->mImages );
773 $this->simpleAddToLinkchanges( $insert, 'image', $now, $existing, $current, NS_IMAGE );
774
775 // Interlangs
776 $existing = $this->getExistingInterlangs();
777 $current = $this->mInterlangs;
778 $this->assocAddToLinkchanges( $insert, 'interlang', $existing, $current );
779
780 // Page links
781 $existing = $this->getExistingLinks();
782 $current = $this->mLinks;
783 $this->addToLinkChangesByNamespace( $insert, 'page', $now, $existing, $current);
784
785 // Properties
786 $existing = $this->getExistingProperties();
787 $current = $this->mProperties;
788 $this->assocAddToLinkchanges( $insert, 'property', $existing, $current );
789
790 // Templates
791 $existing = $this->getExistingTemplates();
792 $current = $this->mTemplates;
793 $this->addToLinkChangesByNamespace( $insert, 'template', $now, $existing, $current);
794
795 $this->mDb->insert( 'recentlinkchanges', $insert, __METHOD__ );
796
797 }
798
799 /*
800 * Compute the difference for arrays of titles with namespace $ns and add
801 * them to $insert.
802 */
803 private function simpleAddToLinkchanges( &$insert, $type, $now, $existing, $current, $ns ) {
804
805 $insertions = array_diff( $current, $existing );
806 foreach ( $insertions as $item )
807 $insert[] = array_combine(self::$rlcFields, array(
808 $type, $now, self::INSERTION,
809 $this->mId, $ns, $item, null
810 ) );
811 $deletions = array_diff( $existing, $current );
812 foreach ( $deletions as $item )
813 $insert[] = array_combine(self::$rlcFields, array(
814 $type, $now, self::DELETION,
815 $this->mId, $ns, $item, null
816 ) );
817
818 }
819
820 /*
821 * Compute the difference for associative arrays and insert them to
822 * $insert as title => blob.
823 */
824 function assocAddToLinkChanges( &$insert, $type, $now, $existing, $current ) {
825 $insertions = array_diff_assoc( $current, $existing );
826 foreach ( $insertions as $key => $value )
827 $insert[] = array_combine(self::$rlcFields, array(
828 $type, $now, self::INSERTION,
829 $this->mId, null, $key, $value
830 ) );
831 $deletions = array_diff_assoc( $existing, $current );
832 foreach ( $deletions as $key => $value )
833 $insert[] = array_combine(self::$rlcFields, array(
834 $type, $now, self::DELETION,
835 $this->mId, null, $key, $value
836 ) );
837 }
838
839 /*
840 * Format arrays in the form $namespace => $titleArray for use in
841 * simpleAddLinkLinkChanges
842 */
843 function addToLinkChangesByNamespace( &$insert, $type, $now, $existing, $current ) {
844 $namespaces = array_merge( array_keys( $existing ), array_keys( $current ) );
845 foreach ( $namespaces as $ns )
846 $this->simpleAddToLinkchanges( $insert, $type, $now,
847 isset( $existing[$ns] ) ? array_keys( $existing[$ns] ) : array(),
848 isset( $current[$ns] ) ? array_keys( $current[$ns] ) : array(),
849 $ns );
850 }
851 }