Tweak/fixes to r49149
[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
12 public function __construct() {
13 parent::__construct( 'Revisiondelete', 'deleterevision' );
14 $this->includable( false ); // paranoia
15 }
16
17 public function execute( $par ) {
18 global $wgOut, $wgUser, $wgRequest;
19 if( !$wgUser->isAllowed( 'deleterevision' ) ) {
20 return $wgOut->permissionRequired( 'deleterevision' );
21 } else if( wfReadOnly() ) {
22 return $wgOut->readOnlyPage();
23 }
24 $this->skin =& $wgUser->getSkin();
25 $this->setHeaders();
26 $this->outputHeader();
27 $this->wasPosted = $wgRequest->wasPosted();
28 # Set title and such
29 $this->target = $wgRequest->getText( 'target' );
30 # Handle our many different possible input types.
31 # Use CVS, since the cgi handling will break on arrays.
32 $this->oldids = array_filter( explode( ',', $wgRequest->getVal('oldid') ) );
33 $this->artimestamps = array_filter( explode( ',', $wgRequest->getVal('artimestamp') ) );
34 $this->logids = array_filter( explode( ',', $wgRequest->getVal('logid') ) );
35 $this->oldimgs = array_filter( explode( ',', $wgRequest->getVal('oldimage') ) );
36 $this->fileids = array_filter( explode( ',', $wgRequest->getVal('fileid') ) );
37 # For reviewing deleted files...
38 $this->file = $wgRequest->getVal( 'file' );
39 # Only one target set at a time please!
40 $types = (bool)$this->file + (bool)$this->oldids + (bool)$this->logids
41 + (bool)$this->artimestamps + (bool)$this->fileids + (bool)$this->oldimgs;
42 # No targets?
43 if( $types == 0 ) {
44 $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
45 return;
46 # Too many targets?
47 } else if( $types > 1 ) {
48 $wgOut->showErrorPage( 'revdelete-toomanytargets-title', 'revdelete-toomanytargets-text' );
49 return;
50 }
51 $this->page = Title::newFromUrl( $this->target );
52 $this->contextPage = Title::newFromUrl( $wgRequest->getText( 'page' ) );
53 # If we have revisions, get the title from the first one
54 # since they should all be from the same page. This allows
55 # for more flexibility with page moves...
56 if( count($this->oldids) > 0 ) {
57 $rev = Revision::newFromId( $this->oldids[0] );
58 $this->page = $rev ? $rev->getTitle() : $this->page;
59 }
60 # We need a target page!
61 if( is_null($this->page) ) {
62 return $wgOut->addWikiMsg( 'undelete-header' );
63 }
64 # For reviewing deleted files...show it now if allowed
65 if( $this->file ) {
66 return $this->tryShowFile( $this->file );
67 }
68 # Logs must have a type given
69 if( $this->logids && !strpos($this->page->getDBKey(),'/') ) {
70 return $wgOut->showErrorPage( 'revdelete-nologtype-title', 'revdelete-nologtype-text' );
71 }
72 # Give a link to the logs/hist for this page
73 $this->showConvenienceLinks();
74 # Lock the operation and the form context
75 $this->secureOperation();
76 # Either submit or create our form
77 if( $this->wasPosted ) {
78 $this->submit( $wgRequest );
79 } else if( $this->deleteKey == 'oldid' || $this->deleteKey == 'artimestamp' ) {
80 $this->showRevs();
81 } else if( $this->deleteKey == 'fileid' || $this->deleteKey == 'oldimage' ) {
82 $this->showImages();
83 } else if( $this->deleteKey == 'logid' ) {
84 $this->showLogItems();
85 }
86 list($qc,$lim) = $this->getLogQueryCond();
87 # Show relevant lines from the deletion log
88 $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" );
89 LogEventsList::showLogExtract( $wgOut, 'delete', $this->page->getPrefixedText(), '', $lim, $qc );
90 # Show relevant lines from the suppression log
91 if( $wgUser->isAllowed( 'suppressionlog' ) ) {
92 $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'suppress' ) ) . "</h2>\n" );
93 LogEventsList::showLogExtract( $wgOut, 'suppress', $this->page->getPrefixedText(), '', $lim, $qc );
94 }
95 }
96
97 private function showConvenienceLinks() {
98 global $wgOut, $wgUser;
99 # Give a link to the logs/hist for this page
100 if( !is_null($this->page) && $this->page->getNamespace() > -1 ) {
101 $links = array();
102 $logtitle = SpecialPage::getTitleFor( 'Log' );
103 $links[] = $this->skin->makeKnownLinkObj( $logtitle, wfMsgHtml( 'viewpagelogs' ),
104 wfArrayToCGI( array( 'page' => $this->page->getPrefixedUrl() ) ) );
105 # Give a link to the page history
106 $links[] = $this->skin->makeKnownLinkObj( $this->page, wfMsgHtml( 'pagehist' ),
107 wfArrayToCGI( array( 'action' => 'history' ) ) );
108 # Link to deleted edits
109 if( $wgUser->isAllowed('undelete') ) {
110 $undelete = SpecialPage::getTitleFor( 'Undelete' );
111 $links[] = $this->skin->makeKnownLinkObj( $undelete, wfMsgHtml( 'deletedhist' ),
112 wfArrayToCGI( array( 'target' => $this->page->getPrefixedDBkey() ) ) );
113 }
114 # Logs themselves don't have histories or archived revisions
115 $wgOut->setSubtitle( '<p>'.implode($links,' / ').'</p>' );
116 }
117 }
118
119 private function getLogQueryCond() {
120 $ids = $safeIds = array();
121 $action = 'revision';
122 $limit = 25; // default
123 switch( $this->deleteKey ) {
124 case 'oldid':
125 $ids = $this->oldids;
126 break;
127 case 'artimestamp':
128 $ids = $this->artimestamps;
129 break;
130 case 'oldimage':
131 $ids = $this->oldimgs;
132 break;
133 case 'fileid':
134 $ids = $this->fileids;
135 break;
136 case 'logid':
137 $ids = $this->logids;
138 $action = 'event';
139 break;
140 }
141 // Revision delete logs
142 $conds = array( 'log_action' => $action );
143 // Just get the whole log if there are a lot if items
144 if( count($ids) > $limit )
145 return array($conds,$limit);
146 // Digit chars only
147 foreach( $ids as $id ) {
148 if( preg_match( '/^\d+$/', $id, $m ) ) {
149 $safeIds[] = $m[0];
150 }
151 }
152 // Optimization for logs
153 if( $action == 'event' ) {
154 $dbr = wfGetDB( DB_SLAVE );
155 # Get the timestamp of the first item
156 $first = $dbr->selectField( 'logging', 'log_timestamp',
157 array('log_id' => $safeIds), __METHOD__, array('ORDER BY' => 'log_id') );
158 # If there are no items, then stop here
159 if( $first == false ) $conds = '1 = 0';
160 # The event was be hidden after it was made
161 $conds[] = 'log_timestamp > '.$dbr->addQuotes($first); // type,time index
162 }
163 // Format is <id1,id2,i3...>
164 if( count($safeIds) ) {
165 $conds[] = "log_params RLIKE '(^|\n|,)(".implode('|',$safeIds).")(,|$)'";
166 }
167 return array($conds,$limit);
168 }
169
170 private function secureOperation() {
171 global $wgUser;
172 $this->deleteKey = '';
173 // At this point, we should only have one of these
174 if( $this->oldids ) {
175 $this->revisions = $this->oldids;
176 $hide_content_name = array( 'revdelete-hide-text', 'wpHideText', Revision::DELETED_TEXT );
177 $this->deleteKey = 'oldid';
178 } else if( $this->artimestamps ) {
179 $this->archrevs = $this->artimestamps;
180 $hide_content_name = array( 'revdelete-hide-text', 'wpHideText', Revision::DELETED_TEXT );
181 $this->deleteKey = 'artimestamp';
182 } else if( $this->oldimgs ) {
183 $this->ofiles = $this->oldimgs;
184 $hide_content_name = array( 'revdelete-hide-image', 'wpHideImage', File::DELETED_FILE );
185 $this->deleteKey = 'oldimage';
186 } else if( $this->fileids ) {
187 $this->afiles = $this->fileids;
188 $hide_content_name = array( 'revdelete-hide-image', 'wpHideImage', File::DELETED_FILE );
189 $this->deleteKey = 'fileid';
190 } else if( $this->logids ) {
191 $this->events = $this->logids;
192 $hide_content_name = array( 'revdelete-hide-name', 'wpHideName', LogPage::DELETED_ACTION );
193 $this->deleteKey = 'logid';
194 }
195 // Our checkbox messages depends one what we are doing,
196 // e.g. we don't hide "text" for logs or images
197 $this->checks = array(
198 $hide_content_name,
199 array( 'revdelete-hide-comment', 'wpHideComment', Revision::DELETED_COMMENT ),
200 array( 'revdelete-hide-user', 'wpHideUser', Revision::DELETED_USER )
201 );
202 if( $wgUser->isAllowed('suppressrevision') ) {
203 $this->checks[] = array( 'revdelete-hide-restricted',
204 'wpHideRestricted', Revision::DELETED_RESTRICTED );
205 }
206 }
207
208 /**
209 * Show a deleted file version requested by the visitor.
210 */
211 private function tryShowFile( $key ) {
212 global $wgOut, $wgRequest;
213 $oimage = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $this->page, $key );
214 $oimage->load();
215 // Check if user is allowed to see this file
216 if( !$oimage->userCan(File::DELETED_FILE) ) {
217 $wgOut->permissionRequired( 'suppressrevision' );
218 } else {
219 $wgOut->disable();
220 # We mustn't allow the output to be Squid cached, otherwise
221 # if an admin previews a deleted image, and it's cached, then
222 # a user without appropriate permissions can toddle off and
223 # nab the image, and Squid will serve it
224 $wgRequest->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
225 $wgRequest->response()->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
226 $wgRequest->response()->header( 'Pragma: no-cache' );
227 # Stream the file to the client
228 $store = FileStore::get( 'deleted' );
229 $store->stream( $key );
230 }
231 }
232
233 /**
234 * This lets a user set restrictions for live and archived revisions
235 */
236 private function showRevs() {
237 global $wgOut, $wgUser;
238 $UserAllowed = true;
239
240 $count = ($this->deleteKey == 'oldid') ?
241 count($this->revisions) : count($this->archrevs);
242 $wgOut->addWikiMsg( 'revdelete-selected', $this->page->getPrefixedText(), $count );
243
244 $bitfields = 0;
245 $wgOut->addHTML( "<ul>" );
246
247 $where = $revObjs = array();
248 $dbr = wfGetDB( DB_MASTER );
249
250 $revisions = 0;
251 // Live revisions...
252 if( $this->deleteKey == 'oldid' ) {
253 // Run through and pull all our data in one query
254 foreach( $this->revisions as $revid ) {
255 $where[] = intval($revid);
256 }
257 $result = $dbr->select( array('revision','page'), '*',
258 array(
259 'rev_page' => $this->page->getArticleID(),
260 'rev_id' => $where,
261 'rev_page = page_id' ),
262 __METHOD__ );
263 while( $row = $dbr->fetchObject( $result ) ) {
264 $revObjs[$row->rev_id] = new Revision( $row );
265 }
266 foreach( $this->revisions as $revid ) {
267 // Hiding top revisison is bad
268 if( !isset($revObjs[$revid]) || $revObjs[$revid]->isCurrent() ) {
269 continue;
270 } else if( !$revObjs[$revid]->userCan(Revision::DELETED_RESTRICTED) ) {
271 // If a rev is hidden from sysops
272 if( !$this->wasPosted ) {
273 $wgOut->permissionRequired( 'suppressrevision' );
274 return;
275 }
276 $UserAllowed = false;
277 }
278 $revisions++;
279 $wgOut->addHTML( $this->historyLine( $revObjs[$revid] ) );
280 $bitfields |= $revObjs[$revid]->mDeleted;
281 }
282 // The archives...
283 } else {
284 // Run through and pull all our data in one query
285 foreach( $this->archrevs as $timestamp ) {
286 $where[] = $dbr->timestamp( $timestamp );
287 }
288 $result = $dbr->select( 'archive', '*',
289 array(
290 'ar_namespace' => $this->page->getNamespace(),
291 'ar_title' => $this->page->getDBKey(),
292 'ar_timestamp' => $where ),
293 __METHOD__ );
294 while( $row = $dbr->fetchObject( $result ) ) {
295 $timestamp = wfTimestamp( TS_MW, $row->ar_timestamp );
296 $revObjs[$timestamp] = new Revision( array(
297 'page' => $this->page->getArticleId(),
298 'id' => $row->ar_rev_id,
299 'text' => $row->ar_text_id,
300 'comment' => $row->ar_comment,
301 'user' => $row->ar_user,
302 'user_text' => $row->ar_user_text,
303 'timestamp' => $timestamp,
304 'minor_edit' => $row->ar_minor_edit,
305 'text_id' => $row->ar_text_id,
306 'deleted' => $row->ar_deleted,
307 'len' => $row->ar_len) );
308 }
309 foreach( $this->archrevs as $timestamp ) {
310 if( !isset($revObjs[$timestamp]) ) {
311 continue;
312 } else if( !$revObjs[$timestamp]->userCan(Revision::DELETED_RESTRICTED) ) {
313 // If a rev is hidden from sysops
314 if( !$this->wasPosted ) {
315 $wgOut->permissionRequired( 'suppressrevision' );
316 return;
317 }
318 $UserAllowed = false;
319 }
320 $revisions++;
321 $wgOut->addHTML( $this->historyLine( $revObjs[$timestamp] ) );
322 $bitfields |= $revObjs[$timestamp]->mDeleted;
323 }
324 }
325 if( !$revisions ) {
326 $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
327 return;
328 }
329
330 $wgOut->addHTML( "</ul>" );
331 // Explanation text
332 $this->addUsageText();
333
334 // Normal sysops can always see what they did, but can't always change it
335 if( !$UserAllowed ) return;
336
337 $items = array(
338 Xml::inputLabel( wfMsg( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ),
339 Xml::submitButton( wfMsg( 'revdelete-submit' ) )
340 );
341 $hidden = array(
342 Xml::hidden( 'wpEditToken', $wgUser->editToken() ),
343 Xml::hidden( 'target', $this->page->getPrefixedText() ),
344 Xml::hidden( 'type', $this->deleteKey )
345 );
346 if( $this->deleteKey == 'oldid' ) {
347 $hidden[] = Xml::hidden( 'oldid', implode(',',$this->oldids) );
348 } else {
349 $hidden[] = Xml::hidden( 'artimestamp', implode(',',$this->artimestamps) );
350 }
351 $special = SpecialPage::getTitleFor( 'Revisiondelete' );
352 $wgOut->addHTML(
353 Xml::openElement( 'form', array( 'method' => 'post',
354 'action' => $special->getLocalUrl( 'action=submit' ),
355 'id' => 'mw-revdel-form-revisions' ) ) .
356 Xml::openElement( 'fieldset' ) .
357 xml::element( 'legend', null, wfMsg( 'revdelete-legend' ) )
358 );
359
360 $wgOut->addHTML( $this->buildCheckBoxes( $bitfields ) );
361 foreach( $items as $item ) {
362 $wgOut->addHTML( Xml::tags( 'p', null, $item ) );
363 }
364 foreach( $hidden as $item ) {
365 $wgOut->addHTML( $item );
366 }
367 $wgOut->addHTML(
368 Xml::closeElement( 'fieldset' ) .
369 Xml::closeElement( 'form' ) . "\n"
370 );
371 }
372
373 /**
374 * This lets a user set restrictions for archived images
375 */
376 private function showImages() {
377 global $wgOut, $wgUser, $wgLang;
378 $UserAllowed = true;
379
380 $count = ($this->deleteKey == 'oldimage') ? count($this->ofiles) : count($this->afiles);
381 $wgOut->addWikiMsg( 'revdelete-selected', $this->page->getPrefixedText(),
382 $wgLang->formatNum($count) );
383
384 $bitfields = 0;
385 $wgOut->addHTML( "<ul>" );
386
387 $where = $filesObjs = array();
388 $dbr = wfGetDB( DB_MASTER );
389 // Live old revisions...
390 $revisions = 0;
391 if( $this->deleteKey == 'oldimage' ) {
392 // Run through and pull all our data in one query
393 foreach( $this->ofiles as $timestamp ) {
394 $where[] = $timestamp.'!'.$this->page->getDBKey();
395 }
396 $result = $dbr->select( 'oldimage', '*',
397 array(
398 'oi_name' => $this->page->getDBKey(),
399 'oi_archive_name' => $where ),
400 __METHOD__ );
401 while( $row = $dbr->fetchObject( $result ) ) {
402 $filesObjs[$row->oi_archive_name] = RepoGroup::singleton()->getLocalRepo()->newFileFromRow( $row );
403 $filesObjs[$row->oi_archive_name]->user = $row->oi_user;
404 $filesObjs[$row->oi_archive_name]->user_text = $row->oi_user_text;
405 }
406 // Check through our images
407 foreach( $this->ofiles as $timestamp ) {
408 $archivename = $timestamp.'!'.$this->page->getDBKey();
409 if( !isset($filesObjs[$archivename]) ) {
410 continue;
411 } else if( !$filesObjs[$archivename]->userCan(File::DELETED_RESTRICTED) ) {
412 // If a rev is hidden from sysops
413 if( !$this->wasPosted ) {
414 $wgOut->permissionRequired( 'suppressrevision' );
415 return;
416 }
417 $UserAllowed = false;
418 }
419 $revisions++;
420 // Inject history info
421 $wgOut->addHTML( $this->fileLine( $filesObjs[$archivename] ) );
422 $bitfields |= $filesObjs[$archivename]->deleted;
423 }
424 // Archived files...
425 } else {
426 // Run through and pull all our data in one query
427 foreach( $this->afiles as $id ) {
428 $where[] = intval($id);
429 }
430 $result = $dbr->select( 'filearchive', '*',
431 array(
432 'fa_name' => $this->page->getDBKey(),
433 'fa_id' => $where ),
434 __METHOD__ );
435 while( $row = $dbr->fetchObject( $result ) ) {
436 $filesObjs[$row->fa_id] = ArchivedFile::newFromRow( $row );
437 }
438
439 foreach( $this->afiles as $fileid ) {
440 if( !isset($filesObjs[$fileid]) ) {
441 continue;
442 } else if( !$filesObjs[$fileid]->userCan(File::DELETED_RESTRICTED) ) {
443 // If a rev is hidden from sysops
444 if( !$this->wasPosted ) {
445 $wgOut->permissionRequired( 'suppressrevision' );
446 return;
447 }
448 $UserAllowed = false;
449 }
450 $revisions++;
451 // Inject history info
452 $wgOut->addHTML( $this->archivedfileLine( $filesObjs[$fileid] ) );
453 $bitfields |= $filesObjs[$fileid]->deleted;
454 }
455 }
456 if( !$revisions ) {
457 $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
458 return;
459 }
460
461 $wgOut->addHTML( "</ul>" );
462 // Explanation text
463 $this->addUsageText();
464 // Normal sysops can always see what they did, but can't always change it
465 if( !$UserAllowed ) return;
466
467 $items = array(
468 Xml::inputLabel( wfMsg( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ),
469 Xml::submitButton( wfMsg( 'revdelete-submit' ) )
470 );
471 $hidden = array(
472 Xml::hidden( 'wpEditToken', $wgUser->editToken() ),
473 Xml::hidden( 'target', $this->page->getPrefixedText() ),
474 Xml::hidden( 'type', $this->deleteKey )
475 );
476 if( $this->deleteKey == 'oldimage' ) {
477 $hidden[] = Xml::hidden( 'oldimage', implode(',',$this->oldimgs) );
478 } else {
479 $hidden[] = Xml::hidden( 'fileid', implode(',',$this->fileids) );
480 }
481 $special = SpecialPage::getTitleFor( 'Revisiondelete' );
482 $wgOut->addHTML(
483 Xml::openElement( 'form', array( 'method' => 'post',
484 'action' => $special->getLocalUrl( 'action=submit' ),
485 'id' => 'mw-revdel-form-filerevisions' )
486 ) .
487 Xml::fieldset( wfMsg( 'revdelete-legend' ) )
488 );
489
490 $wgOut->addHTML( $this->buildCheckBoxes( $bitfields ) );
491 foreach( $items as $item ) {
492 $wgOut->addHTML( "<p>$item</p>" );
493 }
494 foreach( $hidden as $item ) {
495 $wgOut->addHTML( $item );
496 }
497
498 $wgOut->addHTML(
499 Xml::closeElement( 'fieldset' ) .
500 Xml::closeElement( 'form' ) . "\n"
501 );
502 }
503
504 /**
505 * This lets a user set restrictions for log items
506 */
507 private function showLogItems() {
508 global $wgOut, $wgUser, $wgMessageCache, $wgLang;
509 $UserAllowed = true;
510
511 $wgOut->addWikiMsg( 'logdelete-selected', $wgLang->formatNum( count($this->events) ) );
512
513 $bitfields = 0;
514 $wgOut->addHTML( "<ul>" );
515
516 $where = $logRows = array();
517 $dbr = wfGetDB( DB_MASTER );
518 // Run through and pull all our data in one query
519 $logItems = 0;
520 foreach( $this->events as $logid ) {
521 $where[] = intval($logid);
522 }
523 list($log,$logtype) = explode( '/',$this->page->getDBKey(), 2 );
524 $result = $dbr->select( 'logging', '*',
525 array(
526 'log_type' => $logtype,
527 'log_id' => $where ),
528 __METHOD__ );
529 while( $row = $dbr->fetchObject( $result ) ) {
530 $logRows[$row->log_id] = $row;
531 }
532 $wgMessageCache->loadAllMessages();
533 foreach( $this->events as $logid ) {
534 // Don't hide from oversight log!!!
535 if( !isset( $logRows[$logid] ) || $logRows[$logid]->log_type=='suppress' ) {
536 continue;
537 } else if( !LogEventsList::userCan( $logRows[$logid],Revision::DELETED_RESTRICTED) ) {
538 // If an event is hidden from sysops
539 if( !$this->wasPosted ) {
540 $wgOut->permissionRequired( 'suppressrevision' );
541 return;
542 }
543 $UserAllowed = false;
544 }
545 $logItems++;
546 $wgOut->addHTML( $this->logLine( $logRows[$logid] ) );
547 $bitfields |= $logRows[$logid]->log_deleted;
548 }
549 if( !$logItems ) {
550 $wgOut->showErrorPage( 'revdelete-nologid-title', 'revdelete-nologid-text' );
551 return;
552 }
553
554 $wgOut->addHTML( "</ul>" );
555 // Explanation text
556 $this->addUsageText();
557 // Normal sysops can always see what they did, but can't always change it
558 if( !$UserAllowed ) return;
559
560 $items = array(
561 Xml::inputLabel( wfMsg( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ),
562 Xml::submitButton( wfMsg( 'revdelete-submit' ) ) );
563 $hidden = array(
564 Xml::hidden( 'wpEditToken', $wgUser->editToken() ),
565 Xml::hidden( 'target', $this->page->getPrefixedDBKey() ),
566 Xml::hidden( 'page', $this->contextPage ? $this->contextPage->getPrefixedDBKey() : '' ),
567 Xml::hidden( 'type', $this->deleteKey ),
568 Xml::hidden( 'logid', implode(',',$this->logids) )
569 );
570
571 $special = SpecialPage::getTitleFor( 'Revisiondelete' );
572 $wgOut->addHTML(
573 Xml::openElement( 'form', array( 'method' => 'post',
574 'action' => $special->getLocalUrl( 'action=submit' ), 'id' => 'mw-revdel-form-logs' ) ) .
575 Xml::fieldset( wfMsg( 'revdelete-legend' ) )
576 );
577
578 $wgOut->addHTML( $this->buildCheckBoxes( $bitfields ) );
579 foreach( $items as $item ) {
580 $wgOut->addHTML( "<p>$item</p>" );
581 }
582 foreach( $hidden as $item ) {
583 $wgOut->addHTML( $item );
584 }
585
586 $wgOut->addHTML(
587 Xml::closeElement( 'fieldset' ) .
588 Xml::closeElement( 'form' ) . "\n"
589 );
590 }
591
592 private function addUsageText() {
593 global $wgOut, $wgUser;
594 $wgOut->addWikiMsg( 'revdelete-text' );
595 if( $wgUser->isAllowed( 'suppressrevision' ) ) {
596 $wgOut->addWikiMsg( 'revdelete-suppress-text' );
597 }
598 }
599
600 /**
601 * @param int $bitfields, aggregate bitfield of all the bitfields
602 * @returns string HTML
603 */
604 private function buildCheckBoxes( $bitfields ) {
605 $html = '';
606 // FIXME: all items checked for just one rev are checked, even if not set for the others
607 foreach( $this->checks as $item ) {
608 list( $message, $name, $field ) = $item;
609 $line = Xml::tags( 'div', null, Xml::checkLabel( wfMsg($message), $name, $name,
610 $bitfields & $field ) );
611 if( $field == Revision::DELETED_RESTRICTED ) $line = "<b>$line</b>";
612 $html .= $line;
613 }
614 return $html;
615 }
616
617 /**
618 * @param Revision $rev
619 * @returns string
620 */
621 private function historyLine( $rev ) {
622 global $wgLang, $wgUser;
623
624 $date = $wgLang->timeanddate( $rev->getTimestamp() );
625 $difflink = $del = '';
626 // Live revisions
627 if( $this->deleteKey == 'oldid' ) {
628 $tokenParams = '&unhide=1&token='.urlencode( $wgUser->editToken( $rev->getId() ) );
629 $revlink = $this->skin->makeLinkObj( $this->page, $date, 'oldid='.$rev->getId() . $tokenParams );
630 $difflink = '(' . $this->skin->makeKnownLinkObj( $this->page, wfMsgHtml('diff'),
631 'diff=' . $rev->getId() . '&oldid=prev' . $tokenParams ) . ')';
632 // Archived revisions
633 } else {
634 $undelete = SpecialPage::getTitleFor( 'Undelete' );
635 $target = $this->page->getPrefixedText();
636 $revlink = $this->skin->makeLinkObj( $undelete, $date,
637 "target=$target&timestamp=" . $rev->getTimestamp() );
638 $difflink = '(' . $this->skin->makeKnownLinkObj( $undelete, wfMsgHtml('diff'),
639 "target=$target&diff=prev&timestamp=" . $rev->getTimestamp() ) . ')';
640 }
641 // Check permissions; items may be "suppressed"
642 if( $rev->isDeleted(Revision::DELETED_TEXT) ) {
643 $revlink = '<span class="history-deleted">'.$revlink.'</span>';
644 $del = ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
645 if( !$rev->userCan(Revision::DELETED_TEXT) ) {
646 $revlink = '<span class="history-deleted">'.$date.'</span>';
647 $difflink = '(' . wfMsgHtml('diff') . ')';
648 }
649 }
650 $userlink = $this->skin->revUserLink( $rev );
651 $comment = $this->skin->revComment( $rev );
652
653 return "<li>$difflink $revlink $userlink $comment{$del}</li>";
654 }
655
656 /**
657 * @param File $file
658 * @returns string
659 */
660 private function fileLine( $file ) {
661 global $wgLang, $wgTitle;
662
663 $target = $this->page->getPrefixedText();
664 $date = $wgLang->timeanddate( $file->getTimestamp(), true );
665
666 $del = '';
667 # Hidden files...
668 if( $file->isDeleted(File::DELETED_FILE) ) {
669 $del = ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
670 if( !$file->userCan(File::DELETED_FILE) ) {
671 $pageLink = $date;
672 } else {
673 $pageLink = $this->skin->makeKnownLinkObj( $wgTitle, $date,
674 "target=$target&file=$file->sha1.".$file->getExtension() );
675 }
676 $pageLink = '<span class="history-deleted">' . $pageLink . '</span>';
677 # Regular files...
678 } else {
679 $url = $file->getUrlRel();
680 $pageLink = "<a href=\"{$url}\">{$date}</a>";
681 }
682
683 $data = wfMsg( 'widthheight',
684 $wgLang->formatNum( $file->getWidth() ),
685 $wgLang->formatNum( $file->getHeight() ) ) .
686 ' (' . wfMsgExt( 'nbytes', 'parsemag', $wgLang->formatNum( $file->getSize() ) ) . ')';
687 $data = htmlspecialchars( $data );
688
689 return "<li>$pageLink ".$this->fileUserTools( $file )." $data ".
690 $this->fileComment( $file )."$del</li>";
691 }
692
693 /**
694 * @param ArchivedFile $file
695 * @returns string
696 */
697 private function archivedfileLine( $file ) {
698 global $wgLang;
699
700 $target = $this->page->getPrefixedText();
701 $date = $wgLang->timeanddate( $file->getTimestamp(), true );
702
703 $undelete = SpecialPage::getTitleFor( 'Undelete' );
704 $pageLink = $this->skin->makeKnownLinkObj( $undelete, $date,
705 "target=$target&file={$file->getKey()}" );
706
707 $del = '';
708 if( $file->isDeleted(File::DELETED_FILE) ) {
709 $del = ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
710 }
711
712 $data = wfMsg( 'widthheight',
713 $wgLang->formatNum( $file->getWidth() ),
714 $wgLang->formatNum( $file->getHeight() ) ) .
715 ' (' . wfMsgExt( 'nbytes', 'parsemag', $wgLang->formatNum( $file->getSize() ) ) . ')';
716 $data = htmlspecialchars( $data );
717
718 return "<li>$pageLink ".$this->fileUserTools( $file )." $data ".
719 $this->fileComment( $file )."$del</li>";
720 }
721
722 /**
723 * @param Array $row row
724 * @returns string
725 */
726 private function logLine( $row ) {
727 global $wgLang;
728
729 $date = $wgLang->timeanddate( $row->log_timestamp );
730 $paramArray = LogPage::extractParams( $row->log_params );
731 $title = Title::makeTitle( $row->log_namespace, $row->log_title );
732
733 $logtitle = SpecialPage::getTitleFor( 'Log' );
734 $loglink = $this->skin->makeKnownLinkObj( $logtitle, wfMsgHtml( 'log' ),
735 wfArrayToCGI( array( 'page' => $title->getPrefixedUrl() ) ) );
736 // Action text
737 if( !LogEventsList::userCan($row,LogPage::DELETED_ACTION) ) {
738 $action = '<span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>';
739 } else {
740 $action = LogPage::actionText( $row->log_type, $row->log_action, $title,
741 $this->skin, $paramArray, true, true );
742 if( $row->log_deleted & LogPage::DELETED_ACTION )
743 $action = '<span class="history-deleted">' . $action . '</span>';
744 }
745 // User links
746 $userLink = $this->skin->userLink( $row->log_user, User::WhoIs($row->log_user) );
747 if( LogEventsList::isDeleted($row,LogPage::DELETED_USER) ) {
748 $userLink = '<span class="history-deleted">' . $userLink . '</span>';
749 }
750 // Comment
751 $comment = $wgLang->getDirMark() . $this->skin->commentBlock( $row->log_comment );
752 if( LogEventsList::isDeleted($row,LogPage::DELETED_COMMENT) ) {
753 $comment = '<span class="history-deleted">' . $comment . '</span>';
754 }
755 return "<li>($loglink) $date $userLink $action $comment</li>";
756 }
757
758 /**
759 * Generate a user tool link cluster if the current user is allowed to view it
760 * @param ArchivedFile $file
761 * @return string HTML
762 */
763 private function fileUserTools( $file ) {
764 if( $file->userCan( Revision::DELETED_USER ) ) {
765 $link = $this->skin->userLink( $file->user, $file->user_text ) .
766 $this->skin->userToolLinks( $file->user, $file->user_text );
767 } else {
768 $link = wfMsgHtml( 'rev-deleted-user' );
769 }
770 if( $file->isDeleted( Revision::DELETED_USER ) ) {
771 return '<span class="history-deleted">' . $link . '</span>';
772 }
773 return $link;
774 }
775
776 /**
777 * Wrap and format the given file's comment block, if the current
778 * user is allowed to view it.
779 *
780 * @param ArchivedFile $file
781 * @return string HTML
782 */
783 private function fileComment( $file ) {
784 if( $file->userCan( File::DELETED_COMMENT ) ) {
785 $block = $this->skin->commentBlock( $file->description );
786 } else {
787 $block = ' ' . wfMsgHtml( 'rev-deleted-comment' );
788 }
789 if( $file->isDeleted( File::DELETED_COMMENT ) ) {
790 return "<span class=\"history-deleted\">$block</span>";
791 }
792 return $block;
793 }
794
795 /**
796 * @param WebRequest $request
797 */
798 private function submit( $request ) {
799 global $wgUser, $wgOut;
800 # Check edit token on submission
801 if( $this->wasPosted && !$wgUser->matchEditToken( $request->getVal('wpEditToken') ) ) {
802 $wgOut->addWikiMsg( 'sessionfailure' );
803 return false;
804 }
805 $bitfield = $this->extractBitfield( $request );
806 $comment = $request->getText( 'wpReason' );
807 # Can the user set this field?
808 if( $bitfield & Revision::DELETED_RESTRICTED && !$wgUser->isAllowed('suppressrevision') ) {
809 $wgOut->permissionRequired( 'suppressrevision' );
810 return false;
811 }
812 # If the save went through, go to success message. Otherwise
813 # bounce back to form...
814 if( $this->save( $bitfield, $comment, $this->page ) ) {
815 $this->success();
816 } else if( $request->getCheck( 'oldid' ) || $request->getCheck( 'artimestamp' ) ) {
817 return $this->showRevs();
818 } else if( $request->getCheck( 'logid' ) ) {
819 return $this->showLogs();
820 } else if( $request->getCheck( 'oldimage' ) || $request->getCheck( 'fileid' ) ) {
821 return $this->showImages();
822 }
823 }
824
825 private function success() {
826 global $wgOut;
827
828 $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
829
830 $wrap = '<span class="success">$1</span>';
831
832 if( $this->deleteKey == 'logid' ) {
833 $wgOut->wrapWikiMsg( $wrap, 'logdelete-success' );
834 $this->showLogItems();
835 } else if( $this->deleteKey == 'oldid' || $this->deleteKey == 'artimestamp' ) {
836 $wgOut->wrapWikiMsg( $wrap, 'revdelete-success' );
837 $this->showRevs();
838 } else if( $this->deleteKey == 'fileid' ) {
839 $wgOut->wrapWikiMsg( $wrap, 'revdelete-success' );
840 $this->showImages();
841 } else if( $this->deleteKey == 'oldimage' ) {
842 $wgOut->wrapWikiMsg( $wrap, 'revdelete-success' );
843 $this->showImages();
844 }
845 }
846
847 /**
848 * Put together a rev_deleted bitfield from the submitted checkboxes
849 * @param WebRequest $request
850 * @return int
851 */
852 private function extractBitfield( $request ) {
853 $bitfield = 0;
854 foreach( $this->checks as $item ) {
855 list( /* message */ , $name, $field ) = $item;
856 if( $request->getCheck( $name ) ) {
857 $bitfield |= $field;
858 }
859 }
860 return $bitfield;
861 }
862
863 private function save( $bitfield, $reason, $title ) {
864 $dbw = wfGetDB( DB_MASTER );
865 // Don't allow simply locking the interface for no reason
866 if( $bitfield == Revision::DELETED_RESTRICTED ) {
867 $bitfield = 0;
868 }
869 $deleter = new RevisionDeleter( $dbw );
870 // By this point, only one of the below should be set
871 if( isset($this->revisions) ) {
872 return $deleter->setRevVisibility( $title, $this->revisions, $bitfield, $reason );
873 } else if( isset($this->archrevs) ) {
874 return $deleter->setArchiveVisibility( $title, $this->archrevs, $bitfield, $reason );
875 } else if( isset($this->ofiles) ) {
876 return $deleter->setOldImgVisibility( $title, $this->ofiles, $bitfield, $reason );
877 } else if( isset($this->afiles) ) {
878 return $deleter->setArchFileVisibility( $title, $this->afiles, $bitfield, $reason );
879 } else if( isset($this->events) ) {
880 return $deleter->setEventVisibility( $title, $this->events, $bitfield, $reason );
881 }
882 }
883 }
884
885 /**
886 * Implements the actions for Revision Deletion.
887 * @ingroup SpecialPage
888 */
889 class RevisionDeleter {
890 function __construct( $db ) {
891 $this->dbw = $db;
892 }
893
894 /**
895 * @param $title, the page these events apply to
896 * @param array $items list of revision ID numbers
897 * @param int $bitfield new rev_deleted value
898 * @param string $comment Comment for log records
899 */
900 function setRevVisibility( $title, $items, $bitfield, $comment ) {
901 global $wgOut;
902
903 $userAllowedAll = $success = true;
904 $revIDs = array();
905 $revCount = 0;
906 // Run through and pull all our data in one query
907 foreach( $items as $revid ) {
908 $where[] = intval($revid);
909 }
910 $result = $this->dbw->select( 'revision', '*',
911 array(
912 'rev_page' => $title->getArticleID(),
913 'rev_id' => $where ),
914 __METHOD__ );
915 while( $row = $this->dbw->fetchObject( $result ) ) {
916 $revObjs[$row->rev_id] = new Revision( $row );
917 }
918 // To work!
919 foreach( $items as $revid ) {
920 if( !isset($revObjs[$revid]) || $revObjs[$revid]->isCurrent() ) {
921 $success = false;
922 continue; // Must exist
923 } else if( !$revObjs[$revid]->userCan(Revision::DELETED_RESTRICTED) ) {
924 $userAllowedAll=false;
925 continue;
926 }
927 // For logging, maintain a count of revisions
928 if( $revObjs[$revid]->mDeleted != $bitfield ) {
929 $revCount++;
930 $revIDs[]=$revid;
931
932 $this->updateRevision( $revObjs[$revid], $bitfield );
933 $this->updateRecentChangesEdits( $revObjs[$revid], $bitfield, false );
934 }
935 }
936 // Clear caches...
937 // Don't log or touch if nothing changed
938 if( $revCount > 0 ) {
939 $this->updateLog( $title, $revCount, $bitfield, $revObjs[$revid]->mDeleted,
940 $comment, $title, 'oldid', $revIDs );
941 $this->updatePage( $title );
942 }
943 // Where all revs allowed to be set?
944 if( !$userAllowedAll ) {
945 //FIXME: still might be confusing???
946 $wgOut->permissionRequired( 'suppressrevision' );
947 return false;
948 }
949
950 return $success;
951 }
952
953 /**
954 * @param $title, the page these events apply to
955 * @param array $items list of revision ID numbers
956 * @param int $bitfield new rev_deleted value
957 * @param string $comment Comment for log records
958 */
959 function setArchiveVisibility( $title, $items, $bitfield, $comment ) {
960 global $wgOut;
961
962 $userAllowedAll = $success = true;
963 $count = 0;
964 $Id_set = array();
965 // Run through and pull all our data in one query
966 foreach( $items as $timestamp ) {
967 $where[] = $this->dbw->timestamp( $timestamp );
968 }
969 $result = $this->dbw->select( 'archive', '*',
970 array(
971 'ar_namespace' => $title->getNamespace(),
972 'ar_title' => $title->getDBKey(),
973 'ar_timestamp' => $where ),
974 __METHOD__ );
975 while( $row = $this->dbw->fetchObject( $result ) ) {
976 $timestamp = wfTimestamp( TS_MW, $row->ar_timestamp );
977 $revObjs[$timestamp] = new Revision( array(
978 'page' => $title->getArticleId(),
979 'id' => $row->ar_rev_id,
980 'text' => $row->ar_text_id,
981 'comment' => $row->ar_comment,
982 'user' => $row->ar_user,
983 'user_text' => $row->ar_user_text,
984 'timestamp' => $timestamp,
985 'minor_edit' => $row->ar_minor_edit,
986 'text_id' => $row->ar_text_id,
987 'deleted' => $row->ar_deleted,
988 'len' => $row->ar_len) );
989 }
990 // To work!
991 foreach( $items as $timestamp ) {
992 // This will only select the first revision with this timestamp.
993 // Since they are all selected/deleted at once, we can just check the
994 // permissions of one. UPDATE is done via timestamp, so all revs are set.
995 if( !is_object($revObjs[$timestamp]) ) {
996 $success = false;
997 continue; // Must exist
998 } else if( !$revObjs[$timestamp]->userCan(Revision::DELETED_RESTRICTED) ) {
999 $userAllowedAll=false;
1000 continue;
1001 }
1002 // Which revisions did we change anything about?
1003 if( $revObjs[$timestamp]->mDeleted != $bitfield ) {
1004 $Id_set[]=$timestamp;
1005 $count++;
1006
1007 $this->updateArchive( $revObjs[$timestamp], $title, $bitfield );
1008 }
1009 }
1010 // For logging, maintain a count of revisions
1011 if( $count > 0 ) {
1012 $this->updateLog( $title, $count, $bitfield, $revObjs[$timestamp]->mDeleted,
1013 $comment, $title, 'artimestamp', $Id_set );
1014 }
1015 // Where all revs allowed to be set?
1016 if( !$userAllowedAll ) {
1017 $wgOut->permissionRequired( 'suppressrevision' );
1018 return false;
1019 }
1020
1021 return $success;
1022 }
1023
1024 /**
1025 * @param $title, the page these events apply to
1026 * @param array $items list of revision ID numbers
1027 * @param int $bitfield new rev_deleted value
1028 * @param string $comment Comment for log records
1029 */
1030 function setOldImgVisibility( $title, $items, $bitfield, $comment ) {
1031 global $wgOut;
1032
1033 $userAllowedAll = $success = true;
1034 $count = 0;
1035 $set = array();
1036 // Run through and pull all our data in one query
1037 foreach( $items as $timestamp ) {
1038 $where[] = $timestamp.'!'.$title->getDBKey();
1039 }
1040 $result = $this->dbw->select( 'oldimage', '*',
1041 array(
1042 'oi_name' => $title->getDBKey(),
1043 'oi_archive_name' => $where ),
1044 __METHOD__ );
1045 while( $row = $this->dbw->fetchObject( $result ) ) {
1046 $filesObjs[$row->oi_archive_name] = RepoGroup::singleton()->getLocalRepo()->newFileFromRow( $row );
1047 $filesObjs[$row->oi_archive_name]->user = $row->oi_user;
1048 $filesObjs[$row->oi_archive_name]->user_text = $row->oi_user_text;
1049 }
1050 // To work!
1051 foreach( $items as $timestamp ) {
1052 $archivename = $timestamp.'!'.$title->getDBKey();
1053 if( !isset($filesObjs[$archivename]) ) {
1054 $success = false;
1055 continue; // Must exist
1056 } else if( !$filesObjs[$archivename]->userCan(File::DELETED_RESTRICTED) ) {
1057 $userAllowedAll=false;
1058 continue;
1059 }
1060
1061 $transaction = true;
1062 // Which revisions did we change anything about?
1063 if( $filesObjs[$archivename]->deleted != $bitfield ) {
1064 $count++;
1065
1066 $this->dbw->begin();
1067 $this->updateOldFiles( $filesObjs[$archivename], $bitfield );
1068 // If this image is currently hidden...
1069 if( $filesObjs[$archivename]->deleted & File::DELETED_FILE ) {
1070 if( $bitfield & File::DELETED_FILE ) {
1071 # Leave it alone if we are not changing this...
1072 $set[]=$timestamp;
1073 $transaction = true;
1074 } else {
1075 # We are moving this out
1076 $transaction = $this->makeOldImagePublic( $filesObjs[$archivename] );
1077 $set[]=$transaction;
1078 }
1079 // Is it just now becoming hidden?
1080 } else if( $bitfield & File::DELETED_FILE ) {
1081 $transaction = $this->makeOldImagePrivate( $filesObjs[$archivename] );
1082 $set[]=$transaction;
1083 } else {
1084 $set[]=$timestamp;
1085 }
1086 // If our file operations fail, then revert back the db
1087 if( $transaction==false ) {
1088 $this->dbw->rollback();
1089 return false;
1090 }
1091 $this->dbw->commit();
1092 }
1093 }
1094
1095 // Log if something was changed
1096 if( $count > 0 ) {
1097 $this->updateLog( $title, $count, $bitfield, $filesObjs[$archivename]->deleted,
1098 $comment, $title, 'oldimage', $set );
1099 # Purge page/history
1100 $file = wfLocalFile( $title );
1101 $file->purgeCache();
1102 $file->purgeHistory();
1103 # Invalidate cache for all pages using this file
1104 $update = new HTMLCacheUpdate( $title, 'imagelinks' );
1105 $update->doUpdate();
1106 }
1107 // Where all revs allowed to be set?
1108 if( !$userAllowedAll ) {
1109 $wgOut->permissionRequired( 'suppressrevision' );
1110 return false;
1111 }
1112
1113 return $success;
1114 }
1115
1116 /**
1117 * @param $title, the page these events apply to
1118 * @param array $items list of revision ID numbers
1119 * @param int $bitfield new rev_deleted value
1120 * @param string $comment Comment for log records
1121 */
1122 function setArchFileVisibility( $title, $items, $bitfield, $comment ) {
1123 global $wgOut;
1124
1125 $userAllowedAll = $success = true;
1126 $count = 0;
1127 $Id_set = array();
1128
1129 // Run through and pull all our data in one query
1130 foreach( $items as $id ) {
1131 $where[] = intval($id);
1132 }
1133 $result = $this->dbw->select( 'filearchive', '*',
1134 array( 'fa_name' => $title->getDBKey(),
1135 'fa_id' => $where ),
1136 __METHOD__ );
1137 while( $row = $this->dbw->fetchObject( $result ) ) {
1138 $filesObjs[$row->fa_id] = ArchivedFile::newFromRow( $row );
1139 }
1140 // To work!
1141 foreach( $items as $fileid ) {
1142 if( !isset($filesObjs[$fileid]) ) {
1143 $success = false;
1144 continue; // Must exist
1145 } else if( !$filesObjs[$fileid]->userCan(File::DELETED_RESTRICTED) ) {
1146 $userAllowedAll=false;
1147 continue;
1148 }
1149 // Which revisions did we change anything about?
1150 if( $filesObjs[$fileid]->deleted != $bitfield ) {
1151 $Id_set[]=$fileid;
1152 $count++;
1153
1154 $this->updateArchFiles( $filesObjs[$fileid], $bitfield );
1155 }
1156 }
1157 // Log if something was changed
1158 if( $count > 0 ) {
1159 $this->updateLog( $title, $count, $bitfield, $comment,
1160 $filesObjs[$fileid]->deleted, $title, 'fileid', $Id_set );
1161 }
1162 // Where all revs allowed to be set?
1163 if( !$userAllowedAll ) {
1164 $wgOut->permissionRequired( 'suppressrevision' );
1165 return false;
1166 }
1167
1168 return $success;
1169 }
1170
1171 /**
1172 * @param $title, the log page these events apply to
1173 * @param array $items list of log ID numbers
1174 * @param int $bitfield new log_deleted value
1175 * @param string $comment Comment for log records
1176 */
1177 function setEventVisibility( $title, $items, $bitfield, $comment ) {
1178 global $wgOut;
1179
1180 $userAllowedAll = $success = true;
1181 $count = 0;
1182 $log_Ids = array();
1183
1184 // Run through and pull all our data in one query
1185 foreach( $items as $logid ) {
1186 $where[] = intval($logid);
1187 }
1188 list($log,$logtype) = explode( '/',$title->getDBKey(), 2 );
1189 $result = $this->dbw->select( 'logging', '*',
1190 array(
1191 'log_type' => $logtype,
1192 'log_id' => $where ),
1193 __METHOD__ );
1194 while( $row = $this->dbw->fetchObject( $result ) ) {
1195 $logRows[$row->log_id] = $row;
1196 }
1197 // To work!
1198 foreach( $items as $logid ) {
1199 if( !isset($logRows[$logid]) ) {
1200 $success = false;
1201 continue; // Must exist
1202 } else if( !LogEventsList::userCan($logRows[$logid], LogPage::DELETED_RESTRICTED)
1203 || $logRows[$logid]->log_type == 'suppress' ) {
1204 // Don't hide from oversight log!!!
1205 $userAllowedAll=false;
1206 continue;
1207 }
1208 // Which logs did we change anything about?
1209 if( $logRows[$logid]->log_deleted != $bitfield ) {
1210 $log_Ids[]=$logid;
1211 $count++;
1212
1213 $this->updateLogs( $logRows[$logid], $bitfield );
1214 $this->updateRecentChangesLog( $logRows[$logid], $bitfield, true );
1215 }
1216 }
1217 // Don't log or touch if nothing changed
1218 if( $count > 0 ) {
1219 $this->updateLog( $title, $count, $bitfield, $logRows[$logid]->log_deleted,
1220 $comment, $title, 'logid', $log_Ids );
1221 }
1222 // Were all revs allowed to be set?
1223 if( !$userAllowedAll ) {
1224 $wgOut->permissionRequired( 'suppressrevision' );
1225 return false;
1226 }
1227
1228 return $success;
1229 }
1230
1231 /**
1232 * Moves an image to a safe private location
1233 * Caller is responsible for clearing caches
1234 * @param File $oimage
1235 * @returns mixed, timestamp string on success, false on failure
1236 */
1237 function makeOldImagePrivate( $oimage ) {
1238 $transaction = new FSTransaction();
1239 if( !FileStore::lock() ) {
1240 wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" );
1241 return false;
1242 }
1243 $oldpath = $oimage->getArchivePath() . DIRECTORY_SEPARATOR . $oimage->archive_name;
1244 // Dupe the file into the file store
1245 if( file_exists( $oldpath ) ) {
1246 // Is our directory configured?
1247 if( $store = FileStore::get( 'deleted' ) ) {
1248 if( !$oimage->sha1 ) {
1249 $oimage->upgradeRow(); // sha1 may be missing
1250 }
1251 $key = $oimage->sha1 . '.' . $oimage->getExtension();
1252 $transaction->add( $store->insert( $key, $oldpath, FileStore::DELETE_ORIGINAL ) );
1253 } else {
1254 $group = null;
1255 $key = null;
1256 $transaction = false; // Return an error and do nothing
1257 }
1258 } else {
1259 wfDebug( __METHOD__." deleting already-missing '$oldpath'; moving on to database\n" );
1260 $group = null;
1261 $key = '';
1262 $transaction = new FSTransaction(); // empty
1263 }
1264
1265 if( $transaction === false ) {
1266 // Fail to restore?
1267 wfDebug( __METHOD__.": import to file store failed, aborting\n" );
1268 throw new MWException( "Could not archive and delete file $oldpath" );
1269 return false;
1270 }
1271
1272 wfDebug( __METHOD__.": set db items, applying file transactions\n" );
1273 $transaction->commit();
1274 FileStore::unlock();
1275
1276 $m = explode('!',$oimage->archive_name,2);
1277 $timestamp = $m[0];
1278
1279 return $timestamp;
1280 }
1281
1282 /**
1283 * Moves an image from a safe private location
1284 * Caller is responsible for clearing caches
1285 * @param File $oimage
1286 * @returns mixed, string timestamp on success, false on failure
1287 */
1288 function makeOldImagePublic( $oimage ) {
1289 $transaction = new FSTransaction();
1290 if( !FileStore::lock() ) {
1291 wfDebug( __METHOD__." could not acquire filestore lock\n" );
1292 return false;
1293 }
1294
1295 $store = FileStore::get( 'deleted' );
1296 if( !$store ) {
1297 wfDebug( __METHOD__.": skipping row with no file.\n" );
1298 return false;
1299 }
1300
1301 $key = $oimage->sha1.'.'.$oimage->getExtension();
1302 $destDir = $oimage->getArchivePath();
1303 if( !is_dir( $destDir ) ) {
1304 wfMkdirParents( $destDir );
1305 }
1306 $destPath = $destDir . DIRECTORY_SEPARATOR . $oimage->archive_name;
1307 // Check if any other stored revisions use this file;
1308 // if so, we shouldn't remove the file from the hidden
1309 // archives so they will still work. Check hidden files first.
1310 $useCount = $this->dbw->selectField( 'oldimage', '1',
1311 array( 'oi_sha1' => $oimage->sha1,
1312 'oi_deleted & '.File::DELETED_FILE => File::DELETED_FILE ),
1313 __METHOD__, array( 'FOR UPDATE' ) );
1314 // Check the rest of the deleted archives too.
1315 // (these are the ones that don't show in the image history)
1316 if( !$useCount ) {
1317 $useCount = $this->dbw->selectField( 'filearchive', '1',
1318 array( 'fa_storage_group' => 'deleted', 'fa_storage_key' => $key ),
1319 __METHOD__, array( 'FOR UPDATE' ) );
1320 }
1321
1322 if( $useCount == 0 ) {
1323 wfDebug( __METHOD__.": nothing else using {$oimage->sha1}, will deleting after\n" );
1324 $flags = FileStore::DELETE_ORIGINAL;
1325 } else {
1326 $flags = 0;
1327 }
1328 $transaction->add( $store->export( $key, $destPath, $flags ) );
1329
1330 wfDebug( __METHOD__.": set db items, applying file transactions\n" );
1331 $transaction->commit();
1332 FileStore::unlock();
1333
1334 $m = explode('!',$oimage->archive_name,2);
1335 $timestamp = $m[0];
1336
1337 return $timestamp;
1338 }
1339
1340 /**
1341 * Update the revision's rev_deleted field
1342 * @param Revision $rev
1343 * @param int $bitfield new rev_deleted bitfield value
1344 */
1345 function updateRevision( $rev, $bitfield ) {
1346 $this->dbw->update( 'revision',
1347 array( 'rev_deleted' => $bitfield ),
1348 array( 'rev_id' => $rev->getId(),
1349 'rev_page' => $rev->getPage() ),
1350 __METHOD__ );
1351 }
1352
1353 /**
1354 * Update the revision's rev_deleted field
1355 * @param Revision $rev
1356 * @param Title $title
1357 * @param int $bitfield new rev_deleted bitfield value
1358 */
1359 function updateArchive( $rev, $title, $bitfield ) {
1360 $this->dbw->update( 'archive',
1361 array( 'ar_deleted' => $bitfield ),
1362 array( 'ar_namespace' => $title->getNamespace(),
1363 'ar_title' => $title->getDBKey(),
1364 'ar_timestamp' => $this->dbw->timestamp( $rev->getTimestamp() ),
1365 'ar_rev_id' => $rev->getId() ),
1366 __METHOD__ );
1367 }
1368
1369 /**
1370 * Update the images's oi_deleted field
1371 * @param File $file
1372 * @param int $bitfield new rev_deleted bitfield value
1373 */
1374 function updateOldFiles( $file, $bitfield ) {
1375 $this->dbw->update( 'oldimage',
1376 array( 'oi_deleted' => $bitfield ),
1377 array( 'oi_name' => $file->getName(),
1378 'oi_timestamp' => $this->dbw->timestamp( $file->getTimestamp() ) ),
1379 __METHOD__ );
1380 }
1381
1382 /**
1383 * Update the images's fa_deleted field
1384 * @param ArchivedFile $file
1385 * @param int $bitfield new rev_deleted bitfield value
1386 */
1387 function updateArchFiles( $file, $bitfield ) {
1388 $this->dbw->update( 'filearchive',
1389 array( 'fa_deleted' => $bitfield ),
1390 array( 'fa_id' => $file->getId() ),
1391 __METHOD__ );
1392 }
1393
1394 /**
1395 * Update the logging log_deleted field
1396 * @param Row $row
1397 * @param int $bitfield new rev_deleted bitfield value
1398 */
1399 function updateLogs( $row, $bitfield ) {
1400 $this->dbw->update( 'logging',
1401 array( 'log_deleted' => $bitfield ),
1402 array( 'log_id' => $row->log_id ),
1403 __METHOD__ );
1404 }
1405
1406 /**
1407 * Update the revision's recentchanges record if fields have been hidden
1408 * @param Revision $rev
1409 * @param int $bitfield new rev_deleted bitfield value
1410 */
1411 function updateRecentChangesEdits( $rev, $bitfield ) {
1412 $this->dbw->update( 'recentchanges',
1413 array( 'rc_deleted' => $bitfield,
1414 'rc_patrolled' => 1 ),
1415 array( 'rc_this_oldid' => $rev->getId(),
1416 'rc_timestamp' => $this->dbw->timestamp( $rev->getTimestamp() ) ),
1417 __METHOD__ );
1418 }
1419
1420 /**
1421 * Update the revision's recentchanges record if fields have been hidden
1422 * @param Row $row
1423 * @param int $bitfield new rev_deleted bitfield value
1424 */
1425 function updateRecentChangesLog( $row, $bitfield ) {
1426 $this->dbw->update( 'recentchanges',
1427 array( 'rc_deleted' => $bitfield,
1428 'rc_patrolled' => 1 ),
1429 array( 'rc_logid' => $row->log_id,
1430 'rc_timestamp' => $row->log_timestamp ),
1431 __METHOD__ );
1432 }
1433
1434 /**
1435 * Touch the page's cache invalidation timestamp; this forces cached
1436 * history views to refresh, so any newly hidden or shown fields will
1437 * update properly.
1438 * @param Title $title
1439 */
1440 function updatePage( $title ) {
1441 $title->invalidateCache();
1442 $title->purgeSquid();
1443 $title->touchLinks();
1444 // Extensions that require referencing previous revisions may need this
1445 wfRunHooks( 'ArticleRevisionVisiblitySet', array( &$title ) );
1446 }
1447
1448 /**
1449 * Checks for a change in the bitfield for a certain option and updates the
1450 * provided array accordingly.
1451 *
1452 * @param String $desc Description to add to the array if the option was
1453 * enabled / disabled.
1454 * @param int $field The bitmask describing the single option.
1455 * @param int $diff The xor of the old and new bitfields.
1456 * @param array $arr The array to update.
1457 */
1458 function checkItem( $desc, $field, $diff, $new, &$arr ) {
1459 if( $diff & $field ) {
1460 $arr[ ( $new & $field ) ? 0 : 1 ][] = $desc;
1461 }
1462 }
1463
1464 /**
1465 * Gets an array describing the changes made to the visibilit of the revision.
1466 * If the resulting array is $arr, then $arr[0] will contain an array of strings
1467 * describing the items that were hidden, $arr[2] will contain an array of strings
1468 * describing the items that were unhidden, and $arr[3] will contain an array with
1469 * a single string, which can be one of "applied restrictions to sysops",
1470 * "removed restrictions from sysops", or null.
1471 *
1472 * @param int $n The new bitfield.
1473 * @param int $o The old bitfield.
1474 * @return An array as described above.
1475 */
1476 function getChanges( $n, $o ) {
1477 $diff = $n ^ $o;
1478 $ret = array( 0 => array(), 1 => array(), 2 => array() );
1479 // Build bitfield changes in language
1480 $this->checkItem( wfMsgForContent( 'revdelete-content' ),
1481 Revision::DELETED_TEXT, $diff, $n, $ret );
1482 $this->checkItem( wfMsgForContent( 'revdelete-summary' ),
1483 Revision::DELETED_COMMENT, $diff, $n, $ret );
1484 $this->checkItem( wfMsgForContent( 'revdelete-uname' ),
1485 Revision::DELETED_USER, $diff, $n, $ret );
1486 // Restriction application to sysops
1487 if( $diff & Revision::DELETED_RESTRICTED ) {
1488 if( $n & Revision::DELETED_RESTRICTED )
1489 $ret[2][] = wfMsgForContent( 'revdelete-restricted' );
1490 else
1491 $ret[2][] = wfMsgForContent( 'revdelete-unrestricted' );
1492 }
1493 return $ret;
1494 }
1495
1496 /**
1497 * Gets a log message to describe the given revision visibility change. This
1498 * message will be of the form "[hid {content, edit summary, username}];
1499 * [unhid {...}][applied restrictions to sysops] for $count revisions: $comment".
1500 *
1501 * @param int $count The number of effected revisions.
1502 * @param int $nbitfield The new bitfield for the revision.
1503 * @param int $obitfield The old bitfield for the revision.
1504 * @param string $comment The comment associated with the change.
1505 * @param bool $isForLog
1506 */
1507 function getLogMessage( $count, $nbitfield, $obitfield, $comment, $isForLog = false ) {
1508 global $wgContLang;
1509
1510 $s = '';
1511 $changes = $this->getChanges( $nbitfield, $obitfield );
1512
1513 if( count( $changes[0] ) ) {
1514 $s .= wfMsgForContent ( 'revdelete-hid', implode ( ', ', $changes[0] ) );
1515 }
1516 if( count( $changes[1] ) ) {
1517 if ($s) $s .= '; ';
1518 $s .= wfMsgForContent ( 'revdelete-unhid', implode ( ', ', $changes[1] ) );
1519 }
1520 if( count( $changes[2] ) ) {
1521 $s .= $s ? ' (' . $changes[2][0] . ')' : $changes[2][0];
1522 }
1523
1524 $msg = $isForLog ? 'logdelete-log-message' : 'revdelete-log-message';
1525 $ret = wfMsgExt ( $msg, array( 'parsemag', 'content' ),
1526 $s, $wgContLang->formatNum( $count ) );
1527
1528 if( $comment ) $ret .= ": $comment";
1529
1530 return $ret;
1531
1532 }
1533
1534 /**
1535 * Record a log entry on the action
1536 * @param Title $title, page where item was removed from
1537 * @param int $count the number of revisions altered for this page
1538 * @param int $nbitfield the new _deleted value
1539 * @param int $obitfield the old _deleted value
1540 * @param string $comment
1541 * @param Title $target, the relevant page
1542 * @param string $param, URL param
1543 * @param Array $items
1544 */
1545 function updateLog( $title, $count, $nbitfield, $obitfield, $comment, $target,
1546 $param, $items = array() )
1547 {
1548 // Put things hidden from sysops in the oversight log
1549 $logtype = ( ($nbitfield | $obitfield) & Revision::DELETED_RESTRICTED ) ?
1550 'suppress' : 'delete';
1551 $log = new LogPage( $logtype );
1552
1553 $reason = $this->getLogMessage( $count, $nbitfield, $obitfield, $comment, $param == 'logid' );
1554
1555 if( $param == 'logid' ) {
1556 $params = array( implode( ',', $items) );
1557 $log->addEntry( 'event', $title, $reason, $params );
1558 } else {
1559 // Add params for effected page and ids
1560 $params = array( $param, implode( ',', $items) );
1561 $log->addEntry( 'revision', $title, $reason, $params );
1562 }
1563 }
1564 }