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