085c8e5ea7f339f38456978a9ead58dab7daa7fe
[lhc/web/wiklou.git] / includes / specials / SpecialRevisiondelete.php
1 <?php
2 /**
3 * Special page allowing users with the appropriate permissions to view
4 * and hide revisions. Log items can also be hidden.
5 *
6 * @file
7 * @ingroup SpecialPage
8 */
9
10 class SpecialRevisionDelete extends UnlistedSpecialPage {
11 /** Skin object */
12 var $skin;
13
14 /** True if the submit button was clicked, and the form was posted */
15 var $submitClicked;
16
17 /** Target ID list */
18 var $ids;
19
20 /** Archive name, for reviewing deleted files */
21 var $archiveName;
22
23 /** Edit token for securing image views against XSS */
24 var $token;
25
26 /** Title object for target parameter */
27 var $targetObj;
28
29 /** Deletion type, may be revision, archive, oldimage, filearchive, logging. */
30 var $typeName;
31
32 /** Array of checkbox specs (message, name, deletion bits) */
33 var $checks;
34
35 /** Information about the current type */
36 var $typeInfo;
37
38 /** The RevDel_List object, storing the list of items to be deleted/undeleted */
39 var $list;
40
41 /**
42 * Assorted information about each type, needed by the special page.
43 * TODO Move some of this to the list class
44 */
45 static $allowedTypes = array(
46 'revision' => array(
47 'check-label' => 'revdelete-hide-text',
48 'deletion-bits' => Revision::DELETED_TEXT,
49 'success' => 'revdelete-success',
50 'failure' => 'revdelete-failure',
51 'list-class' => 'RevDel_RevisionList',
52 ),
53 'archive' => array(
54 'check-label' => 'revdelete-hide-text',
55 'deletion-bits' => Revision::DELETED_TEXT,
56 'success' => 'revdelete-success',
57 'failure' => 'revdelete-failure',
58 'list-class' => 'RevDel_ArchiveList',
59 ),
60 'oldimage'=> array(
61 'check-label' => 'revdelete-hide-image',
62 'deletion-bits' => File::DELETED_FILE,
63 'success' => 'revdelete-success',
64 'failure' => 'revdelete-failure',
65 'list-class' => 'RevDel_FileList',
66 ),
67 'filearchive' => array(
68 'check-label' => 'revdelete-hide-image',
69 'deletion-bits' => File::DELETED_FILE,
70 'success' => 'revdelete-success',
71 'failure' => 'revdelete-failure',
72 'list-class' => 'RevDel_ArchivedFileList',
73 ),
74 'logging' => array(
75 'check-label' => 'revdelete-hide-name',
76 'deletion-bits' => LogPage::DELETED_ACTION,
77 'success' => 'logdelete-success',
78 'failure' => 'logdelete-failure',
79 'list-class' => 'RevDel_LogList',
80 ),
81 );
82
83 /** Type map to support old log entries */
84 static $deprecatedTypeMap = array(
85 'oldid' => 'revision',
86 'artimestamp' => 'archive',
87 'oldimage' => 'oldimage',
88 'fileid' => 'filearchive',
89 'logid' => 'logging',
90 );
91
92 public function __construct() {
93 parent::__construct( 'Revisiondelete', 'deletedhistory' );
94 }
95
96 public function execute( $par ) {
97 global $wgOut, $wgUser, $wgRequest;
98 if( !$wgUser->isAllowed( 'deletedhistory' ) ) {
99 $wgOut->permissionRequired( 'deletedhistory' );
100 return;
101 } else if( wfReadOnly() ) {
102 $wgOut->readOnlyPage();
103 return;
104 }
105 $this->mIsAllowed = $wgUser->isAllowed('deleterevision'); // for changes
106 $this->skin = $wgUser->getSkin();
107 $this->setHeaders();
108 $this->outputHeader();
109 $this->submitClicked = $wgRequest->wasPosted() && $wgRequest->getBool( 'wpSubmit' );
110 # Handle our many different possible input types.
111 $ids = $wgRequest->getVal( 'ids' );
112 if ( !is_null( $ids ) ) {
113 # Allow CSV, for backwards compatibility, or a single ID for show/hide links
114 $this->ids = explode( ',', $ids );
115 } else {
116 # Array input
117 $this->ids = array_keys( $wgRequest->getArray('ids',array()) );
118 }
119 // $this->ids = array_map( 'intval', $this->ids );
120 $this->ids = array_unique( array_filter( $this->ids ) );
121
122 if ( $wgRequest->getVal( 'action' ) == 'historysubmit' ) {
123 # For show/hide form submission from history page
124 $this->targetObj = $GLOBALS['wgTitle'];
125 $this->typeName = 'revision';
126 } else {
127 $this->typeName = $wgRequest->getVal( 'type' );
128 $this->targetObj = Title::newFromText( $wgRequest->getText( 'target' ) );
129 }
130
131 # For reviewing deleted files...
132 $this->archiveName = $wgRequest->getVal( 'file' );
133 $this->token = $wgRequest->getVal( 'token' );
134 if ( $this->archiveName && $this->targetObj ) {
135 $this->tryShowFile( $this->archiveName );
136 return;
137 }
138
139 if ( isset( self::$deprecatedTypeMap[$this->typeName] ) ) {
140 $this->typeName = self::$deprecatedTypeMap[$this->typeName];
141 }
142
143 # No targets?
144 if( !isset( self::$allowedTypes[$this->typeName] ) || count( $this->ids ) == 0 ) {
145 $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
146 return;
147 }
148 $this->typeInfo = self::$allowedTypes[$this->typeName];
149
150 # If we have revisions, get the title from the first one
151 # since they should all be from the same page. This allows
152 # for more flexibility with page moves...
153 if( $this->typeName == 'revision' ) {
154 $rev = Revision::newFromId( $this->ids[0] );
155 $this->targetObj = $rev ? $rev->getTitle() : $this->targetObj;
156 }
157
158 $this->otherReason = $wgRequest->getVal( 'wpReason' );
159 # We need a target page!
160 if( is_null($this->targetObj) ) {
161 $wgOut->addWikiMsg( 'undelete-header' );
162 return;
163 }
164 # Give a link to the logs/hist for this page
165 $this->showConvenienceLinks();
166
167 # Initialise checkboxes
168 $this->checks = array(
169 array( $this->typeInfo['check-label'], 'wpHidePrimary', $this->typeInfo['deletion-bits'] ),
170 array( 'revdelete-hide-comment', 'wpHideComment', Revision::DELETED_COMMENT ),
171 array( 'revdelete-hide-user', 'wpHideUser', Revision::DELETED_USER )
172 );
173 if( $wgUser->isAllowed('suppressrevision') ) {
174 $this->checks[] = array( 'revdelete-hide-restricted',
175 'wpHideRestricted', Revision::DELETED_RESTRICTED );
176 }
177
178 # Either submit or create our form
179 if( $this->mIsAllowed && $this->submitClicked ) {
180 $this->submit( $wgRequest );
181 } else {
182 $this->showForm();
183 }
184 $qc = $this->getLogQueryCond();
185 # Show relevant lines from the deletion log
186 $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" );
187 LogEventsList::showLogExtract( $wgOut, 'delete',
188 $this->targetObj->getPrefixedText(), '', array( 'lim' => 25, 'conds' => $qc ) );
189 # Show relevant lines from the suppression log
190 if( $wgUser->isAllowed( 'suppressionlog' ) ) {
191 $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'suppress' ) ) . "</h2>\n" );
192 LogEventsList::showLogExtract( $wgOut, 'suppress',
193 $this->targetObj->getPrefixedText(), '', array( 'lim' => 25, 'conds' => $qc ) );
194 }
195 }
196
197 /**
198 * Show some useful links in the subtitle
199 */
200 protected function showConvenienceLinks() {
201 global $wgOut, $wgUser, $wgLang;
202 # Give a link to the logs/hist for this page
203 if( $this->targetObj ) {
204 $links = array();
205 $links[] = $this->skin->linkKnown(
206 SpecialPage::getTitleFor( 'Log' ),
207 wfMsgHtml( 'viewpagelogs' ),
208 array(),
209 array( 'page' => $this->targetObj->getPrefixedText() )
210 );
211 if ( $this->targetObj->getNamespace() != NS_SPECIAL ) {
212 # Give a link to the page history
213 $links[] = $this->skin->linkKnown(
214 $this->targetObj,
215 wfMsgHtml( 'pagehist' ),
216 array(),
217 array( 'action' => 'history' )
218 );
219 # Link to deleted edits
220 if( $wgUser->isAllowed('undelete') ) {
221 $undelete = SpecialPage::getTitleFor( 'Undelete' );
222 $links[] = $this->skin->linkKnown(
223 $undelete,
224 wfMsgHtml( 'deletedhist' ),
225 array(),
226 array( 'target' => $this->targetObj->getPrefixedDBkey() )
227 );
228 }
229 }
230 # Logs themselves don't have histories or archived revisions
231 $wgOut->setSubtitle( '<p>' . $wgLang->pipeList( $links ) . '</p>' );
232 }
233 }
234
235 /**
236 * Get the condition used for fetching log snippets
237 */
238 protected function getLogQueryCond() {
239 $conds = array();
240 // Revision delete logs for these item
241 $conds['log_type'] = array('delete','suppress');
242 $conds['log_action'] = $this->getList()->getLogAction();
243 $conds['ls_field'] = RevisionDeleter::getRelationType( $this->typeName );
244 $conds['ls_value'] = $this->ids;
245 return $conds;
246 }
247
248 /**
249 * Show a deleted file version requested by the visitor.
250 * TODO Mostly copied from Special:Undelete. Refactor.
251 */
252 protected function tryShowFile( $archiveName ) {
253 global $wgOut, $wgRequest, $wgUser, $wgLang;
254
255 $repo = RepoGroup::singleton()->getLocalRepo();
256 $oimage = $repo->newFromArchiveName( $this->targetObj, $archiveName );
257 $oimage->load();
258 // Check if user is allowed to see this file
259 if ( !$oimage->exists() ) {
260 $wgOut->addWikiMsg( 'revdelete-no-file' );
261 return;
262 }
263 if( !$oimage->userCan(File::DELETED_FILE) ) {
264 if( $oimage->isDeleted( File::DELETED_RESTRICTED ) ) {
265 $wgOut->permissionRequired( 'suppressrevision' );
266 } else {
267 $wgOut->permissionRequired( 'deletedtext' );
268 }
269 return;
270 }
271 if ( !$wgUser->matchEditToken( $this->token, $archiveName ) ) {
272 $wgOut->addWikiMsg( 'revdelete-show-file-confirm',
273 $this->targetObj->getText(),
274 $wgLang->date( $oimage->getTimestamp() ),
275 $wgLang->time( $oimage->getTimestamp() ) );
276 $wgOut->addHTML(
277 Xml::openElement( 'form', array(
278 'method' => 'POST',
279 'action' => $this->getTitle()->getLocalUrl(
280 'target=' . urlencode( $oimage->getName() ) .
281 '&file=' . urlencode( $archiveName ) .
282 '&token=' . urlencode( $wgUser->editToken( $archiveName ) ) )
283 )
284 ) .
285 Xml::submitButton( wfMsg( 'revdelete-show-file-submit' ) ) .
286 '</form>'
287 );
288 return;
289 }
290 $wgOut->disable();
291 # We mustn't allow the output to be Squid cached, otherwise
292 # if an admin previews a deleted image, and it's cached, then
293 # a user without appropriate permissions can toddle off and
294 # nab the image, and Squid will serve it
295 $wgRequest->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
296 $wgRequest->response()->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
297 $wgRequest->response()->header( 'Pragma: no-cache' );
298
299 # Stream the file to the client
300 global $IP;
301 require_once( "$IP/includes/StreamFile.php" );
302 $key = $oimage->getStorageKey();
303 $path = $repo->getZonePath( 'deleted' ) . '/' . $repo->getDeletedHashPath( $key ) . $key;
304 wfStreamFile( $path );
305 }
306
307 /**
308 * Get the list object for this request
309 */
310 protected function getList() {
311 if ( is_null( $this->list ) ) {
312 $class = $this->typeInfo['list-class'];
313 $this->list = new $class( $this, $this->targetObj, $this->ids );
314 }
315 return $this->list;
316 }
317
318 /**
319 * Show a list of items that we will operate on, and show a form with checkboxes
320 * which will allow the user to choose new visibility settings.
321 */
322 protected function showForm() {
323 global $wgOut, $wgUser, $wgLang;
324 $UserAllowed = true;
325
326 if ( $this->typeName == 'logging' ) {
327 $wgOut->addWikiMsg( 'logdelete-selected', $wgLang->formatNum( count($this->ids) ) );
328 } else {
329 $wgOut->addWikiMsg( 'revdelete-selected',
330 $this->targetObj->getPrefixedText(), count( $this->ids ) );
331 }
332
333 $bitfields = 0;
334 $wgOut->addHTML( "<ul>" );
335
336 $where = $revObjs = array();
337
338 $numRevisions = 0;
339 // Live revisions...
340 $list = $this->getList();
341 for ( $list->reset(); $list->current(); $list->next() ) {
342 $item = $list->current();
343 if ( !$item->canView() ) {
344 if( !$this->submitClicked ) {
345 $wgOut->permissionRequired( 'suppressrevision' );
346 return;
347 }
348 $UserAllowed = false;
349 }
350 $numRevisions++;
351 $bitfields |= $item->getBits();
352 $wgOut->addHTML( $item->getHTML() );
353 }
354
355 if( !$numRevisions ) {
356 $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
357 return;
358 }
359
360 $wgOut->addHTML( "</ul>" );
361 // Explanation text
362 $this->addUsageText();
363
364 // Normal sysops can always see what they did, but can't always change it
365 if( !$UserAllowed ) return;
366
367 // Show form if the user can submit
368 if( $this->mIsAllowed ) {
369 $out = Xml::openElement( 'form', array( 'method' => 'post',
370 'action' => $this->getTitle()->getLocalUrl( array( 'action' => 'submit' ) ),
371 'id' => 'mw-revdel-form-revisions' ) ) .
372 Xml::fieldset( wfMsg( 'revdelete-legend' ) ) .
373 $this->buildCheckBoxes( $bitfields ) .
374 Xml::openElement( 'table' ) .
375 "<tr>\n" .
376 '<td class="mw-label">' .
377 Xml::label( wfMsg( 'revdelete-log' ), 'wpRevDeleteReasonList' ) .
378 '</td>' .
379 '<td class="mw-input">' .
380 Xml::listDropDown( 'wpRevDeleteReasonList',
381 wfMsgForContent( 'revdelete-reason-dropdown' ),
382 wfMsgForContent( 'revdelete-reasonotherlist' ), '', 'wpReasonDropDown', 1
383 ) .
384 '</td>' .
385 "</tr><tr>\n" .
386 '<td class="mw-label">' .
387 Xml::label( wfMsg( 'revdelete-otherreason' ), 'wpReason' ) .
388 '</td>' .
389 '<td class="mw-input">' .
390 Xml::input( 'wpReason', 60, $this->otherReason, array( 'id' => 'wpReason' ) ) .
391 '</td>' .
392 "</tr><tr>\n" .
393 '<td></td>' .
394 '<td class="mw-submit">' .
395 Xml::submitButton( wfMsgExt('revdelete-submit','parsemag',$numRevisions),
396 array( 'name' => 'wpSubmit' ) ) .
397 '</td>' .
398 "</tr>\n" .
399 Xml::closeElement( 'table' ) .
400 Xml::hidden( 'wpEditToken', $wgUser->editToken() ) .
401 Xml::hidden( 'target', $this->targetObj->getPrefixedText() ) .
402 Xml::hidden( 'type', $this->typeName ) .
403 Xml::hidden( 'ids', implode( ',', $this->ids ) ) .
404 Xml::closeElement( 'fieldset' ) . "\n";
405 } else {
406 $out = '';
407 }
408 if( $this->mIsAllowed ) {
409 $out .= Xml::closeElement( 'form' ) . "\n";
410 // Show link to edit the dropdown reasons
411 if( $wgUser->isAllowed( 'editinterface' ) ) {
412 $title = Title::makeTitle( NS_MEDIAWIKI, 'revdelete-reason-dropdown' );
413 $link = $wgUser->getSkin()->link(
414 $title,
415 wfMsgHtml( 'revdelete-edit-reasonlist' ),
416 array(),
417 array( 'action' => 'edit' )
418 );
419 $out .= Xml::tags( 'p', array( 'class' => 'mw-revdel-editreasons' ), $link ) . "\n";
420 }
421 }
422 $wgOut->addHTML( $out );
423 }
424
425 /**
426 * Show some introductory text
427 * FIXME Wikimedia-specific policy text
428 */
429 protected function addUsageText() {
430 global $wgOut, $wgUser;
431 $wgOut->addWikiMsg( 'revdelete-text' );
432 if( $wgUser->isAllowed( 'suppressrevision' ) ) {
433 $wgOut->addWikiMsg( 'revdelete-suppress-text' );
434 }
435 if( $this->mIsAllowed ) {
436 $wgOut->addWikiMsg( 'revdelete-confirm' );
437 }
438 }
439
440 /**
441 * @param $bitfields Interger: aggregate bitfield of all the bitfields
442 * @return String: HTML
443 */
444 protected function buildCheckBoxes( $bitfields ) {
445 $html = '<table>';
446 // FIXME: all items checked for just one rev are checked, even if not set for the others
447 foreach( $this->checks as $item ) {
448 list( $message, $name, $field ) = $item;
449 $innerHTML = Xml::checkLabel( wfMsg($message), $name, $name, $bitfields & $field );
450 if( $field == Revision::DELETED_RESTRICTED )
451 $innerHTML = "<b>$innerHTML</b>";
452 $line = Xml::tags( 'td', array( 'class' => 'mw-input' ), $innerHTML );
453 $html .= '<tr>' . $line . "</tr>\n";
454 }
455 $html .= '</table>';
456 return $html;
457 }
458
459 /**
460 * UI entry point for form submission.
461 * @param $request WebRequest
462 */
463 protected function submit( $request ) {
464 global $wgUser, $wgOut;
465 # Check edit token on submission
466 if( $this->submitClicked && !$wgUser->matchEditToken( $request->getVal('wpEditToken') ) ) {
467 $wgOut->addWikiMsg( 'sessionfailure' );
468 return false;
469 }
470 $bitfield = $this->extractBitfield( $request );
471 $listReason = $request->getText( 'wpRevDeleteReasonList', 'other' ); // from dropdown
472 $comment = $listReason;
473 if( $comment != 'other' && $this->otherReason != '' ) {
474 // Entry from drop down menu + additional comment
475 $comment .= wfMsgForContent( 'colon-separator' ) . $this->otherReason;
476 } elseif( $comment == 'other' ) {
477 $comment = $this->otherReason;
478 }
479 # Can the user set this field?
480 if( $bitfield & Revision::DELETED_RESTRICTED && !$wgUser->isAllowed('suppressrevision') ) {
481 $wgOut->permissionRequired( 'suppressrevision' );
482 return false;
483 }
484 # If the save went through, go to success message...
485 $status = $this->save( $bitfield, $comment, $this->targetObj );
486 if ( $status->isGood() ) {
487 $this->success();
488 return true;
489 # ...otherwise, bounce back to form...
490 } else {
491 $this->failure( $status );
492 }
493 return false;
494 }
495
496 /**
497 * Report that the submit operation succeeded
498 */
499 protected function success() {
500 global $wgOut;
501 $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
502 $wgOut->wrapWikiMsg( '<span class="success">$1</span>', $this->typeInfo['success'] );
503 $this->list->reloadFromMaster();
504 $this->showForm();
505 }
506
507 /**
508 * Report that the submit operation failed
509 */
510 protected function failure( $status ) {
511 global $wgOut;
512 $wgOut->setPagetitle( wfMsg( 'actionfailed' ) );
513 $wgOut->addWikiText( $status->getWikiText( $this->typeInfo['failure'] ) );
514 $this->showForm();
515 }
516
517 /**
518 * Put together a rev_deleted bitfield from the submitted checkboxes
519 * @param $request WebRequest
520 * @return Integer
521 */
522 protected function extractBitfield( $request ) {
523 $bitfield = 0;
524 foreach( $this->checks as $item ) {
525 list( /* message */ , $name, $field ) = $item;
526 if( $request->getCheck( $name ) ) {
527 $bitfield |= $field;
528 }
529 }
530 return $bitfield;
531 }
532
533 /**
534 * Do the write operations. Simple wrapper for RevDel_*List::setVisibility().
535 */
536 protected function save( $bitfield, $reason, $title ) {
537 // Don't allow simply locking the interface for no reason
538 if( $bitfield == Revision::DELETED_RESTRICTED ) {
539 return Status::newFatal( 'revdelete-only-restricted' );
540 }
541 return $this->getList()->setVisibility( array(
542 'value' => $bitfield,
543 'comment' => $reason ) );
544 }
545 }
546
547 /**
548 * Temporary b/c interface, collection of static functions.
549 * @ingroup SpecialPage
550 */
551 class RevisionDeleter {
552 /**
553 * Checks for a change in the bitfield for a certain option and updates the
554 * provided array accordingly.
555 *
556 * @param $desc String: description to add to the array if the option was
557 * enabled / disabled.
558 * @param $field Integer: the bitmask describing the single option.
559 * @param $diff Integer: the xor of the old and new bitfields.
560 * @param $new Integer: the new bitfield
561 * @param $arr Array: the array to update.
562 */
563 protected static function checkItem( $desc, $field, $diff, $new, &$arr ) {
564 if( $diff & $field ) {
565 $arr[ ( $new & $field ) ? 0 : 1 ][] = $desc;
566 }
567 }
568
569 /**
570 * Gets an array describing the changes made to the visibilit of the revision.
571 * If the resulting array is $arr, then $arr[0] will contain an array of strings
572 * describing the items that were hidden, $arr[2] will contain an array of strings
573 * describing the items that were unhidden, and $arr[3] will contain an array with
574 * a single string, which can be one of "applied restrictions to sysops",
575 * "removed restrictions from sysops", or null.
576 *
577 * @param $n Integer: the new bitfield.
578 * @param $o Integer: the old bitfield.
579 * @return An array as described above.
580 */
581 protected static function getChanges( $n, $o ) {
582 $diff = $n ^ $o;
583 $ret = array( 0 => array(), 1 => array(), 2 => array() );
584 // Build bitfield changes in language
585 self::checkItem( wfMsgForContent( 'revdelete-content' ),
586 Revision::DELETED_TEXT, $diff, $n, $ret );
587 self::checkItem( wfMsgForContent( 'revdelete-summary' ),
588 Revision::DELETED_COMMENT, $diff, $n, $ret );
589 self::checkItem( wfMsgForContent( 'revdelete-uname' ),
590 Revision::DELETED_USER, $diff, $n, $ret );
591 // Restriction application to sysops
592 if( $diff & Revision::DELETED_RESTRICTED ) {
593 if( $n & Revision::DELETED_RESTRICTED )
594 $ret[2][] = wfMsgForContent( 'revdelete-restricted' );
595 else
596 $ret[2][] = wfMsgForContent( 'revdelete-unrestricted' );
597 }
598 return $ret;
599 }
600
601 /**
602 * Gets a log message to describe the given revision visibility change. This
603 * message will be of the form "[hid {content, edit summary, username}];
604 * [unhid {...}][applied restrictions to sysops] for $count revisions: $comment".
605 *
606 * @param $count Integer: The number of effected revisions.
607 * @param $nbitfield Integer: The new bitfield for the revision.
608 * @param $obitfield Integer: The old bitfield for the revision.
609 * @param $isForLog Boolean
610 */
611 public static function getLogMessage( $count, $nbitfield, $obitfield, $isForLog = false ) {
612 global $wgLang;
613 $s = '';
614 $changes = self::getChanges( $nbitfield, $obitfield );
615 if( count( $changes[0] ) ) {
616 $s .= wfMsgForContent( 'revdelete-hid', implode( ', ', $changes[0] ) );
617 }
618 if( count( $changes[1] ) ) {
619 if ($s) $s .= '; ';
620 $s .= wfMsgForContent( 'revdelete-unhid', implode( ', ', $changes[1] ) );
621 }
622 if( count( $changes[2] ) ) {
623 $s .= $s ? ' (' . $changes[2][0] . ')' : $changes[2][0];
624 }
625 $msg = $isForLog ? 'logdelete-log-message' : 'revdelete-log-message';
626 return wfMsgExt( $msg, array( 'parsemag', 'content' ), $s, $wgLang->formatNum($count) );
627
628 }
629
630 // Get DB field name for URL param...
631 // Future code for other things may also track
632 // other types of revision-specific changes.
633 // @returns string One of log_id/rev_id/fa_id/ar_timestamp/oi_archive_name
634 public static function getRelationType( $typeName ) {
635 if ( isset( SpecialRevisionDelete::$deprecatedTypeMap[$typeName] ) ) {
636 $typeName = SpecialRevisionDelete::$deprecatedTypeMap[$typeName];
637 }
638 if ( isset( SpecialRevisionDelete::$allowedTypes[$typeName] ) ) {
639 $class = SpecialRevisionDelete::$allowedTypes[$typeName]['list-class'];
640 $list = new $class( null, null, null );
641 return $list->getIdField();
642 } else {
643 return null;
644 }
645 }
646 }
647
648 /**
649 * Abstract base class for a list of deletable items
650 */
651 abstract class RevDel_List {
652 var $special, $title, $ids, $res, $current;
653 var $type = null; // override this
654 var $idField = null; // override this
655 var $dateField = false; // override this
656 var $authorIdField = false; // override this
657 var $authorNameField = false; // override this
658
659 /**
660 * @param $special The parent SpecialPage
661 * @param $title The target title
662 * @param $ids Array of IDs
663 */
664 public function __construct( $special, $title, $ids ) {
665 $this->special = $special;
666 $this->title = $title;
667 $this->ids = $ids;
668 }
669
670 /**
671 * Get the internal type name of this list. Equal to the table name.
672 */
673 public function getType() {
674 return $this->type;
675 }
676
677 /**
678 * Get the DB field name associated with the ID list
679 */
680 public function getIdField() {
681 return $this->idField;
682 }
683
684 /**
685 * Get the DB field name storing timestamps
686 */
687 public function getTimestampField() {
688 return $this->dateField;
689 }
690
691 /**
692 * Get the DB field name storing user ids
693 */
694 public function getAuthorIdField() {
695 return $this->authorIdField;
696 }
697
698 /**
699 * Get the DB field name storing user names
700 */
701 public function getAuthorNameField() {
702 return $this->authorNameField;
703 }
704 /**
705 * Set the visibility for the revisions in this list. Logging and
706 * transactions are done here.
707 *
708 * @param $params Associative array of parameters. Members are:
709 * value: The integer value to set the visibility to
710 * comment: The log comment.
711 * @return Status
712 */
713 public function setVisibility( $params ) {
714 $newBits = $params['value'];
715 $comment = $params['comment'];
716
717 $this->res = false;
718 $dbw = wfGetDB( DB_MASTER );
719 $this->doQuery( $dbw );
720 $dbw->begin();
721 $status = Status::newGood();
722 $missing = array_flip( $this->ids );
723 $this->clearFileOps();
724 $idsForLog = array();
725 $authorIds = $authorIPs = array();
726
727 for ( $this->reset(); $this->current(); $this->next() ) {
728 $item = $this->current();
729 unset( $missing[ $item->getId() ] );
730
731 // Make error messages less vague
732 $oldBits = $item->getBits();
733 if ( $oldBits == $newBits ) {
734 $status->warning( 'revdelete-no-change', $item->formatDate(), $item->formatTime() );
735 $status->failCount++;
736 continue;
737 } elseif ( $oldBits == 0 && $newBits != 0 ) {
738 $opType = 'hide';
739 } elseif ( $oldBits != 0 && $newBits == 0 ) {
740 $opType = 'show';
741 } else {
742 $opType = 'modify';
743 }
744
745 if ( $item->isHideCurrentOp( $newBits ) ) {
746 // Cannot hide current version text
747 $status->error( 'revdelete-hide-current', $item->formatDate(), $item->formatTime() );
748 $status->failCount++;
749 continue;
750 }
751 if ( !$item->canView() ) {
752 // Cannot access this revision
753 $msg = $opType == 'show' ? 'revdelete-show-no-access' : 'revdelete-modify-no-access';
754 $status->error( $msg, $item->formatDate(), $item->formatTime() );
755 $status->failCount++;
756 continue;
757 }
758
759 // Update the revision
760 $ok = $item->setBits( $newBits );
761
762 if ( $ok ) {
763 $idsForLog[] = $item->getId();
764 $status->successCount++;
765 if( $item->getAuthorId() > 0 ) {
766 $authorIds[] = $item->getAuthorId();
767 } else if( IP::isIPAddress( $item->getAuthorName() ) ) {
768 $authorIPs[] = $item->getAuthorName();
769 }
770 } else {
771 $status->error( 'revdelete-concurrent-change', $item->formatDate(), $item->formatTime() );
772 $status->failCount++;
773 }
774 }
775
776 // Handle missing revisions
777 foreach ( $missing as $id => $unused ) {
778 $status->error( 'revdelete-modify-missing', $id );
779 $status->failCount++;
780 }
781
782 if ( $status->successCount == 0 ) {
783 $status->ok = false;
784 $dbw->rollback();
785 return $status;
786 }
787
788 // Save success count
789 $successCount = $status->successCount;
790
791 // Move files, if there are any
792 $status->merge( $this->doPreCommitUpdates() );
793 if ( !$status->isOK() ) {
794 // Fatal error, such as no configured archive directory
795 $dbw->rollback();
796 return $status;
797 }
798
799 // Log it
800 $this->updateLog( array(
801 'title' => $this->title,
802 'count' => $successCount,
803 'newBits' => $newBits,
804 'oldBits' => $oldBits,
805 'comment' => $comment,
806 'ids' => $idsForLog,
807 'authorIds' => $authorIds,
808 'authorIPs' => $authorIPs
809 ) );
810 $dbw->commit();
811
812 // Clear caches
813 $status->merge( $this->doPostCommitUpdates() );
814 return $status;
815 }
816
817 /**
818 * Reload the list data from the master DB. This can be done after setVisibility()
819 * to allow $item->getHTML() to show the new data.
820 */
821 function reloadFromMaster() {
822 $dbw = wfGetDB( DB_MASTER );
823 $this->res = $this->doQuery( $dbw );
824 }
825
826 /**
827 * Record a log entry on the action
828 * @param $params Associative array of parameters:
829 * newBits: The new value of the *_deleted bitfield
830 * oldBits: The old value of the *_deleted bitfield.
831 * title: The target title
832 * ids: The ID list
833 * comment: The log comment
834 * authorsIds: The array of the user IDs of the offenders
835 * authorsIPs: The array of the IP/anon user offenders
836 */
837 protected function updateLog( $params ) {
838 // Get the URL param's corresponding DB field
839 $field = RevisionDeleter::getRelationType( $this->getType() );
840 if( !$field ) {
841 throw new MWException( "Bad log URL param type!" );
842 }
843 // Put things hidden from sysops in the oversight log
844 if ( ( $params['newBits'] | $params['oldBits'] ) & $this->getSuppressBit() ) {
845 $logType = 'suppress';
846 } else {
847 $logType = 'delete';
848 }
849 // Add params for effected page and ids
850 $logParams = $this->getLogParams( $params );
851 // Actually add the deletion log entry
852 $log = new LogPage( $logType );
853 $logid = $log->addEntry( $this->getLogAction(), $params['title'],
854 $params['comment'], $logParams );
855 // Allow for easy searching of deletion log items for revision/log items
856 $log->addRelations( $field, $params['ids'], $logid );
857 $log->addRelations( 'target_author_id', $params['authorIds'], $logid );
858 $log->addRelations( 'target_author_ip', $params['authorIPs'], $logid );
859 }
860
861 /**
862 * Get the log action for this list type
863 */
864 public function getLogAction() {
865 return 'revision';
866 }
867
868 /**
869 * Get log parameter array.
870 * @param $params Associative array of log parameters, same as updateLog()
871 * @return array
872 */
873 public function getLogParams( $params ) {
874 return array(
875 $this->getType(),
876 implode( ',', $params['ids'] ),
877 "ofield={$params['oldBits']}",
878 "nfield={$params['newBits']}"
879 );
880 }
881
882 /**
883 * Initialise the current iteration pointer
884 */
885 protected function initCurrent() {
886 $row = $this->res->current();
887 if ( $row ) {
888 $this->current = $this->newItem( $row );
889 } else {
890 $this->current = false;
891 }
892 }
893
894 /**
895 * Start iteration. This must be called before current() or next().
896 * @return First list item
897 */
898 public function reset() {
899 if ( !$this->res ) {
900 $this->res = $this->doQuery( wfGetDB( DB_SLAVE ) );
901 } else {
902 $this->res->rewind();
903 }
904 $this->initCurrent();
905 return $this->current;
906 }
907
908 /**
909 * Get the current list item, or false if we are at the end
910 */
911 public function current() {
912 return $this->current;
913 }
914
915 /**
916 * Move the iteration pointer to the next list item, and return it.
917 */
918 public function next() {
919 $this->res->next();
920 $this->initCurrent();
921 return $this->current;
922 }
923
924 /**
925 * Clear any data structures needed for doPreCommitUpdates() and doPostCommitUpdates()
926 * STUB
927 */
928 public function clearFileOps() {
929 }
930
931 /**
932 * A hook for setVisibility(): do batch updates pre-commit.
933 * STUB
934 * @return Status
935 */
936 public function doPreCommitUpdates() {
937 return Status::newGood();
938 }
939
940 /**
941 * A hook for setVisibility(): do any necessary updates post-commit.
942 * STUB
943 * @return Status
944 */
945 public function doPostCommitUpdates() {
946 return Status::newGood();
947 }
948
949 /**
950 * Create an item object from a DB result row
951 * @param $row stdclass
952 */
953 abstract public function newItem( $row );
954
955 /**
956 * Do the DB query to iterate through the objects.
957 * @param $db Database object to use for the query
958 */
959 abstract public function doQuery( $db );
960
961 /**
962 * Get the integer value of the flag used for suppression
963 */
964 abstract public function getSuppressBit();
965 }
966
967 /**
968 * Abstract base class for deletable items
969 */
970 abstract class RevDel_Item {
971 /** The parent SpecialPage */
972 var $special;
973
974 /** The parent RevDel_List */
975 var $list;
976
977 /** The DB result row */
978 var $row;
979
980 /**
981 * @param $list RevDel_List
982 * @param $row DB result row
983 */
984 public function __construct( $list, $row ) {
985 $this->special = $list->special;
986 $this->list = $list;
987 $this->row = $row;
988 }
989
990 /**
991 * Get the ID, as it would appear in the ids URL parameter
992 */
993 public function getId() {
994 $field = $this->list->getIdField();
995 return $this->row->$field;
996 }
997
998 /**
999 * Get the date, formatted with $wgLang
1000 */
1001 public function formatDate() {
1002 global $wgLang;
1003 return $wgLang->date( $this->getTimestamp() );
1004 }
1005
1006 /**
1007 * Get the time, formatted with $wgLang
1008 */
1009 public function formatTime() {
1010 global $wgLang;
1011 return $wgLang->time( $this->getTimestamp() );
1012 }
1013
1014 /**
1015 * Get the timestamp in MW 14-char form
1016 */
1017 public function getTimestamp() {
1018 $field = $this->list->getTimestampField();
1019 return wfTimestamp( TS_MW, $this->row->$field );
1020 }
1021
1022 /**
1023 * Get the author user ID
1024 */
1025 public function getAuthorId() {
1026 $field = $this->list->getAuthorIdField();
1027 return intval( $this->row->$field );
1028 }
1029
1030 /**
1031 * Get the author user name
1032 */
1033 public function getAuthorName() {
1034 $field = $this->list->getAuthorNameField();
1035 return strval( $this->row->$field );
1036 }
1037
1038 /**
1039 * Returns true if the item is "current", and the operation to set the given
1040 * bits can't be executed for that reason
1041 * STUB
1042 */
1043 public function isHideCurrentOp( $newBits ) {
1044 return false;
1045 }
1046
1047 /**
1048 * Returns true if the current user can view the item
1049 */
1050 abstract public function canView();
1051
1052 /**
1053 * Returns true if the current user can view the item text/file
1054 */
1055 abstract public function canViewContent();
1056
1057 /**
1058 * Get the current deletion bitfield value
1059 */
1060 abstract public function getBits();
1061
1062 /**
1063 * Get the HTML of the list item. Should be include <li></li> tags.
1064 * This is used to show the list in HTML form, by the special page.
1065 */
1066 abstract public function getHTML();
1067
1068 /**
1069 * Set the visibility of the item. This should do any necessary DB queries.
1070 *
1071 * The DB update query should have a condition which forces it to only update
1072 * if the value in the DB matches the value fetched earlier with the SELECT.
1073 * If the update fails because it did not match, the function should return
1074 * false. This prevents concurrency problems.
1075 *
1076 * @return boolean success
1077 */
1078 abstract public function setBits( $newBits );
1079 }
1080
1081 /**
1082 * List for revision table items
1083 */
1084 class RevDel_RevisionList extends RevDel_List {
1085 var $currentRevId;
1086 var $type = 'revision';
1087 var $idField = 'rev_id';
1088 var $dateField = 'rev_timestamp';
1089 var $authorIdField = 'rev_user';
1090 var $authorNameField = 'rev_user_text';
1091
1092 public function doQuery( $db ) {
1093 $ids = array_map( 'intval', $this->ids );
1094 return $db->select( array('revision','page'), '*',
1095 array(
1096 'rev_page' => $this->title->getArticleID(),
1097 'rev_id' => $ids,
1098 'rev_page = page_id'
1099 ),
1100 __METHOD__
1101 );
1102 }
1103
1104 public function newItem( $row ) {
1105 return new RevDel_RevisionItem( $this, $row );
1106 }
1107
1108 public function getCurrent() {
1109 if ( is_null( $this->currentRevId ) ) {
1110 $dbw = wfGetDB( DB_MASTER );
1111 $this->currentRevId = $dbw->selectField(
1112 'page', 'page_latest', $this->title->pageCond(), __METHOD__ );
1113 }
1114 return $this->currentRevId;
1115 }
1116
1117 public function getSuppressBit() {
1118 return Revision::DELETED_RESTRICTED;
1119 }
1120
1121 public function doPreCommitUpdates() {
1122 $this->title->invalidateCache();
1123 return Status::newGood();
1124 }
1125
1126 public function doPostCommitUpdates() {
1127 $this->title->purgeSquid();
1128 // Extensions that require referencing previous revisions may need this
1129 wfRunHooks( 'ArticleRevisionVisiblitySet', array( &$this->title ) );
1130 return Status::newGood();
1131 }
1132 }
1133
1134 /**
1135 * Item class for a revision table row
1136 */
1137 class RevDel_RevisionItem extends RevDel_Item {
1138 var $revision;
1139
1140 public function __construct( $list, $row ) {
1141 parent::__construct( $list, $row );
1142 $this->revision = new Revision( $row );
1143 }
1144
1145 public function canView() {
1146 return $this->revision->userCan( Revision::DELETED_RESTRICTED );
1147 }
1148
1149 public function canViewContent() {
1150 return $this->revision->userCan( Revision::DELETED_TEXT );
1151 }
1152
1153 public function getBits() {
1154 return $this->revision->mDeleted;
1155 }
1156
1157 public function setBits( $bits ) {
1158 $dbw = wfGetDB( DB_MASTER );
1159 // Update revision table
1160 $dbw->update( 'revision',
1161 array( 'rev_deleted' => $bits ),
1162 array(
1163 'rev_id' => $this->revision->getId(),
1164 'rev_page' => $this->revision->getPage(),
1165 'rev_deleted' => $this->getBits()
1166 ),
1167 __METHOD__
1168 );
1169 if ( !$dbw->affectedRows() ) {
1170 // Concurrent fail!
1171 return false;
1172 }
1173 // Update recentchanges table
1174 $dbw->update( 'recentchanges',
1175 array(
1176 'rc_deleted' => $bits,
1177 'rc_patrolled' => 1
1178 ),
1179 array(
1180 'rc_this_oldid' => $this->revision->getId(), // condition
1181 // non-unique timestamp index
1182 'rc_timestamp' => $dbw->timestamp( $this->revision->getTimestamp() ),
1183 ),
1184 __METHOD__
1185 );
1186 return true;
1187 }
1188
1189 public function isDeleted() {
1190 return $this->revision->isDeleted( Revision::DELETED_TEXT );
1191 }
1192
1193 public function isHideCurrentOp( $newBits ) {
1194 return ( $newBits & Revision::DELETED_TEXT )
1195 && $this->list->getCurrent() == $this->getId();
1196 }
1197
1198 /**
1199 * Get the HTML link to the revision text.
1200 * Overridden by RevDel_ArchiveItem.
1201 */
1202 protected function getRevisionLink() {
1203 global $wgLang;
1204 $date = $wgLang->timeanddate( $this->revision->getTimestamp(), true );
1205 if ( $this->isDeleted() && !$this->canViewContent() ) {
1206 return $date;
1207 }
1208 return $this->special->skin->link(
1209 $this->list->title,
1210 $date,
1211 array(),
1212 array(
1213 'oldid' => $this->revision->getId(),
1214 'unhide' => 1
1215 )
1216 );
1217 }
1218
1219 /**
1220 * Get the HTML link to the diff.
1221 * Overridden by RevDel_ArchiveItem
1222 */
1223 protected function getDiffLink() {
1224 if ( $this->isDeleted() && !$this->canViewContent() ) {
1225 return wfMsgHtml('diff');
1226 } else {
1227 return
1228 $this->special->skin->link(
1229 $this->list->title,
1230 wfMsgHtml('diff'),
1231 array(),
1232 array(
1233 'diff' => $this->revision->getId(),
1234 'oldid' => 'prev',
1235 'unhide' => 1
1236 ),
1237 array(
1238 'known',
1239 'noclasses'
1240 )
1241 );
1242 }
1243 }
1244
1245 public function getHTML() {
1246 $difflink = $this->getDiffLink();
1247 $revlink = $this->getRevisionLink();
1248 $userlink = $this->special->skin->revUserLink( $this->revision );
1249 $comment = $this->special->skin->revComment( $this->revision );
1250 if ( $this->isDeleted() ) {
1251 $revlink = "<span class=\"history-deleted\">$revlink</span>";
1252 }
1253 return "<li>($difflink) $revlink $userlink $comment</li>";
1254 }
1255 }
1256
1257 /**
1258 * List for archive table items, i.e. revisions deleted via action=delete
1259 */
1260 class RevDel_ArchiveList extends RevDel_RevisionList {
1261 var $type = 'archive';
1262 var $idField = 'ar_timestamp';
1263 var $dateField = 'ar_timestamp';
1264 var $authorIdField = 'ar_user';
1265 var $authorNameField = 'ar_user_text';
1266
1267 public function doQuery( $db ) {
1268 $timestamps = array();
1269 foreach ( $this->ids as $id ) {
1270 $timestamps[] = $db->timestamp( $id );
1271 }
1272 return $db->select( 'archive', '*',
1273 array(
1274 'ar_namespace' => $this->title->getNamespace(),
1275 'ar_title' => $this->title->getDBkey(),
1276 'ar_timestamp' => $timestamps
1277 ),
1278 __METHOD__
1279 );
1280 }
1281
1282 public function newItem( $row ) {
1283 return new RevDel_ArchiveItem( $this, $row );
1284 }
1285
1286 public function doPreCommitUpdates() {
1287 return Status::newGood();
1288 }
1289
1290 public function doPostCommitUpdates() {
1291 return Status::newGood();
1292 }
1293 }
1294
1295 /**
1296 * Item class for a archive table row
1297 */
1298 class RevDel_ArchiveItem extends RevDel_RevisionItem {
1299 public function __construct( $list, $row ) {
1300 RevDel_Item::__construct( $list, $row );
1301 $this->revision = Revision::newFromArchiveRow( $row,
1302 array( 'page' => $this->list->title->getArticleId() ) );
1303 }
1304
1305 public function getId() {
1306 # Convert DB timestamp to MW timestamp
1307 return $this->revision->getTimestamp();
1308 }
1309
1310 public function setBits( $bits ) {
1311 $dbw = wfGetDB( DB_MASTER );
1312 $dbw->update( 'archive',
1313 array( 'ar_deleted' => $bits ),
1314 array( 'ar_namespace' => $this->list->title->getNamespace(),
1315 'ar_title' => $this->list->title->getDBkey(),
1316 // use timestamp for index
1317 'ar_timestamp' => $this->row->ar_timestamp,
1318 'ar_rev_id' => $this->row->ar_rev_id,
1319 'ar_deleted' => $this->getBits()
1320 ),
1321 __METHOD__ );
1322 return (bool)$dbw->affectedRows();
1323 }
1324
1325 protected function getRevisionLink() {
1326 global $wgLang;
1327 $undelete = SpecialPage::getTitleFor( 'Undelete' );
1328 $date = $wgLang->timeanddate( $this->revision->getTimestamp(), true );
1329 if ( $this->isDeleted() && !$this->canViewContent() ) {
1330 return $date;
1331 }
1332 return $this->special->skin->link( $undelete, $date, array(),
1333 array(
1334 'target' => $this->list->title->getPrefixedText(),
1335 'timestamp' => $this->revision->getTimestamp()
1336 ) );
1337 }
1338
1339 protected function getDiffLink() {
1340 if ( $this->isDeleted() && !$this->canViewContent() ) {
1341 return wfMsgHtml( 'diff' );
1342 }
1343 $undelete = SpecialPage::getTitleFor( 'Undelete' );
1344 return $this->special->skin->link( $undelete, wfMsgHtml('diff'), array(),
1345 array(
1346 'target' => $this->list->title->getPrefixedText(),
1347 'diff' => 'prev',
1348 'timestamp' => $this->revision->getTimestamp()
1349 ) );
1350 }
1351 }
1352
1353 /**
1354 * List for oldimage table items
1355 */
1356 class RevDel_FileList extends RevDel_List {
1357 var $type = 'oldimage';
1358 var $idField = 'oi_archive_name';
1359 var $dateField = 'oi_timestamp';
1360 var $authorIdField = 'oi_user';
1361 var $authorNameField = 'oi_user_text';
1362 var $storeBatch, $deleteBatch, $cleanupBatch;
1363
1364 public function doQuery( $db ) {
1365 $archiveName = array();
1366 foreach( $this->ids as $timestamp ) {
1367 $archiveNames[] = $timestamp . '!' . $this->title->getDBkey();
1368 }
1369 return $db->select( 'oldimage', '*',
1370 array(
1371 'oi_name' => $this->title->getDBkey(),
1372 'oi_archive_name' => $archiveNames
1373 ),
1374 __METHOD__
1375 );
1376 }
1377
1378 public function newItem( $row ) {
1379 return new RevDel_FileItem( $this, $row );
1380 }
1381
1382 public function clearFileOps() {
1383 $this->deleteBatch = array();
1384 $this->storeBatch = array();
1385 $this->cleanupBatch = array();
1386 }
1387
1388 public function doPreCommitUpdates() {
1389 $status = Status::newGood();
1390 $repo = RepoGroup::singleton()->getLocalRepo();
1391 if ( $this->storeBatch ) {
1392 $status->merge( $repo->storeBatch( $this->storeBatch, FileRepo::OVERWRITE_SAME ) );
1393 }
1394 if ( !$status->isOK() ) {
1395 return $status;
1396 }
1397 if ( $this->deleteBatch ) {
1398 $status->merge( $repo->deleteBatch( $this->deleteBatch ) );
1399 }
1400 if ( !$status->isOK() ) {
1401 // Running cleanupDeletedBatch() after a failed storeBatch() with the DB already
1402 // modified (but destined for rollback) causes data loss
1403 return $status;
1404 }
1405 if ( $this->cleanupBatch ) {
1406 $status->merge( $repo->cleanupDeletedBatch( $this->cleanupBatch ) );
1407 }
1408 return $status;
1409 }
1410
1411 public function doPostCommitUpdates() {
1412 $file = wfLocalFile( $this->title );
1413 $file->purgeCache();
1414 $file->purgeDescription();
1415 return Status::newGood();
1416 }
1417
1418 public function getSuppressBit() {
1419 return File::DELETED_RESTRICTED;
1420 }
1421 }
1422
1423 /**
1424 * Item class for an oldimage table row
1425 */
1426 class RevDel_FileItem extends RevDel_Item {
1427 var $file;
1428
1429 public function __construct( $list, $row ) {
1430 parent::__construct( $list, $row );
1431 $this->file = RepoGroup::singleton()->getLocalRepo()->newFileFromRow( $row );
1432 }
1433
1434 public function getId() {
1435 $parts = explode( '!', $this->row->oi_archive_name );
1436 return $parts[0];
1437 }
1438
1439 public function canView() {
1440 return $this->file->userCan( File::DELETED_RESTRICTED );
1441 }
1442
1443 public function canViewContent() {
1444 return $this->file->userCan( File::DELETED_FILE );
1445 }
1446
1447 public function getBits() {
1448 return $this->file->getVisibility();
1449 }
1450
1451 public function setBits( $bits ) {
1452 # Queue the file op
1453 # FIXME: move to LocalFile.php
1454 if ( $this->isDeleted() ) {
1455 if ( $bits & File::DELETED_FILE ) {
1456 # Still deleted
1457 } else {
1458 # Newly undeleted
1459 $key = $this->file->getStorageKey();
1460 $srcRel = $this->file->repo->getDeletedHashPath( $key ) . $key;
1461 $this->list->storeBatch[] = array(
1462 $this->file->repo->getVirtualUrl( 'deleted' ) . '/' . $srcRel,
1463 'public',
1464 $this->file->getRel()
1465 );
1466 $this->list->cleanupBatch[] = $key;
1467 }
1468 } elseif ( $bits & File::DELETED_FILE ) {
1469 # Newly deleted
1470 $key = $this->file->getStorageKey();
1471 $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key;
1472 $this->list->deleteBatch[] = array( $this->file->getRel(), $dstRel );
1473 }
1474
1475 # Do the database operations
1476 $dbw = wfGetDB( DB_MASTER );
1477 $dbw->update( 'oldimage',
1478 array( 'oi_deleted' => $bits ),
1479 array(
1480 'oi_name' => $this->row->oi_name,
1481 'oi_timestamp' => $this->row->oi_timestamp,
1482 'oi_deleted' => $this->getBits()
1483 ),
1484 __METHOD__
1485 );
1486 return (bool)$dbw->affectedRows();
1487 }
1488
1489 public function isDeleted() {
1490 return $this->file->isDeleted( File::DELETED_FILE );
1491 }
1492
1493 /**
1494 * Get the link to the file.
1495 * Overridden by RevDel_ArchivedFileItem.
1496 */
1497 protected function getLink() {
1498 global $wgLang, $wgUser;
1499 $date = $wgLang->timeanddate( $this->file->getTimestamp(), true );
1500 if ( $this->isDeleted() ) {
1501 # Hidden files...
1502 if ( !$this->canViewContent() ) {
1503 $link = $date;
1504 } else {
1505 $link = $this->special->skin->link(
1506 $this->special->getTitle(),
1507 $date, array(),
1508 array(
1509 'target' => $this->list->title->getPrefixedText(),
1510 'file' => $this->file->getArchiveName(),
1511 'token' => $wgUser->editToken( $this->file->getArchiveName() )
1512 )
1513 );
1514 }
1515 return '<span class="history-deleted">' . $link . '</span>';
1516 } else {
1517 # Regular files...
1518 $url = $this->file->getUrl();
1519 return Xml::element( 'a', array( 'href' => $this->file->getUrl() ), $date );
1520 }
1521 }
1522 /**
1523 * Generate a user tool link cluster if the current user is allowed to view it
1524 * @return string HTML
1525 */
1526 protected function getUserTools() {
1527 if( $this->file->userCan( Revision::DELETED_USER ) ) {
1528 $link = $this->special->skin->userLink( $this->file->user, $this->file->user_text ) .
1529 $this->special->skin->userToolLinks( $this->file->user, $this->file->user_text );
1530 } else {
1531 $link = wfMsgHtml( 'rev-deleted-user' );
1532 }
1533 if( $this->file->isDeleted( Revision::DELETED_USER ) ) {
1534 return '<span class="history-deleted">' . $link . '</span>';
1535 }
1536 return $link;
1537 }
1538
1539 /**
1540 * Wrap and format the file's comment block, if the current
1541 * user is allowed to view it.
1542 *
1543 * @return string HTML
1544 */
1545 protected function getComment() {
1546 if( $this->file->userCan( File::DELETED_COMMENT ) ) {
1547 $block = $this->special->skin->commentBlock( $this->file->description );
1548 } else {
1549 $block = ' ' . wfMsgHtml( 'rev-deleted-comment' );
1550 }
1551 if( $this->file->isDeleted( File::DELETED_COMMENT ) ) {
1552 return "<span class=\"history-deleted\">$block</span>";
1553 }
1554 return $block;
1555 }
1556
1557 public function getHTML() {
1558 global $wgLang;
1559 $data =
1560 wfMsg(
1561 'widthheight',
1562 $wgLang->formatNum( $this->file->getWidth() ),
1563 $wgLang->formatNum( $this->file->getHeight() )
1564 ) .
1565 ' (' .
1566 wfMsgExt( 'nbytes', 'parsemag', $wgLang->formatNum( $this->file->getSize() ) ) .
1567 ')';
1568 $pageLink = $this->getLink();
1569
1570 return '<li>' . $this->getLink() . ' ' . $this->getUserTools() . ' ' .
1571 $data . ' ' . $this->getComment(). '</li>';
1572 }
1573 }
1574
1575 /**
1576 * List for filearchive table items
1577 */
1578 class RevDel_ArchivedFileList extends RevDel_FileList {
1579 var $type = 'filearchive';
1580 var $idField = 'fa_id';
1581 var $dateField = 'fa_timestamp';
1582 var $authorIdField = 'fa_user';
1583 var $authorNameField = 'fa_user_text';
1584
1585 public function doQuery( $db ) {
1586 $ids = array_map( 'intval', $this->ids );
1587 return $db->select( 'filearchive', '*',
1588 array(
1589 'fa_name' => $this->title->getDBkey(),
1590 'fa_id' => $ids
1591 ),
1592 __METHOD__
1593 );
1594 }
1595
1596 public function newItem( $row ) {
1597 return new RevDel_ArchivedFileItem( $this, $row );
1598 }
1599 }
1600
1601 /**
1602 * Item class for a filearchive table row
1603 */
1604 class RevDel_ArchivedFileItem extends RevDel_FileItem {
1605 public function __construct( $list, $row ) {
1606 RevDel_Item::__construct( $list, $row );
1607 $this->file = ArchivedFile::newFromRow( $row );
1608 }
1609
1610 public function getId() {
1611 return $this->row->fa_id;
1612 }
1613
1614 public function setBits( $bits ) {
1615 $dbw = wfGetDB( DB_MASTER );
1616 $dbw->update( 'filearchive',
1617 array( 'fa_deleted' => $bits ),
1618 array(
1619 'fa_id' => $this->row->fa_id,
1620 'fa_deleted' => $this->getBits(),
1621 ),
1622 __METHOD__
1623 );
1624 return (bool)$dbw->affectedRows();
1625 }
1626
1627 protected function getLink() {
1628 global $wgLang, $wgUser;
1629 $date = $wgLang->timeanddate( $this->file->getTimestamp(), true );
1630 $undelete = SpecialPage::getTitleFor( 'Undelete' );
1631 $key = $this->file->getKey();
1632 # Hidden files...
1633 if( !$this->canViewContent() ) {
1634 $link = $date;
1635 } else {
1636 $link = $this->special->skin->link( $undelete, $date, array(),
1637 array(
1638 'target' => $this->list->title->getPrefixedText(),
1639 'file' => $key,
1640 'token' => $wgUser->editToken( $key )
1641 )
1642 );
1643 }
1644 if( $this->isDeleted() ) {
1645 $link = '<span class="history-deleted">' . $link . '</span>';
1646 }
1647 return $link;
1648 }
1649 }
1650
1651 /**
1652 * List for logging table items
1653 */
1654 class RevDel_LogList extends RevDel_List {
1655 var $type = 'logging';
1656 var $idField = 'log_id';
1657 var $dateField = 'log_timestamp';
1658 var $authorIdField = 'log_user';
1659 var $authorNameField = 'log_user_text';
1660
1661 public function doQuery( $db ) {
1662 global $wgMessageCache;
1663 $wgMessageCache->loadAllMessages();
1664 $ids = array_map( 'intval', $this->ids );
1665 return $db->select( 'logging', '*',
1666 array( 'log_id' => $ids ),
1667 __METHOD__
1668 );
1669 }
1670
1671 public function newItem( $row ) {
1672 return new RevDel_LogItem( $this, $row );
1673 }
1674
1675 public function getSuppressBit() {
1676 return Revision::DELETED_RESTRICTED;
1677 }
1678
1679 public function getLogAction() {
1680 return 'event';
1681 }
1682
1683 public function getLogParams( $params ) {
1684 return array(
1685 implode( ',', $params['ids'] ),
1686 "ofield={$params['oldBits']}",
1687 "nfield={$params['newBits']}"
1688 );
1689 }
1690 }
1691
1692 /**
1693 * Item class for a logging table row
1694 */
1695 class RevDel_LogItem extends RevDel_Item {
1696 public function canView() {
1697 return LogEventsList::userCan( $this->row, Revision::DELETED_RESTRICTED );
1698 }
1699
1700 public function canViewContent() {
1701 return true; // none
1702 }
1703
1704 public function getBits() {
1705 return $this->row->log_deleted;
1706 }
1707
1708 public function setBits( $bits ) {
1709 $dbw = wfGetDB( DB_MASTER );
1710 $dbw->update( 'recentchanges',
1711 array(
1712 'rc_deleted' => $bits,
1713 'rc_patrolled' => 1
1714 ),
1715 array(
1716 'rc_logid' => $this->row->log_id,
1717 'rc_timestamp' => $this->row->log_timestamp // index
1718 ),
1719 __METHOD__
1720 );
1721 $dbw->update( 'logging',
1722 array( 'log_deleted' => $bits ),
1723 array(
1724 'log_id' => $this->row->log_id,
1725 'log_deleted' => $this->getBits()
1726 ),
1727 __METHOD__
1728 );
1729 return (bool)$dbw->affectedRows();
1730 }
1731
1732 public function getHTML() {
1733 global $wgLang;
1734
1735 $date = htmlspecialchars( $wgLang->timeanddate( $this->row->log_timestamp ) );
1736 $paramArray = LogPage::extractParams( $this->row->log_params );
1737 $title = Title::makeTitle( $this->row->log_namespace, $this->row->log_title );
1738
1739 // Log link for this page
1740 $loglink = $this->special->skin->link(
1741 SpecialPage::getTitleFor( 'Log' ),
1742 wfMsgHtml( 'log' ),
1743 array(),
1744 array( 'page' => $title->getPrefixedText() )
1745 );
1746 // Action text
1747 if( !$this->canView() ) {
1748 $action = '<span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>';
1749 } else {
1750 $action = LogPage::actionText( $this->row->log_type, $this->row->log_action, $title,
1751 $this->special->skin, $paramArray, true, true );
1752 if( $this->row->log_deleted & LogPage::DELETED_ACTION )
1753 $action = '<span class="history-deleted">' . $action . '</span>';
1754 }
1755 // User links
1756 $userLink = $this->special->skin->userLink( $this->row->log_user,
1757 User::WhoIs( $this->row->log_user ) );
1758 if( LogEventsList::isDeleted($this->row,LogPage::DELETED_USER) ) {
1759 $userLink = '<span class="history-deleted">' . $userLink . '</span>';
1760 }
1761 // Comment
1762 $comment = $wgLang->getDirMark() . $this->special->skin->commentBlock( $this->row->log_comment );
1763 if( LogEventsList::isDeleted($this->row,LogPage::DELETED_COMMENT) ) {
1764 $comment = '<span class="history-deleted">' . $comment . '</span>';
1765 }
1766 return "<li>($loglink) $date $userLink $action $comment</li>";
1767 }
1768 }