Explicit variable definition, tweak documentation
[lhc/web/wiklou.git] / includes / RevisionDelete.php
1 <?php
2 /**
3 * Revision/log/file deletion backend
4 *
5 * @file
6 */
7
8 /**
9 * Temporary b/c interface, collection of static functions.
10 * @ingroup SpecialPage
11 */
12 class RevisionDeleter {
13 /**
14 * Checks for a change in the bitfield for a certain option and updates the
15 * provided array accordingly.
16 *
17 * @param $desc String: description to add to the array if the option was
18 * enabled / disabled.
19 * @param $field Integer: the bitmask describing the single option.
20 * @param $diff Integer: the xor of the old and new bitfields.
21 * @param $new Integer: the new bitfield
22 * @param $arr Array: the array to update.
23 */
24 protected static function checkItem( $desc, $field, $diff, $new, &$arr ) {
25 if( $diff & $field ) {
26 $arr[ ( $new & $field ) ? 0 : 1 ][] = $desc;
27 }
28 }
29
30 /**
31 * Gets an array of message keys describing the changes made to the visibility
32 * of the revision. If the resulting array is $arr, then $arr[0] will contain an
33 * array of strings describing the items that were hidden, $arr[2] will contain
34 * an array of strings describing the items that were unhidden, and $arr[3] will
35 * contain an array with a single string, which can be one of "applied
36 * restrictions to sysops", "removed restrictions from sysops", or null.
37 *
38 * @param $n Integer: the new bitfield.
39 * @param $o Integer: the old bitfield.
40 * @return An array as described above.
41 */
42 protected static function getChanges( $n, $o ) {
43 $diff = $n ^ $o;
44 $ret = array( 0 => array(), 1 => array(), 2 => array() );
45 // Build bitfield changes in language
46 self::checkItem( 'revdelete-content',
47 Revision::DELETED_TEXT, $diff, $n, $ret );
48 self::checkItem( 'revdelete-summary',
49 Revision::DELETED_COMMENT, $diff, $n, $ret );
50 self::checkItem( 'revdelete-uname',
51 Revision::DELETED_USER, $diff, $n, $ret );
52 // Restriction application to sysops
53 if( $diff & Revision::DELETED_RESTRICTED ) {
54 if( $n & Revision::DELETED_RESTRICTED )
55 $ret[2][] = 'revdelete-restricted';
56 else
57 $ret[2][] = 'revdelete-unrestricted';
58 }
59 return $ret;
60 }
61
62 /**
63 * Gets a log message to describe the given revision visibility change. This
64 * message will be of the form "[hid {content, edit summary, username}];
65 * [unhid {...}][applied restrictions to sysops] for $count revisions: $comment".
66 *
67 * @param $count Integer: The number of effected revisions.
68 * @param $nbitfield Integer: The new bitfield for the revision.
69 * @param $obitfield Integer: The old bitfield for the revision.
70 * @param $isForLog Boolean
71 * @param $forContent Boolean
72 */
73 public static function getLogMessage( $count, $nbitfield, $obitfield, $isForLog = false, $forContent = false ) {
74 global $wgLang, $wgContLang;
75
76 $lang = $forContent ? $wgContLang : $wgLang;
77 $msgFunc = $forContent ? "wfMsgForContent" : "wfMsg";
78
79 $changes = self::getChanges( $nbitfield, $obitfield );
80 array_walk($changes, 'RevisionDeleter::expandMessageArray', $forContent);
81
82 $changesText = array();
83
84 if( count( $changes[0] ) ) {
85 $changesText[] = $msgFunc( 'revdelete-hid', $lang->commaList( $changes[0] ) );
86 }
87 if( count( $changes[1] ) ) {
88 $changesText[] = $msgFunc( 'revdelete-unhid', $lang->commaList( $changes[1] ) );
89 }
90
91 $s = $lang->semicolonList( $changesText );
92 if( count( $changes[2] ) ) {
93 $s .= $s ? ' (' . $changes[2][0] . ')' : ' ' . $changes[2][0];
94 }
95
96 $msg = $isForLog ? 'logdelete-log-message' : 'revdelete-log-message';
97 return wfMsgExt( $msg, $forContent ? array( 'parsemag', 'content' ) : array( 'parsemag' ), $s, $lang->formatNum($count) );
98 }
99
100 private static function expandMessageArray(& $msg, $key, $forContent) {
101 if ( is_array ($msg) ) {
102 array_walk($msg, 'RevisionDeleter::expandMessageArray', $forContent);
103 } else {
104 if ( $forContent ) {
105 $msg = wfMsgForContent($msg);
106 } else {
107 $msg = wfMsg($msg);
108 }
109 }
110 }
111
112 // Get DB field name for URL param...
113 // Future code for other things may also track
114 // other types of revision-specific changes.
115 // @returns string One of log_id/rev_id/fa_id/ar_timestamp/oi_archive_name
116 public static function getRelationType( $typeName ) {
117 if ( isset( SpecialRevisionDelete::$deprecatedTypeMap[$typeName] ) ) {
118 $typeName = SpecialRevisionDelete::$deprecatedTypeMap[$typeName];
119 }
120 if ( isset( SpecialRevisionDelete::$allowedTypes[$typeName] ) ) {
121 $class = SpecialRevisionDelete::$allowedTypes[$typeName]['list-class'];
122 $list = new $class( null, null, null );
123 return $list->getIdField();
124 } else {
125 return null;
126 }
127 }
128
129 // Checks if a revision still exists in the revision table.
130 // If it doesn't, returns the corresponding ar_timestamp field
131 // so that this key can be used instead.
132 public static function checkRevisionExistence( $title, $revid ) {
133 $dbr = wfGetDB( DB_SLAVE );
134 $exists = $dbr->selectField( 'revision', '1',
135 array( 'rev_id' => $revid ), __METHOD__ );
136
137 if ( $exists ) {
138 return true;
139 }
140
141 $timestamp = $dbr->selectField( 'archive', 'ar_timestamp',
142 array( 'ar_namespace' => $title->getNamespace(),
143 'ar_title' => $title->getDBkey(),
144 'ar_rev_id' => $revid ), __METHOD__ );
145
146 return $timestamp;
147 }
148
149 // Creates utility links for log entries.
150 public static function getLogLinks( $title, $paramArray, $skin, $messages ) {
151 global $wgLang;
152
153 if( count($paramArray) >= 2 ) {
154 // Different revision types use different URL params...
155 $originalKey = $key = $paramArray[0];
156 // $paramArray[1] is a CSV of the IDs
157 $Ids = explode( ',', $paramArray[1] );
158
159 $revert = array();
160
161 // For if undeleted revisions are found amidst deleted ones.
162 $undeletedRevisions = array();
163
164 // This is not going to work if some revs are deleted and some
165 // aren't.
166 if ($key == 'revision') {
167 foreach( $Ids as $k => $id ) {
168 $existResult =
169 self::checkRevisionExistence( $title, $id );
170
171 if ($existResult !== true) {
172 $key = 'archive';
173 $Ids[$k] = $existResult;
174 } else {
175 // Undeleted revision amidst deleted ones
176 unset($Ids[$k]);
177 $undeletedRevisions[] = $id;
178 }
179 }
180
181 if ( $key == $originalKey ) {
182 $Ids = $undeletedRevisions;
183 $undeletedRevisions = array();
184 }
185 }
186
187 // Diff link for single rev deletions
188 if( count($Ids) == 1 && !count($undeletedRevisions) ) {
189 // Live revision diffs...
190 if( in_array( $key, array( 'oldid', 'revision' ) ) ) {
191 $revert[] = $skin->link(
192 $title,
193 $messages['diff'],
194 array(),
195 array(
196 'diff' => intval( $Ids[0] ),
197 'unhide' => 1
198 ),
199 array( 'known', 'noclasses' )
200 );
201 // Deleted revision diffs...
202 } else if( in_array( $key, array( 'artimestamp','archive' ) ) ) {
203 $revert[] = $skin->link(
204 SpecialPage::getTitleFor( 'Undelete' ),
205 $messages['diff'],
206 array(),
207 array(
208 'target' => $title->getPrefixedDBKey(),
209 'diff' => 'prev',
210 'timestamp' => $Ids[0]
211 ),
212 array( 'known', 'noclasses' )
213 );
214 }
215 }
216
217 // View/modify link...
218 if ( count($undeletedRevisions) ) {
219 // FIXME THIS IS A HORRIBLE HORRIBLE HACK AND SHOULD DIE
220 // It's not possible to pass a list of both deleted and
221 // undeleted revisions to SpecialRevisionDelete, so we're
222 // stuck with two links. See bug 23363.
223 $restoreLinks = array();
224
225 $restoreLinks[] = $skin->link(
226 SpecialPage::getTitleFor( 'Revisiondelete' ),
227 $messages['revdel-restore-visible'],
228 array(),
229 array(
230 'target' => $title->getPrefixedText(),
231 'type' => $originalKey,
232 'ids' => implode(',', $undeletedRevisions),
233 ),
234 array( 'known', 'noclasses' )
235 );
236
237 $restoreLinks[] = $skin->link(
238 SpecialPage::getTitleFor( 'Revisiondelete' ),
239 $messages['revdel-restore-deleted'],
240 array(),
241 array(
242 'target' => $title->getPrefixedText(),
243 'type' => $key,
244 'ids' => implode(',', $Ids),
245 ),
246 array( 'known', 'noclasses' )
247 );
248
249 $revert[] = $messages['revdel-restore'] . ' [' .
250 $wgLang->pipeList( $restoreLinks ) . ']';
251 } else {
252 $revert[] = $skin->link(
253 SpecialPage::getTitleFor( 'Revisiondelete' ),
254 $messages['revdel-restore'],
255 array(),
256 array(
257 'target' => $title->getPrefixedText(),
258 'type' => $key,
259 'ids' => implode(',', $Ids),
260 ),
261 array( 'known', 'noclasses' )
262 );
263 }
264
265 // Pipe links
266 return wfMsg( 'parentheses', $wgLang->pipeList( $revert ) );
267 }
268 return '';
269 }
270 }
271
272 /**
273 * Abstract base class for a list of deletable items
274 */
275 abstract class RevDel_List {
276 var $special, $title, $ids, $res, $current;
277 var $type = null; // override this
278 var $idField = null; // override this
279 var $dateField = false; // override this
280 var $authorIdField = false; // override this
281 var $authorNameField = false; // override this
282
283 /**
284 * @param $special The parent SpecialPage
285 * @param $title The target title
286 * @param $ids Array of IDs
287 */
288 public function __construct( $special, $title, $ids ) {
289 $this->special = $special;
290 $this->title = $title;
291 $this->ids = $ids;
292 }
293
294 /**
295 * Get the internal type name of this list. Equal to the table name.
296 */
297 public function getType() {
298 return $this->type;
299 }
300
301 /**
302 * Get the DB field name associated with the ID list
303 */
304 public function getIdField() {
305 return $this->idField;
306 }
307
308 /**
309 * Get the DB field name storing timestamps
310 */
311 public function getTimestampField() {
312 return $this->dateField;
313 }
314
315 /**
316 * Get the DB field name storing user ids
317 */
318 public function getAuthorIdField() {
319 return $this->authorIdField;
320 }
321
322 /**
323 * Get the DB field name storing user names
324 */
325 public function getAuthorNameField() {
326 return $this->authorNameField;
327 }
328 /**
329 * Set the visibility for the revisions in this list. Logging and
330 * transactions are done here.
331 *
332 * @param $params Associative array of parameters. Members are:
333 * value: The integer value to set the visibility to
334 * comment: The log comment.
335 * @return Status
336 */
337 public function setVisibility( $params ) {
338 $bitPars = $params['value'];
339 $comment = $params['comment'];
340
341 $this->res = false;
342 $dbw = wfGetDB( DB_MASTER );
343 $this->doQuery( $dbw );
344 $dbw->begin();
345 $status = Status::newGood();
346 $missing = array_flip( $this->ids );
347 $this->clearFileOps();
348 $idsForLog = array();
349 $authorIds = $authorIPs = array();
350
351 for ( $this->reset(); $this->current(); $this->next() ) {
352 $item = $this->current();
353 unset( $missing[ $item->getId() ] );
354
355 $oldBits = $item->getBits();
356 // Build the actual new rev_deleted bitfield
357 $newBits = SpecialRevisionDelete::extractBitfield( $bitPars, $oldBits );
358
359 if ( $oldBits == $newBits ) {
360 $status->warning( 'revdelete-no-change', $item->formatDate(), $item->formatTime() );
361 $status->failCount++;
362 continue;
363 } elseif ( $oldBits == 0 && $newBits != 0 ) {
364 $opType = 'hide';
365 } elseif ( $oldBits != 0 && $newBits == 0 ) {
366 $opType = 'show';
367 } else {
368 $opType = 'modify';
369 }
370
371 if ( $item->isHideCurrentOp( $newBits ) ) {
372 // Cannot hide current version text
373 $status->error( 'revdelete-hide-current', $item->formatDate(), $item->formatTime() );
374 $status->failCount++;
375 continue;
376 }
377 if ( !$item->canView() ) {
378 // Cannot access this revision
379 $msg = ($opType == 'show') ?
380 'revdelete-show-no-access' : 'revdelete-modify-no-access';
381 $status->error( $msg, $item->formatDate(), $item->formatTime() );
382 $status->failCount++;
383 continue;
384 }
385 // Cannot just "hide from Sysops" without hiding any fields
386 if( $newBits == Revision::DELETED_RESTRICTED ) {
387 $status->warning( 'revdelete-only-restricted', $item->formatDate(), $item->formatTime() );
388 $status->failCount++;
389 continue;
390 }
391
392 // Update the revision
393 $ok = $item->setBits( $newBits );
394
395 if ( $ok ) {
396 $idsForLog[] = $item->getId();
397 $status->successCount++;
398 if( $item->getAuthorId() > 0 ) {
399 $authorIds[] = $item->getAuthorId();
400 } else if( IP::isIPAddress( $item->getAuthorName() ) ) {
401 $authorIPs[] = $item->getAuthorName();
402 }
403 } else {
404 $status->error( 'revdelete-concurrent-change', $item->formatDate(), $item->formatTime() );
405 $status->failCount++;
406 }
407 }
408
409 // Handle missing revisions
410 foreach ( $missing as $id => $unused ) {
411 $status->error( 'revdelete-modify-missing', $id );
412 $status->failCount++;
413 }
414
415 if ( $status->successCount == 0 ) {
416 $status->ok = false;
417 $dbw->rollback();
418 return $status;
419 }
420
421 // Save success count
422 $successCount = $status->successCount;
423
424 // Move files, if there are any
425 $status->merge( $this->doPreCommitUpdates() );
426 if ( !$status->isOK() ) {
427 // Fatal error, such as no configured archive directory
428 $dbw->rollback();
429 return $status;
430 }
431
432 // Log it
433 $this->updateLog( array(
434 'title' => $this->title,
435 'count' => $successCount,
436 'newBits' => $newBits,
437 'oldBits' => $oldBits,
438 'comment' => $comment,
439 'ids' => $idsForLog,
440 'authorIds' => $authorIds,
441 'authorIPs' => $authorIPs
442 ) );
443 $dbw->commit();
444
445 // Clear caches
446 $status->merge( $this->doPostCommitUpdates() );
447 return $status;
448 }
449
450 /**
451 * Reload the list data from the master DB. This can be done after setVisibility()
452 * to allow $item->getHTML() to show the new data.
453 */
454 function reloadFromMaster() {
455 $dbw = wfGetDB( DB_MASTER );
456 $this->res = $this->doQuery( $dbw );
457 }
458
459 /**
460 * Record a log entry on the action
461 * @param $params Associative array of parameters:
462 * newBits: The new value of the *_deleted bitfield
463 * oldBits: The old value of the *_deleted bitfield.
464 * title: The target title
465 * ids: The ID list
466 * comment: The log comment
467 * authorsIds: The array of the user IDs of the offenders
468 * authorsIPs: The array of the IP/anon user offenders
469 */
470 protected function updateLog( $params ) {
471 // Get the URL param's corresponding DB field
472 $field = RevisionDeleter::getRelationType( $this->getType() );
473 if( !$field ) {
474 throw new MWException( "Bad log URL param type!" );
475 }
476 // Put things hidden from sysops in the oversight log
477 if ( ( $params['newBits'] | $params['oldBits'] ) & $this->getSuppressBit() ) {
478 $logType = 'suppress';
479 } else {
480 $logType = 'delete';
481 }
482 // Add params for effected page and ids
483 $logParams = $this->getLogParams( $params );
484 // Actually add the deletion log entry
485 $log = new LogPage( $logType );
486 $logid = $log->addEntry( $this->getLogAction(), $params['title'],
487 $params['comment'], $logParams );
488 // Allow for easy searching of deletion log items for revision/log items
489 $log->addRelations( $field, $params['ids'], $logid );
490 $log->addRelations( 'target_author_id', $params['authorIds'], $logid );
491 $log->addRelations( 'target_author_ip', $params['authorIPs'], $logid );
492 }
493
494 /**
495 * Get the log action for this list type
496 */
497 public function getLogAction() {
498 return 'revision';
499 }
500
501 /**
502 * Get log parameter array.
503 * @param $params Associative array of log parameters, same as updateLog()
504 * @return array
505 */
506 public function getLogParams( $params ) {
507 return array(
508 $this->getType(),
509 implode( ',', $params['ids'] ),
510 "ofield={$params['oldBits']}",
511 "nfield={$params['newBits']}"
512 );
513 }
514
515 /**
516 * Initialise the current iteration pointer
517 */
518 protected function initCurrent() {
519 $row = $this->res->current();
520 if ( $row ) {
521 $this->current = $this->newItem( $row );
522 } else {
523 $this->current = false;
524 }
525 }
526
527 /**
528 * Start iteration. This must be called before current() or next().
529 * @return First list item
530 */
531 public function reset() {
532 if ( !$this->res ) {
533 $this->res = $this->doQuery( wfGetDB( DB_SLAVE ) );
534 } else {
535 $this->res->rewind();
536 }
537 $this->initCurrent();
538 return $this->current;
539 }
540
541 /**
542 * Get the current list item, or false if we are at the end
543 */
544 public function current() {
545 return $this->current;
546 }
547
548 /**
549 * Move the iteration pointer to the next list item, and return it.
550 */
551 public function next() {
552 $this->res->next();
553 $this->initCurrent();
554 return $this->current;
555 }
556
557 /**
558 * Get the number of items in the list.
559 */
560 public function length() {
561 if( !$this->res ) {
562 return 0;
563 } else {
564 return $this->res->numRows();
565 }
566 }
567
568 /**
569 * Clear any data structures needed for doPreCommitUpdates() and doPostCommitUpdates()
570 * STUB
571 */
572 public function clearFileOps() {
573 }
574
575 /**
576 * A hook for setVisibility(): do batch updates pre-commit.
577 * STUB
578 * @return Status
579 */
580 public function doPreCommitUpdates() {
581 return Status::newGood();
582 }
583
584 /**
585 * A hook for setVisibility(): do any necessary updates post-commit.
586 * STUB
587 * @return Status
588 */
589 public function doPostCommitUpdates() {
590 return Status::newGood();
591 }
592
593 /**
594 * Create an item object from a DB result row
595 * @param $row stdclass
596 */
597 abstract public function newItem( $row );
598
599 /**
600 * Do the DB query to iterate through the objects.
601 * @param $db Database object to use for the query
602 */
603 abstract public function doQuery( $db );
604
605 /**
606 * Get the integer value of the flag used for suppression
607 */
608 abstract public function getSuppressBit();
609 }
610
611 /**
612 * Abstract base class for deletable items
613 */
614 abstract class RevDel_Item {
615 /** The parent SpecialPage */
616 var $special;
617
618 /** The parent RevDel_List */
619 var $list;
620
621 /** The DB result row */
622 var $row;
623
624 /**
625 * @param $list RevDel_List
626 * @param $row DB result row
627 */
628 public function __construct( $list, $row ) {
629 $this->special = $list->special;
630 $this->list = $list;
631 $this->row = $row;
632 }
633
634 /**
635 * Get the ID, as it would appear in the ids URL parameter
636 */
637 public function getId() {
638 $field = $this->list->getIdField();
639 return $this->row->$field;
640 }
641
642 /**
643 * Get the date, formatted with $wgLang
644 */
645 public function formatDate() {
646 global $wgLang;
647 return $wgLang->date( $this->getTimestamp() );
648 }
649
650 /**
651 * Get the time, formatted with $wgLang
652 */
653 public function formatTime() {
654 global $wgLang;
655 return $wgLang->time( $this->getTimestamp() );
656 }
657
658 /**
659 * Get the timestamp in MW 14-char form
660 */
661 public function getTimestamp() {
662 $field = $this->list->getTimestampField();
663 return wfTimestamp( TS_MW, $this->row->$field );
664 }
665
666 /**
667 * Get the author user ID
668 */
669 public function getAuthorId() {
670 $field = $this->list->getAuthorIdField();
671 return intval( $this->row->$field );
672 }
673
674 /**
675 * Get the author user name
676 */
677 public function getAuthorName() {
678 $field = $this->list->getAuthorNameField();
679 return strval( $this->row->$field );
680 }
681
682 /**
683 * Returns true if the item is "current", and the operation to set the given
684 * bits can't be executed for that reason
685 * STUB
686 */
687 public function isHideCurrentOp( $newBits ) {
688 return false;
689 }
690
691 /**
692 * Returns true if the current user can view the item
693 */
694 abstract public function canView();
695
696 /**
697 * Returns true if the current user can view the item text/file
698 */
699 abstract public function canViewContent();
700
701 /**
702 * Get the current deletion bitfield value
703 */
704 abstract public function getBits();
705
706 /**
707 * Get the HTML of the list item. Should be include <li></li> tags.
708 * This is used to show the list in HTML form, by the special page.
709 */
710 abstract public function getHTML();
711
712 /**
713 * Set the visibility of the item. This should do any necessary DB queries.
714 *
715 * The DB update query should have a condition which forces it to only update
716 * if the value in the DB matches the value fetched earlier with the SELECT.
717 * If the update fails because it did not match, the function should return
718 * false. This prevents concurrency problems.
719 *
720 * @return boolean success
721 */
722 abstract public function setBits( $newBits );
723 }
724
725 /**
726 * List for revision table items
727 */
728 class RevDel_RevisionList extends RevDel_List {
729 var $currentRevId;
730 var $type = 'revision';
731 var $idField = 'rev_id';
732 var $dateField = 'rev_timestamp';
733 var $authorIdField = 'rev_user';
734 var $authorNameField = 'rev_user_text';
735
736 public function doQuery( $db ) {
737 $ids = array_map( 'intval', $this->ids );
738 return $db->select( array('revision','page'), '*',
739 array(
740 'rev_page' => $this->title->getArticleID(),
741 'rev_id' => $ids,
742 'rev_page = page_id'
743 ),
744 __METHOD__,
745 array( 'ORDER BY' => 'rev_id DESC' )
746 );
747 }
748
749 public function newItem( $row ) {
750 return new RevDel_RevisionItem( $this, $row );
751 }
752
753 public function getCurrent() {
754 if ( is_null( $this->currentRevId ) ) {
755 $dbw = wfGetDB( DB_MASTER );
756 $this->currentRevId = $dbw->selectField(
757 'page', 'page_latest', $this->title->pageCond(), __METHOD__ );
758 }
759 return $this->currentRevId;
760 }
761
762 public function getSuppressBit() {
763 return Revision::DELETED_RESTRICTED;
764 }
765
766 public function doPreCommitUpdates() {
767 $this->title->invalidateCache();
768 return Status::newGood();
769 }
770
771 public function doPostCommitUpdates() {
772 $this->title->purgeSquid();
773 // Extensions that require referencing previous revisions may need this
774 wfRunHooks( 'ArticleRevisionVisibilitySet', array( &$this->title ) );
775 return Status::newGood();
776 }
777 }
778
779 /**
780 * Item class for a revision table row
781 */
782 class RevDel_RevisionItem extends RevDel_Item {
783 var $revision;
784
785 public function __construct( $list, $row ) {
786 parent::__construct( $list, $row );
787 $this->revision = new Revision( $row );
788 }
789
790 public function canView() {
791 return $this->revision->userCan( Revision::DELETED_RESTRICTED );
792 }
793
794 public function canViewContent() {
795 return $this->revision->userCan( Revision::DELETED_TEXT );
796 }
797
798 public function getBits() {
799 return $this->revision->mDeleted;
800 }
801
802 public function setBits( $bits ) {
803 $dbw = wfGetDB( DB_MASTER );
804 // Update revision table
805 $dbw->update( 'revision',
806 array( 'rev_deleted' => $bits ),
807 array(
808 'rev_id' => $this->revision->getId(),
809 'rev_page' => $this->revision->getPage(),
810 'rev_deleted' => $this->getBits()
811 ),
812 __METHOD__
813 );
814 if ( !$dbw->affectedRows() ) {
815 // Concurrent fail!
816 return false;
817 }
818 // Update recentchanges table
819 $dbw->update( 'recentchanges',
820 array(
821 'rc_deleted' => $bits,
822 'rc_patrolled' => 1
823 ),
824 array(
825 'rc_this_oldid' => $this->revision->getId(), // condition
826 // non-unique timestamp index
827 'rc_timestamp' => $dbw->timestamp( $this->revision->getTimestamp() ),
828 ),
829 __METHOD__
830 );
831 return true;
832 }
833
834 public function isDeleted() {
835 return $this->revision->isDeleted( Revision::DELETED_TEXT );
836 }
837
838 public function isHideCurrentOp( $newBits ) {
839 return ( $newBits & Revision::DELETED_TEXT )
840 && $this->list->getCurrent() == $this->getId();
841 }
842
843 /**
844 * Get the HTML link to the revision text.
845 * Overridden by RevDel_ArchiveItem.
846 */
847 protected function getRevisionLink() {
848 global $wgLang;
849 $date = $wgLang->timeanddate( $this->revision->getTimestamp(), true );
850 if ( $this->isDeleted() && !$this->canViewContent() ) {
851 return $date;
852 }
853 return $this->special->skin->link(
854 $this->list->title,
855 $date,
856 array(),
857 array(
858 'oldid' => $this->revision->getId(),
859 'unhide' => 1
860 )
861 );
862 }
863
864 /**
865 * Get the HTML link to the diff.
866 * Overridden by RevDel_ArchiveItem
867 */
868 protected function getDiffLink() {
869 if ( $this->isDeleted() && !$this->canViewContent() ) {
870 return wfMsgHtml('diff');
871 } else {
872 return
873 $this->special->skin->link(
874 $this->list->title,
875 wfMsgHtml('diff'),
876 array(),
877 array(
878 'diff' => $this->revision->getId(),
879 'oldid' => 'prev',
880 'unhide' => 1
881 ),
882 array(
883 'known',
884 'noclasses'
885 )
886 );
887 }
888 }
889
890 public function getHTML() {
891 $difflink = $this->getDiffLink();
892 $revlink = $this->getRevisionLink();
893 $userlink = $this->special->skin->revUserLink( $this->revision );
894 $comment = $this->special->skin->revComment( $this->revision );
895 if ( $this->isDeleted() ) {
896 $revlink = "<span class=\"history-deleted\">$revlink</span>";
897 }
898 return "<li>($difflink) $revlink $userlink $comment</li>";
899 }
900 }
901
902 /**
903 * List for archive table items, i.e. revisions deleted via action=delete
904 */
905 class RevDel_ArchiveList extends RevDel_RevisionList {
906 var $type = 'archive';
907 var $idField = 'ar_timestamp';
908 var $dateField = 'ar_timestamp';
909 var $authorIdField = 'ar_user';
910 var $authorNameField = 'ar_user_text';
911
912 public function doQuery( $db ) {
913 $timestamps = array();
914 foreach ( $this->ids as $id ) {
915 $timestamps[] = $db->timestamp( $id );
916 }
917 return $db->select( 'archive', '*',
918 array(
919 'ar_namespace' => $this->title->getNamespace(),
920 'ar_title' => $this->title->getDBkey(),
921 'ar_timestamp' => $timestamps
922 ),
923 __METHOD__,
924 array( 'ORDER BY' => 'ar_timestamp DESC' )
925 );
926 }
927
928 public function newItem( $row ) {
929 return new RevDel_ArchiveItem( $this, $row );
930 }
931
932 public function doPreCommitUpdates() {
933 return Status::newGood();
934 }
935
936 public function doPostCommitUpdates() {
937 return Status::newGood();
938 }
939 }
940
941 /**
942 * Item class for a archive table row
943 */
944 class RevDel_ArchiveItem extends RevDel_RevisionItem {
945 public function __construct( $list, $row ) {
946 parent::__construct( $list, $row );
947 $this->revision = Revision::newFromArchiveRow( $row,
948 array( 'page' => $this->list->title->getArticleId() ) );
949 }
950
951 public function getId() {
952 # Convert DB timestamp to MW timestamp
953 return $this->revision->getTimestamp();
954 }
955
956 public function setBits( $bits ) {
957 $dbw = wfGetDB( DB_MASTER );
958 $dbw->update( 'archive',
959 array( 'ar_deleted' => $bits ),
960 array( 'ar_namespace' => $this->list->title->getNamespace(),
961 'ar_title' => $this->list->title->getDBkey(),
962 // use timestamp for index
963 'ar_timestamp' => $this->row->ar_timestamp,
964 'ar_rev_id' => $this->row->ar_rev_id,
965 'ar_deleted' => $this->getBits()
966 ),
967 __METHOD__ );
968 return (bool)$dbw->affectedRows();
969 }
970
971 protected function getRevisionLink() {
972 global $wgLang;
973 $undelete = SpecialPage::getTitleFor( 'Undelete' );
974 $date = $wgLang->timeanddate( $this->revision->getTimestamp(), true );
975 if ( $this->isDeleted() && !$this->canViewContent() ) {
976 return $date;
977 }
978 return $this->special->skin->link( $undelete, $date, array(),
979 array(
980 'target' => $this->list->title->getPrefixedText(),
981 'timestamp' => $this->revision->getTimestamp()
982 ) );
983 }
984
985 protected function getDiffLink() {
986 if ( $this->isDeleted() && !$this->canViewContent() ) {
987 return wfMsgHtml( 'diff' );
988 }
989 $undelete = SpecialPage::getTitleFor( 'Undelete' );
990 return $this->special->skin->link( $undelete, wfMsgHtml('diff'), array(),
991 array(
992 'target' => $this->list->title->getPrefixedText(),
993 'diff' => 'prev',
994 'timestamp' => $this->revision->getTimestamp()
995 ) );
996 }
997 }
998
999 /**
1000 * List for oldimage table items
1001 */
1002 class RevDel_FileList extends RevDel_List {
1003 var $type = 'oldimage';
1004 var $idField = 'oi_archive_name';
1005 var $dateField = 'oi_timestamp';
1006 var $authorIdField = 'oi_user';
1007 var $authorNameField = 'oi_user_text';
1008 var $storeBatch, $deleteBatch, $cleanupBatch;
1009
1010 public function doQuery( $db ) {
1011 $archiveNames = array();
1012 foreach( $this->ids as $timestamp ) {
1013 $archiveNames[] = $timestamp . '!' . $this->title->getDBkey();
1014 }
1015 return $db->select( 'oldimage', '*',
1016 array(
1017 'oi_name' => $this->title->getDBkey(),
1018 'oi_archive_name' => $archiveNames
1019 ),
1020 __METHOD__,
1021 array( 'ORDER BY' => 'oi_timestamp DESC' )
1022 );
1023 }
1024
1025 public function newItem( $row ) {
1026 return new RevDel_FileItem( $this, $row );
1027 }
1028
1029 public function clearFileOps() {
1030 $this->deleteBatch = array();
1031 $this->storeBatch = array();
1032 $this->cleanupBatch = array();
1033 }
1034
1035 public function doPreCommitUpdates() {
1036 $status = Status::newGood();
1037 $repo = RepoGroup::singleton()->getLocalRepo();
1038 if ( $this->storeBatch ) {
1039 $status->merge( $repo->storeBatch( $this->storeBatch, FileRepo::OVERWRITE_SAME ) );
1040 }
1041 if ( !$status->isOK() ) {
1042 return $status;
1043 }
1044 if ( $this->deleteBatch ) {
1045 $status->merge( $repo->deleteBatch( $this->deleteBatch ) );
1046 }
1047 if ( !$status->isOK() ) {
1048 // Running cleanupDeletedBatch() after a failed storeBatch() with the DB already
1049 // modified (but destined for rollback) causes data loss
1050 return $status;
1051 }
1052 if ( $this->cleanupBatch ) {
1053 $status->merge( $repo->cleanupDeletedBatch( $this->cleanupBatch ) );
1054 }
1055 return $status;
1056 }
1057
1058 public function doPostCommitUpdates() {
1059 $file = wfLocalFile( $this->title );
1060 $file->purgeCache();
1061 $file->purgeDescription();
1062 return Status::newGood();
1063 }
1064
1065 public function getSuppressBit() {
1066 return File::DELETED_RESTRICTED;
1067 }
1068 }
1069
1070 /**
1071 * Item class for an oldimage table row
1072 */
1073 class RevDel_FileItem extends RevDel_Item {
1074 var $file;
1075
1076 public function __construct( $list, $row ) {
1077 parent::__construct( $list, $row );
1078 $this->file = RepoGroup::singleton()->getLocalRepo()->newFileFromRow( $row );
1079 }
1080
1081 public function getId() {
1082 $parts = explode( '!', $this->row->oi_archive_name );
1083 return $parts[0];
1084 }
1085
1086 public function canView() {
1087 return $this->file->userCan( File::DELETED_RESTRICTED );
1088 }
1089
1090 public function canViewContent() {
1091 return $this->file->userCan( File::DELETED_FILE );
1092 }
1093
1094 public function getBits() {
1095 return $this->file->getVisibility();
1096 }
1097
1098 public function setBits( $bits ) {
1099 # Queue the file op
1100 # FIXME: move to LocalFile.php
1101 if ( $this->isDeleted() ) {
1102 if ( $bits & File::DELETED_FILE ) {
1103 # Still deleted
1104 } else {
1105 # Newly undeleted
1106 $key = $this->file->getStorageKey();
1107 $srcRel = $this->file->repo->getDeletedHashPath( $key ) . $key;
1108 $this->list->storeBatch[] = array(
1109 $this->file->repo->getVirtualUrl( 'deleted' ) . '/' . $srcRel,
1110 'public',
1111 $this->file->getRel()
1112 );
1113 $this->list->cleanupBatch[] = $key;
1114 }
1115 } elseif ( $bits & File::DELETED_FILE ) {
1116 # Newly deleted
1117 $key = $this->file->getStorageKey();
1118 $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key;
1119 $this->list->deleteBatch[] = array( $this->file->getRel(), $dstRel );
1120 }
1121
1122 # Do the database operations
1123 $dbw = wfGetDB( DB_MASTER );
1124 $dbw->update( 'oldimage',
1125 array( 'oi_deleted' => $bits ),
1126 array(
1127 'oi_name' => $this->row->oi_name,
1128 'oi_timestamp' => $this->row->oi_timestamp,
1129 'oi_deleted' => $this->getBits()
1130 ),
1131 __METHOD__
1132 );
1133 return (bool)$dbw->affectedRows();
1134 }
1135
1136 public function isDeleted() {
1137 return $this->file->isDeleted( File::DELETED_FILE );
1138 }
1139
1140 /**
1141 * Get the link to the file.
1142 * Overridden by RevDel_ArchivedFileItem.
1143 */
1144 protected function getLink() {
1145 global $wgLang, $wgUser;
1146 $date = $wgLang->timeanddate( $this->file->getTimestamp(), true );
1147 if ( $this->isDeleted() ) {
1148 # Hidden files...
1149 if ( !$this->canViewContent() ) {
1150 $link = $date;
1151 } else {
1152 $link = $this->special->skin->link(
1153 $this->special->getTitle(),
1154 $date, array(),
1155 array(
1156 'target' => $this->list->title->getPrefixedText(),
1157 'file' => $this->file->getArchiveName(),
1158 'token' => $wgUser->editToken( $this->file->getArchiveName() )
1159 )
1160 );
1161 }
1162 return '<span class="history-deleted">' . $link . '</span>';
1163 } else {
1164 # Regular files...
1165 return Xml::element( 'a', array( 'href' => $this->file->getUrl() ), $date );
1166 }
1167 }
1168 /**
1169 * Generate a user tool link cluster if the current user is allowed to view it
1170 * @return string HTML
1171 */
1172 protected function getUserTools() {
1173 if( $this->file->userCan( Revision::DELETED_USER ) ) {
1174 $link = $this->special->skin->userLink( $this->file->user, $this->file->user_text ) .
1175 $this->special->skin->userToolLinks( $this->file->user, $this->file->user_text );
1176 } else {
1177 $link = wfMsgHtml( 'rev-deleted-user' );
1178 }
1179 if( $this->file->isDeleted( Revision::DELETED_USER ) ) {
1180 return '<span class="history-deleted">' . $link . '</span>';
1181 }
1182 return $link;
1183 }
1184
1185 /**
1186 * Wrap and format the file's comment block, if the current
1187 * user is allowed to view it.
1188 *
1189 * @return string HTML
1190 */
1191 protected function getComment() {
1192 if( $this->file->userCan( File::DELETED_COMMENT ) ) {
1193 $block = $this->special->skin->commentBlock( $this->file->description );
1194 } else {
1195 $block = ' ' . wfMsgHtml( 'rev-deleted-comment' );
1196 }
1197 if( $this->file->isDeleted( File::DELETED_COMMENT ) ) {
1198 return "<span class=\"history-deleted\">$block</span>";
1199 }
1200 return $block;
1201 }
1202
1203 public function getHTML() {
1204 global $wgLang;
1205 $data =
1206 wfMsg(
1207 'widthheight',
1208 $wgLang->formatNum( $this->file->getWidth() ),
1209 $wgLang->formatNum( $this->file->getHeight() )
1210 ) .
1211 ' (' .
1212 wfMsgExt( 'nbytes', 'parsemag', $wgLang->formatNum( $this->file->getSize() ) ) .
1213 ')';
1214
1215 return '<li>' . $this->getLink() . ' ' . $this->getUserTools() . ' ' .
1216 $data . ' ' . $this->getComment(). '</li>';
1217 }
1218 }
1219
1220 /**
1221 * List for filearchive table items
1222 */
1223 class RevDel_ArchivedFileList extends RevDel_FileList {
1224 var $type = 'filearchive';
1225 var $idField = 'fa_id';
1226 var $dateField = 'fa_timestamp';
1227 var $authorIdField = 'fa_user';
1228 var $authorNameField = 'fa_user_text';
1229
1230 public function doQuery( $db ) {
1231 $ids = array_map( 'intval', $this->ids );
1232 return $db->select( 'filearchive', '*',
1233 array(
1234 'fa_name' => $this->title->getDBkey(),
1235 'fa_id' => $ids
1236 ),
1237 __METHOD__,
1238 array( 'ORDER BY' => 'fa_id DESC' )
1239 );
1240 }
1241
1242 public function newItem( $row ) {
1243 return new RevDel_ArchivedFileItem( $this, $row );
1244 }
1245 }
1246
1247 /**
1248 * Item class for a filearchive table row
1249 */
1250 class RevDel_ArchivedFileItem extends RevDel_FileItem {
1251 public function __construct( $list, $row ) {
1252 parent::__construct( $list, $row );
1253 $this->file = ArchivedFile::newFromRow( $row );
1254 }
1255
1256 public function getId() {
1257 return $this->row->fa_id;
1258 }
1259
1260 public function setBits( $bits ) {
1261 $dbw = wfGetDB( DB_MASTER );
1262 $dbw->update( 'filearchive',
1263 array( 'fa_deleted' => $bits ),
1264 array(
1265 'fa_id' => $this->row->fa_id,
1266 'fa_deleted' => $this->getBits(),
1267 ),
1268 __METHOD__
1269 );
1270 return (bool)$dbw->affectedRows();
1271 }
1272
1273 protected function getLink() {
1274 global $wgLang, $wgUser;
1275 $date = $wgLang->timeanddate( $this->file->getTimestamp(), true );
1276 $undelete = SpecialPage::getTitleFor( 'Undelete' );
1277 $key = $this->file->getKey();
1278 # Hidden files...
1279 if( !$this->canViewContent() ) {
1280 $link = $date;
1281 } else {
1282 $link = $this->special->skin->link( $undelete, $date, array(),
1283 array(
1284 'target' => $this->list->title->getPrefixedText(),
1285 'file' => $key,
1286 'token' => $wgUser->editToken( $key )
1287 )
1288 );
1289 }
1290 if( $this->isDeleted() ) {
1291 $link = '<span class="history-deleted">' . $link . '</span>';
1292 }
1293 return $link;
1294 }
1295 }
1296
1297 /**
1298 * List for logging table items
1299 */
1300 class RevDel_LogList extends RevDel_List {
1301 var $type = 'logging';
1302 var $idField = 'log_id';
1303 var $dateField = 'log_timestamp';
1304 var $authorIdField = 'log_user';
1305 var $authorNameField = 'log_user_text';
1306
1307 public function doQuery( $db ) {
1308 $ids = array_map( 'intval', $this->ids );
1309 return $db->select( 'logging', '*',
1310 array( 'log_id' => $ids ),
1311 __METHOD__,
1312 array( 'ORDER BY' => 'log_id DESC' )
1313 );
1314 }
1315
1316 public function newItem( $row ) {
1317 return new RevDel_LogItem( $this, $row );
1318 }
1319
1320 public function getSuppressBit() {
1321 return Revision::DELETED_RESTRICTED;
1322 }
1323
1324 public function getLogAction() {
1325 return 'event';
1326 }
1327
1328 public function getLogParams( $params ) {
1329 return array(
1330 implode( ',', $params['ids'] ),
1331 "ofield={$params['oldBits']}",
1332 "nfield={$params['newBits']}"
1333 );
1334 }
1335 }
1336
1337 /**
1338 * Item class for a logging table row
1339 */
1340 class RevDel_LogItem extends RevDel_Item {
1341 public function canView() {
1342 return LogEventsList::userCan( $this->row, Revision::DELETED_RESTRICTED );
1343 }
1344
1345 public function canViewContent() {
1346 return true; // none
1347 }
1348
1349 public function getBits() {
1350 return $this->row->log_deleted;
1351 }
1352
1353 public function setBits( $bits ) {
1354 $dbw = wfGetDB( DB_MASTER );
1355 $dbw->update( 'recentchanges',
1356 array(
1357 'rc_deleted' => $bits,
1358 'rc_patrolled' => 1
1359 ),
1360 array(
1361 'rc_logid' => $this->row->log_id,
1362 'rc_timestamp' => $this->row->log_timestamp // index
1363 ),
1364 __METHOD__
1365 );
1366 $dbw->update( 'logging',
1367 array( 'log_deleted' => $bits ),
1368 array(
1369 'log_id' => $this->row->log_id,
1370 'log_deleted' => $this->getBits()
1371 ),
1372 __METHOD__
1373 );
1374 return (bool)$dbw->affectedRows();
1375 }
1376
1377 public function getHTML() {
1378 global $wgLang;
1379
1380 $date = htmlspecialchars( $wgLang->timeanddate( $this->row->log_timestamp ) );
1381 $paramArray = LogPage::extractParams( $this->row->log_params );
1382 $title = Title::makeTitle( $this->row->log_namespace, $this->row->log_title );
1383
1384 // Log link for this page
1385 $loglink = $this->special->skin->link(
1386 SpecialPage::getTitleFor( 'Log' ),
1387 wfMsgHtml( 'log' ),
1388 array(),
1389 array( 'page' => $title->getPrefixedText() )
1390 );
1391 // Action text
1392 if( !$this->canView() ) {
1393 $action = '<span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>';
1394 } else {
1395 $action = LogPage::actionText( $this->row->log_type, $this->row->log_action, $title,
1396 $this->special->skin, $paramArray, true, true );
1397 if( $this->row->log_deleted & LogPage::DELETED_ACTION )
1398 $action = '<span class="history-deleted">' . $action . '</span>';
1399 }
1400 // User links
1401 $userLink = $this->special->skin->userLink( $this->row->log_user,
1402 User::WhoIs( $this->row->log_user ) );
1403 if( LogEventsList::isDeleted($this->row,LogPage::DELETED_USER) ) {
1404 $userLink = '<span class="history-deleted">' . $userLink . '</span>';
1405 }
1406 // Comment
1407 $comment = $wgLang->getDirMark() . $this->special->skin->commentBlock( $this->row->log_comment );
1408 if( LogEventsList::isDeleted($this->row,LogPage::DELETED_COMMENT) ) {
1409 $comment = '<span class="history-deleted">' . $comment . '</span>';
1410 }
1411 return "<li>($loglink) $date $userLink $action $comment</li>";
1412 }
1413 }