Merge "Remove 'prefsnologin' message, don't use 'watchnologin' where inappropriate"
[lhc/web/wiklou.git] / includes / deferred / LinksUpdate.php
1 <?php
2 /**
3 * Updater for link tracking tables after a page edit.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 */
22
23 /**
24 * See docs/deferred.txt
25 *
26 * @todo document (e.g. one-sentence top-level class description).
27 */
28 class LinksUpdate extends SqlDataUpdate {
29 // @todo make members protected, but make sure extensions don't break
30
31 /** @var int Page ID of the article linked from */
32 public $mId;
33
34 /** @var Title object of the article linked from */
35 public $mTitle;
36
37 /** @var ParserOutput */
38 public $mParserOutput;
39
40 /** @var array Map of title strings to IDs for the links in the document */
41 public $mLinks;
42
43 /** @var array DB keys of the images used, in the array key only */
44 public $mImages;
45
46 /** @var array Map of title strings to IDs for the template references, including broken ones */
47 public $mTemplates;
48
49 /** @var array URLs of external links, array key only */
50 public $mExternals;
51
52 /** @var array Map of category names to sort keys */
53 public $mCategories;
54
55 /** @var array ap of language codes to titles */
56 public $mInterlangs;
57
58 /** @var array Map of arbitrary name to value */
59 public $mProperties;
60
61 /** @var DatabaseBase Database connection reference */
62 public $mDb;
63
64 /** @var array SELECT options to be used */
65 public $mOptions;
66
67 /** @var bool Whether to queue jobs for recursive updates */
68 public $mRecursive;
69
70 /**
71 * @var null|array Added links if calculated.
72 */
73 private $linkInsertions = null;
74
75 /**
76 * @var null|array Deleted links if calculated.
77 */
78 private $linkDeletions = null;
79
80 /**
81 * Constructor
82 *
83 * @param Title $title Title of the page we're updating
84 * @param ParserOutput $parserOutput Output from a full parse of this page
85 * @param bool $recursive Queue jobs for recursive updates?
86 * @throws MWException
87 */
88 function __construct( $title, $parserOutput, $recursive = true ) {
89 parent::__construct( false ); // no implicit transaction
90
91 if ( !( $title instanceof Title ) ) {
92 throw new MWException( "The calling convention to LinksUpdate::LinksUpdate() has changed. " .
93 "Please see Article::editUpdates() for an invocation example.\n" );
94 }
95
96 if ( !( $parserOutput instanceof ParserOutput ) ) {
97 throw new MWException( "The calling convention to LinksUpdate::__construct() has changed. " .
98 "Please see WikiPage::doEditUpdates() for an invocation example.\n" );
99 }
100
101 $this->mTitle = $title;
102 $this->mId = $title->getArticleID();
103
104 if ( !$this->mId ) {
105 throw new MWException( "The Title object did not provide an article " .
106 "ID. Perhaps the page doesn't exist?" );
107 }
108
109 $this->mParserOutput = $parserOutput;
110
111 $this->mLinks = $parserOutput->getLinks();
112 $this->mImages = $parserOutput->getImages();
113 $this->mTemplates = $parserOutput->getTemplates();
114 $this->mExternals = $parserOutput->getExternalLinks();
115 $this->mCategories = $parserOutput->getCategories();
116 $this->mProperties = $parserOutput->getProperties();
117 $this->mInterwikis = $parserOutput->getInterwikiLinks();
118
119 # Convert the format of the interlanguage links
120 # I didn't want to change it in the ParserOutput, because that array is passed all
121 # the way back to the skin, so either a skin API break would be required, or an
122 # inefficient back-conversion.
123 $ill = $parserOutput->getLanguageLinks();
124 $this->mInterlangs = array();
125 foreach ( $ill as $link ) {
126 list( $key, $title ) = explode( ':', $link, 2 );
127 $this->mInterlangs[$key] = $title;
128 }
129
130 foreach ( $this->mCategories as &$sortkey ) {
131 # If the sortkey is longer then 255 bytes,
132 # it truncated by DB, and then doesn't get
133 # matched when comparing existing vs current
134 # categories, causing bug 25254.
135 # Also. substr behaves weird when given "".
136 if ( $sortkey !== '' ) {
137 $sortkey = substr( $sortkey, 0, 255 );
138 }
139 }
140
141 $this->mRecursive = $recursive;
142
143 wfRunHooks( 'LinksUpdateConstructed', array( &$this ) );
144 }
145
146 /**
147 * Update link tables with outgoing links from an updated article
148 */
149 public function doUpdate() {
150 wfRunHooks( 'LinksUpdate', array( &$this ) );
151 $this->doIncrementalUpdate();
152 wfRunHooks( 'LinksUpdateComplete', array( &$this ) );
153 }
154
155 protected function doIncrementalUpdate() {
156 wfProfileIn( __METHOD__ );
157
158 # Page links
159 $existing = $this->getExistingLinks();
160 $this->linkDeletions = $this->getLinkDeletions( $existing );
161 $this->linkInsertions = $this->getLinkInsertions( $existing );
162 $this->incrTableUpdate( 'pagelinks', 'pl', $this->linkDeletions, $this->linkInsertions );
163
164 # Image links
165 $existing = $this->getExistingImages();
166
167 $imageDeletes = $this->getImageDeletions( $existing );
168 $this->incrTableUpdate( 'imagelinks', 'il', $imageDeletes,
169 $this->getImageInsertions( $existing ) );
170
171 # Invalidate all image description pages which had links added or removed
172 $imageUpdates = $imageDeletes + array_diff_key( $this->mImages, $existing );
173 $this->invalidateImageDescriptions( $imageUpdates );
174
175 # External links
176 $existing = $this->getExistingExternals();
177 $this->incrTableUpdate( 'externallinks', 'el', $this->getExternalDeletions( $existing ),
178 $this->getExternalInsertions( $existing ) );
179
180 # Language links
181 $existing = $this->getExistingInterlangs();
182 $this->incrTableUpdate( 'langlinks', 'll', $this->getInterlangDeletions( $existing ),
183 $this->getInterlangInsertions( $existing ) );
184
185 # Inline interwiki links
186 $existing = $this->getExistingInterwikis();
187 $this->incrTableUpdate( 'iwlinks', 'iwl', $this->getInterwikiDeletions( $existing ),
188 $this->getInterwikiInsertions( $existing ) );
189
190 # Template links
191 $existing = $this->getExistingTemplates();
192 $this->incrTableUpdate( 'templatelinks', 'tl', $this->getTemplateDeletions( $existing ),
193 $this->getTemplateInsertions( $existing ) );
194
195 # Category links
196 $existing = $this->getExistingCategories();
197
198 $categoryDeletes = $this->getCategoryDeletions( $existing );
199
200 $this->incrTableUpdate( 'categorylinks', 'cl', $categoryDeletes,
201 $this->getCategoryInsertions( $existing ) );
202
203 # Invalidate all categories which were added, deleted or changed (set symmetric difference)
204 $categoryInserts = array_diff_assoc( $this->mCategories, $existing );
205 $categoryUpdates = $categoryInserts + $categoryDeletes;
206 $this->invalidateCategories( $categoryUpdates );
207 $this->updateCategoryCounts( $categoryInserts, $categoryDeletes );
208
209 # Page properties
210 $existing = $this->getExistingProperties();
211
212 $propertiesDeletes = $this->getPropertyDeletions( $existing );
213
214 $this->incrTableUpdate( 'page_props', 'pp', $propertiesDeletes,
215 $this->getPropertyInsertions( $existing ) );
216
217 # Invalidate the necessary pages
218 $changed = $propertiesDeletes + array_diff_assoc( $this->mProperties, $existing );
219 $this->invalidateProperties( $changed );
220
221 # Refresh links of all pages including this page
222 # This will be in a separate transaction
223 if ( $this->mRecursive ) {
224 $this->queueRecursiveJobs();
225 }
226
227 wfProfileOut( __METHOD__ );
228 }
229
230 /**
231 * Queue recursive jobs for this page
232 *
233 * Which means do LinksUpdate on all templates
234 * that include the current page, using the job queue.
235 */
236 function queueRecursiveJobs() {
237 self::queueRecursiveJobsForTable( $this->mTitle, 'templatelinks' );
238 }
239
240 /**
241 * Queue a RefreshLinks job for any table.
242 *
243 * @param Title $title Title to do job for
244 * @param string $table Table to use (e.g. 'templatelinks')
245 */
246 public static function queueRecursiveJobsForTable( Title $title, $table ) {
247 wfProfileIn( __METHOD__ );
248 if ( $title->getBacklinkCache()->hasLinks( $table ) ) {
249 $job = new RefreshLinksJob2(
250 $title,
251 array(
252 'table' => $table,
253 ) + Job::newRootJobParams( // "overall" refresh links job info
254 "refreshlinks:{$table}:{$title->getPrefixedText()}"
255 )
256 );
257 JobQueueGroup::singleton()->push( $job );
258 JobQueueGroup::singleton()->deduplicateRootJob( $job );
259 }
260 wfProfileOut( __METHOD__ );
261 }
262
263 /**
264 * @param $cats
265 */
266 function invalidateCategories( $cats ) {
267 $this->invalidatePages( NS_CATEGORY, array_keys( $cats ) );
268 }
269
270 /**
271 * Update all the appropriate counts in the category table.
272 * @param array $added Associative array of category name => sort key
273 * @param array $deleted Associative array of category name => sort key
274 */
275 function updateCategoryCounts( $added, $deleted ) {
276 $a = WikiPage::factory( $this->mTitle );
277 $a->updateCategoryCounts(
278 array_keys( $added ), array_keys( $deleted )
279 );
280 }
281
282 /**
283 * @param $images
284 */
285 function invalidateImageDescriptions( $images ) {
286 $this->invalidatePages( NS_FILE, array_keys( $images ) );
287 }
288
289 /**
290 * Update a table by doing a delete query then an insert query
291 * @param string $table Table name
292 * @param string $prefix Field name prefix
293 * @param array $deletions
294 * @param array $insertions Rows to insert
295 */
296 function incrTableUpdate( $table, $prefix, $deletions, $insertions ) {
297 if ( $table == 'page_props' ) {
298 $fromField = 'pp_page';
299 } else {
300 $fromField = "{$prefix}_from";
301 }
302 $where = array( $fromField => $this->mId );
303 if ( $table == 'pagelinks' || $table == 'templatelinks' || $table == 'iwlinks' ) {
304 if ( $table == 'iwlinks' ) {
305 $baseKey = 'iwl_prefix';
306 } else {
307 $baseKey = "{$prefix}_namespace";
308 }
309 $clause = $this->mDb->makeWhereFrom2d( $deletions, $baseKey, "{$prefix}_title" );
310 if ( $clause ) {
311 $where[] = $clause;
312 } else {
313 $where = false;
314 }
315 } else {
316 if ( $table == 'langlinks' ) {
317 $toField = 'll_lang';
318 } elseif ( $table == 'page_props' ) {
319 $toField = 'pp_propname';
320 } else {
321 $toField = $prefix . '_to';
322 }
323 if ( count( $deletions ) ) {
324 $where[] = "$toField IN (" . $this->mDb->makeList( array_keys( $deletions ) ) . ')';
325 } else {
326 $where = false;
327 }
328 }
329 if ( $where ) {
330 $this->mDb->delete( $table, $where, __METHOD__ );
331 }
332 if ( count( $insertions ) ) {
333 $this->mDb->insert( $table, $insertions, __METHOD__, 'IGNORE' );
334 wfRunHooks( 'LinksUpdateAfterInsert', array( $this, $table, $insertions ) );
335 }
336 }
337
338 /**
339 * Get an array of pagelinks insertions for passing to the DB
340 * Skips the titles specified by the 2-D array $existing
341 * @param array $existing
342 * @return array
343 */
344 private function getLinkInsertions( $existing = array() ) {
345 $arr = array();
346 foreach ( $this->mLinks as $ns => $dbkeys ) {
347 $diffs = isset( $existing[$ns] )
348 ? array_diff_key( $dbkeys, $existing[$ns] )
349 : $dbkeys;
350 foreach ( $diffs as $dbk => $id ) {
351 $arr[] = array(
352 'pl_from' => $this->mId,
353 'pl_namespace' => $ns,
354 'pl_title' => $dbk
355 );
356 }
357 }
358
359 return $arr;
360 }
361
362 /**
363 * Get an array of template insertions. Like getLinkInsertions()
364 * @param array $existing
365 * @return array
366 */
367 private function getTemplateInsertions( $existing = array() ) {
368 $arr = array();
369 foreach ( $this->mTemplates as $ns => $dbkeys ) {
370 $diffs = isset( $existing[$ns] ) ? array_diff_key( $dbkeys, $existing[$ns] ) : $dbkeys;
371 foreach ( $diffs as $dbk => $id ) {
372 $arr[] = array(
373 'tl_from' => $this->mId,
374 'tl_namespace' => $ns,
375 'tl_title' => $dbk
376 );
377 }
378 }
379
380 return $arr;
381 }
382
383 /**
384 * Get an array of image insertions
385 * Skips the names specified in $existing
386 * @param array $existing
387 * @return array
388 */
389 private function getImageInsertions( $existing = array() ) {
390 $arr = array();
391 $diffs = array_diff_key( $this->mImages, $existing );
392 foreach ( $diffs as $iname => $dummy ) {
393 $arr[] = array(
394 'il_from' => $this->mId,
395 'il_to' => $iname
396 );
397 }
398
399 return $arr;
400 }
401
402 /**
403 * Get an array of externallinks insertions. Skips the names specified in $existing
404 * @param array $existing
405 * @return array
406 */
407 private function getExternalInsertions( $existing = array() ) {
408 $arr = array();
409 $diffs = array_diff_key( $this->mExternals, $existing );
410 foreach ( $diffs as $url => $dummy ) {
411 foreach ( wfMakeUrlIndexes( $url ) as $index ) {
412 $arr[] = array(
413 'el_from' => $this->mId,
414 'el_to' => $url,
415 'el_index' => $index,
416 );
417 }
418 }
419
420 return $arr;
421 }
422
423 /**
424 * Get an array of category insertions
425 *
426 * @param array $existing mapping existing category names to sort keys. If both
427 * match a link in $this, the link will be omitted from the output
428 *
429 * @return array
430 */
431 private function getCategoryInsertions( $existing = array() ) {
432 global $wgContLang, $wgCategoryCollation;
433 $diffs = array_diff_assoc( $this->mCategories, $existing );
434 $arr = array();
435 foreach ( $diffs as $name => $prefix ) {
436 $nt = Title::makeTitleSafe( NS_CATEGORY, $name );
437 $wgContLang->findVariantLink( $name, $nt, true );
438
439 if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
440 $type = 'subcat';
441 } elseif ( $this->mTitle->getNamespace() == NS_FILE ) {
442 $type = 'file';
443 } else {
444 $type = 'page';
445 }
446
447 # Treat custom sortkeys as a prefix, so that if multiple
448 # things are forced to sort as '*' or something, they'll
449 # sort properly in the category rather than in page_id
450 # order or such.
451 $sortkey = Collation::singleton()->getSortKey(
452 $this->mTitle->getCategorySortkey( $prefix ) );
453
454 $arr[] = array(
455 'cl_from' => $this->mId,
456 'cl_to' => $name,
457 'cl_sortkey' => $sortkey,
458 'cl_timestamp' => $this->mDb->timestamp(),
459 'cl_sortkey_prefix' => $prefix,
460 'cl_collation' => $wgCategoryCollation,
461 'cl_type' => $type,
462 );
463 }
464
465 return $arr;
466 }
467
468 /**
469 * Get an array of interlanguage link insertions
470 *
471 * @param array $existing mapping existing language codes to titles
472 *
473 * @return array
474 */
475 private function getInterlangInsertions( $existing = array() ) {
476 $diffs = array_diff_assoc( $this->mInterlangs, $existing );
477 $arr = array();
478 foreach ( $diffs as $lang => $title ) {
479 $arr[] = array(
480 'll_from' => $this->mId,
481 'll_lang' => $lang,
482 'll_title' => $title
483 );
484 }
485
486 return $arr;
487 }
488
489 /**
490 * Get an array of page property insertions
491 * @param array $existing
492 * @return array
493 */
494 function getPropertyInsertions( $existing = array() ) {
495 $diffs = array_diff_assoc( $this->mProperties, $existing );
496 $arr = array();
497 foreach ( $diffs as $name => $value ) {
498 $arr[] = array(
499 'pp_page' => $this->mId,
500 'pp_propname' => $name,
501 'pp_value' => $value,
502 );
503 }
504
505 return $arr;
506 }
507
508 /**
509 * Get an array of interwiki insertions for passing to the DB
510 * Skips the titles specified by the 2-D array $existing
511 * @param array $existing
512 * @return array
513 */
514 private function getInterwikiInsertions( $existing = array() ) {
515 $arr = array();
516 foreach ( $this->mInterwikis as $prefix => $dbkeys ) {
517 $diffs = isset( $existing[$prefix] )
518 ? array_diff_key( $dbkeys, $existing[$prefix] )
519 : $dbkeys;
520
521 foreach ( $diffs as $dbk => $id ) {
522 $arr[] = array(
523 'iwl_from' => $this->mId,
524 'iwl_prefix' => $prefix,
525 'iwl_title' => $dbk
526 );
527 }
528 }
529
530 return $arr;
531 }
532
533 /**
534 * Given an array of existing links, returns those links which are not in $this
535 * and thus should be deleted.
536 * @param array $existing
537 * @return array
538 */
539 private function getLinkDeletions( $existing ) {
540 $del = array();
541 foreach ( $existing as $ns => $dbkeys ) {
542 if ( isset( $this->mLinks[$ns] ) ) {
543 $del[$ns] = array_diff_key( $existing[$ns], $this->mLinks[$ns] );
544 } else {
545 $del[$ns] = $existing[$ns];
546 }
547 }
548
549 return $del;
550 }
551
552 /**
553 * Given an array of existing templates, returns those templates which are not in $this
554 * and thus should be deleted.
555 * @param array $existing
556 * @return array
557 */
558 private function getTemplateDeletions( $existing ) {
559 $del = array();
560 foreach ( $existing as $ns => $dbkeys ) {
561 if ( isset( $this->mTemplates[$ns] ) ) {
562 $del[$ns] = array_diff_key( $existing[$ns], $this->mTemplates[$ns] );
563 } else {
564 $del[$ns] = $existing[$ns];
565 }
566 }
567
568 return $del;
569 }
570
571 /**
572 * Given an array of existing images, returns those images which are not in $this
573 * and thus should be deleted.
574 * @param array $existing
575 * @return array
576 */
577 private function getImageDeletions( $existing ) {
578 return array_diff_key( $existing, $this->mImages );
579 }
580
581 /**
582 * Given an array of existing external links, returns those links which are not
583 * in $this and thus should be deleted.
584 * @param array $existing
585 * @return array
586 */
587 private function getExternalDeletions( $existing ) {
588 return array_diff_key( $existing, $this->mExternals );
589 }
590
591 /**
592 * Given an array of existing categories, returns those categories which are not in $this
593 * and thus should be deleted.
594 * @param array $existing
595 * @return array
596 */
597 private function getCategoryDeletions( $existing ) {
598 return array_diff_assoc( $existing, $this->mCategories );
599 }
600
601 /**
602 * Given an array of existing interlanguage links, returns those links which are not
603 * in $this and thus should be deleted.
604 * @param array $existing
605 * @return array
606 */
607 private function getInterlangDeletions( $existing ) {
608 return array_diff_assoc( $existing, $this->mInterlangs );
609 }
610
611 /**
612 * Get array of properties which should be deleted.
613 * @param array $existing
614 * @return array
615 */
616 function getPropertyDeletions( $existing ) {
617 return array_diff_assoc( $existing, $this->mProperties );
618 }
619
620 /**
621 * Given an array of existing interwiki links, returns those links which are not in $this
622 * and thus should be deleted.
623 * @param array $existing
624 * @return array
625 */
626 private function getInterwikiDeletions( $existing ) {
627 $del = array();
628 foreach ( $existing as $prefix => $dbkeys ) {
629 if ( isset( $this->mInterwikis[$prefix] ) ) {
630 $del[$prefix] = array_diff_key( $existing[$prefix], $this->mInterwikis[$prefix] );
631 } else {
632 $del[$prefix] = $existing[$prefix];
633 }
634 }
635
636 return $del;
637 }
638
639 /**
640 * Get an array of existing links, as a 2-D array
641 *
642 * @return array
643 */
644 private function getExistingLinks() {
645 $res = $this->mDb->select( 'pagelinks', array( 'pl_namespace', 'pl_title' ),
646 array( 'pl_from' => $this->mId ), __METHOD__, $this->mOptions );
647 $arr = array();
648 foreach ( $res as $row ) {
649 if ( !isset( $arr[$row->pl_namespace] ) ) {
650 $arr[$row->pl_namespace] = array();
651 }
652 $arr[$row->pl_namespace][$row->pl_title] = 1;
653 }
654
655 return $arr;
656 }
657
658 /**
659 * Get an array of existing templates, as a 2-D array
660 *
661 * @return array
662 */
663 private function getExistingTemplates() {
664 $res = $this->mDb->select( 'templatelinks', array( 'tl_namespace', 'tl_title' ),
665 array( 'tl_from' => $this->mId ), __METHOD__, $this->mOptions );
666 $arr = array();
667 foreach ( $res as $row ) {
668 if ( !isset( $arr[$row->tl_namespace] ) ) {
669 $arr[$row->tl_namespace] = array();
670 }
671 $arr[$row->tl_namespace][$row->tl_title] = 1;
672 }
673
674 return $arr;
675 }
676
677 /**
678 * Get an array of existing images, image names in the keys
679 *
680 * @return array
681 */
682 private function getExistingImages() {
683 $res = $this->mDb->select( 'imagelinks', array( 'il_to' ),
684 array( 'il_from' => $this->mId ), __METHOD__, $this->mOptions );
685 $arr = array();
686 foreach ( $res as $row ) {
687 $arr[$row->il_to] = 1;
688 }
689
690 return $arr;
691 }
692
693 /**
694 * Get an array of existing external links, URLs in the keys
695 *
696 * @return array
697 */
698 private function getExistingExternals() {
699 $res = $this->mDb->select( 'externallinks', array( 'el_to' ),
700 array( 'el_from' => $this->mId ), __METHOD__, $this->mOptions );
701 $arr = array();
702 foreach ( $res as $row ) {
703 $arr[$row->el_to] = 1;
704 }
705
706 return $arr;
707 }
708
709 /**
710 * Get an array of existing categories, with the name in the key and sort key in the value.
711 *
712 * @return array
713 */
714 private function getExistingCategories() {
715 $res = $this->mDb->select( 'categorylinks', array( 'cl_to', 'cl_sortkey_prefix' ),
716 array( 'cl_from' => $this->mId ), __METHOD__, $this->mOptions );
717 $arr = array();
718 foreach ( $res as $row ) {
719 $arr[$row->cl_to] = $row->cl_sortkey_prefix;
720 }
721
722 return $arr;
723 }
724
725 /**
726 * Get an array of existing interlanguage links, with the language code in the key and the
727 * title in the value.
728 *
729 * @return array
730 */
731 private function getExistingInterlangs() {
732 $res = $this->mDb->select( 'langlinks', array( 'll_lang', 'll_title' ),
733 array( 'll_from' => $this->mId ), __METHOD__, $this->mOptions );
734 $arr = array();
735 foreach ( $res as $row ) {
736 $arr[$row->ll_lang] = $row->ll_title;
737 }
738
739 return $arr;
740 }
741
742 /**
743 * Get an array of existing inline interwiki links, as a 2-D array
744 * @return array (prefix => array(dbkey => 1))
745 */
746 protected function getExistingInterwikis() {
747 $res = $this->mDb->select( 'iwlinks', array( 'iwl_prefix', 'iwl_title' ),
748 array( 'iwl_from' => $this->mId ), __METHOD__, $this->mOptions );
749 $arr = array();
750 foreach ( $res as $row ) {
751 if ( !isset( $arr[$row->iwl_prefix] ) ) {
752 $arr[$row->iwl_prefix] = array();
753 }
754 $arr[$row->iwl_prefix][$row->iwl_title] = 1;
755 }
756
757 return $arr;
758 }
759
760 /**
761 * Get an array of existing categories, with the name in the key and sort key in the value.
762 *
763 * @return array of property names and values
764 */
765 private function getExistingProperties() {
766 $res = $this->mDb->select( 'page_props', array( 'pp_propname', 'pp_value' ),
767 array( 'pp_page' => $this->mId ), __METHOD__, $this->mOptions );
768 $arr = array();
769 foreach ( $res as $row ) {
770 $arr[$row->pp_propname] = $row->pp_value;
771 }
772
773 return $arr;
774 }
775
776 /**
777 * Return the title object of the page being updated
778 * @return Title
779 */
780 public function getTitle() {
781 return $this->mTitle;
782 }
783
784 /**
785 * Returns parser output
786 * @since 1.19
787 * @return ParserOutput
788 */
789 public function getParserOutput() {
790 return $this->mParserOutput;
791 }
792
793 /**
794 * Return the list of images used as generated by the parser
795 * @return array
796 */
797 public function getImages() {
798 return $this->mImages;
799 }
800
801 /**
802 * Invalidate any necessary link lists related to page property changes
803 * @param array $changed
804 */
805 private function invalidateProperties( $changed ) {
806 global $wgPagePropLinkInvalidations;
807
808 foreach ( $changed as $name => $value ) {
809 if ( isset( $wgPagePropLinkInvalidations[$name] ) ) {
810 $inv = $wgPagePropLinkInvalidations[$name];
811 if ( !is_array( $inv ) ) {
812 $inv = array( $inv );
813 }
814 foreach ( $inv as $table ) {
815 $update = new HTMLCacheUpdate( $this->mTitle, $table );
816 $update->doUpdate();
817 }
818 }
819 }
820 }
821
822 /**
823 * Fetch page links added by this LinksUpdate. Only available after the update is complete.
824 * @since 1.22
825 * @return null|array of Titles
826 */
827 public function getAddedLinks() {
828 if ( $this->linkInsertions === null ) {
829 return null;
830 }
831 $result = array();
832 foreach ( $this->linkInsertions as $insertion ) {
833 $result[] = Title::makeTitle( $insertion['pl_namespace'], $insertion['pl_title'] );
834 }
835
836 return $result;
837 }
838
839 /**
840 * Fetch page links removed by this LinksUpdate. Only available after the update is complete.
841 * @since 1.22
842 * @return null|array of Titles
843 */
844 public function getRemovedLinks() {
845 if ( $this->linkDeletions === null ) {
846 return null;
847 }
848 $result = array();
849 foreach ( $this->linkDeletions as $ns => $titles ) {
850 foreach ( $titles as $title => $unused ) {
851 $result[] = Title::makeTitle( $ns, $title );
852 }
853 }
854
855 return $result;
856 }
857 }
858
859 /**
860 * Update object handling the cleanup of links tables after a page was deleted.
861 **/
862 class LinksDeletionUpdate extends SqlDataUpdate {
863 /** @var WikiPage The WikiPage that was deleted */
864 protected $mPage;
865
866 /**
867 * Constructor
868 *
869 * @param WikiPage $page Page we are updating
870 * @throws MWException
871 */
872 function __construct( WikiPage $page ) {
873 parent::__construct( false ); // no implicit transaction
874
875 $this->mPage = $page;
876
877 if ( !$page->exists() ) {
878 throw new MWException( "Page ID not known, perhaps the page doesn't exist?" );
879 }
880 }
881
882 /**
883 * Do some database updates after deletion
884 */
885 public function doUpdate() {
886 $title = $this->mPage->getTitle();
887 $id = $this->mPage->getId();
888
889 # Delete restrictions for it
890 $this->mDb->delete( 'page_restrictions', array( 'pr_page' => $id ), __METHOD__ );
891
892 # Fix category table counts
893 $cats = array();
894 $res = $this->mDb->select( 'categorylinks', 'cl_to', array( 'cl_from' => $id ), __METHOD__ );
895
896 foreach ( $res as $row ) {
897 $cats[] = $row->cl_to;
898 }
899
900 $this->mPage->updateCategoryCounts( array(), $cats );
901
902 # If using cascading deletes, we can skip some explicit deletes
903 if ( !$this->mDb->cascadingDeletes() ) {
904 # Delete outgoing links
905 $this->mDb->delete( 'pagelinks', array( 'pl_from' => $id ), __METHOD__ );
906 $this->mDb->delete( 'imagelinks', array( 'il_from' => $id ), __METHOD__ );
907 $this->mDb->delete( 'categorylinks', array( 'cl_from' => $id ), __METHOD__ );
908 $this->mDb->delete( 'templatelinks', array( 'tl_from' => $id ), __METHOD__ );
909 $this->mDb->delete( 'externallinks', array( 'el_from' => $id ), __METHOD__ );
910 $this->mDb->delete( 'langlinks', array( 'll_from' => $id ), __METHOD__ );
911 $this->mDb->delete( 'iwlinks', array( 'iwl_from' => $id ), __METHOD__ );
912 $this->mDb->delete( 'redirect', array( 'rd_from' => $id ), __METHOD__ );
913 $this->mDb->delete( 'page_props', array( 'pp_page' => $id ), __METHOD__ );
914 }
915
916 # If using cleanup triggers, we can skip some manual deletes
917 if ( !$this->mDb->cleanupTriggers() ) {
918 # Clean up recentchanges entries...
919 $this->mDb->delete( 'recentchanges',
920 array( 'rc_type != ' . RC_LOG,
921 'rc_namespace' => $title->getNamespace(),
922 'rc_title' => $title->getDBkey() ),
923 __METHOD__ );
924 $this->mDb->delete( 'recentchanges',
925 array( 'rc_type != ' . RC_LOG, 'rc_cur_id' => $id ),
926 __METHOD__ );
927 }
928 }
929
930 /**
931 * Update all the appropriate counts in the category table.
932 * @param array $added Associative array of category name => sort key
933 * @param array $deleted Associative array of category name => sort key
934 */
935 function updateCategoryCounts( $added, $deleted ) {
936 $a = WikiPage::factory( $this->mTitle );
937 $a->updateCategoryCounts(
938 array_keys( $added ), array_keys( $deleted )
939 );
940 }
941 }