MediaWiki
REL1_19
|
00001 <?php 00030 class SpecialRevisionDelete extends UnlistedSpecialPage { 00032 var $submitClicked; 00033 00035 var $ids; 00036 00038 var $archiveName; 00039 00041 var $token; 00042 00044 var $targetObj; 00045 00047 var $typeName; 00048 00050 var $checks; 00051 00053 var $typeInfo; 00054 00056 var $list; 00057 00062 static $allowedTypes = array( 00063 'revision' => array( 00064 'check-label' => 'revdelete-hide-text', 00065 'deletion-bits' => Revision::DELETED_TEXT, 00066 'success' => 'revdelete-success', 00067 'failure' => 'revdelete-failure', 00068 'list-class' => 'RevDel_RevisionList', 00069 ), 00070 'archive' => array( 00071 'check-label' => 'revdelete-hide-text', 00072 'deletion-bits' => Revision::DELETED_TEXT, 00073 'success' => 'revdelete-success', 00074 'failure' => 'revdelete-failure', 00075 'list-class' => 'RevDel_ArchiveList', 00076 ), 00077 'oldimage'=> array( 00078 'check-label' => 'revdelete-hide-image', 00079 'deletion-bits' => File::DELETED_FILE, 00080 'success' => 'revdelete-success', 00081 'failure' => 'revdelete-failure', 00082 'list-class' => 'RevDel_FileList', 00083 ), 00084 'filearchive' => array( 00085 'check-label' => 'revdelete-hide-image', 00086 'deletion-bits' => File::DELETED_FILE, 00087 'success' => 'revdelete-success', 00088 'failure' => 'revdelete-failure', 00089 'list-class' => 'RevDel_ArchivedFileList', 00090 ), 00091 'logging' => array( 00092 'check-label' => 'revdelete-hide-name', 00093 'deletion-bits' => LogPage::DELETED_ACTION, 00094 'success' => 'logdelete-success', 00095 'failure' => 'logdelete-failure', 00096 'list-class' => 'RevDel_LogList', 00097 ), 00098 ); 00099 00101 static $deprecatedTypeMap = array( 00102 'oldid' => 'revision', 00103 'artimestamp' => 'archive', 00104 'oldimage' => 'oldimage', 00105 'fileid' => 'filearchive', 00106 'logid' => 'logging', 00107 ); 00108 00109 public function __construct() { 00110 parent::__construct( 'Revisiondelete', 'deletedhistory' ); 00111 } 00112 00113 public function execute( $par ) { 00114 $this->checkPermissions(); 00115 $this->checkReadOnly(); 00116 00117 $output = $this->getOutput(); 00118 $user = $this->getUser(); 00119 00120 $this->mIsAllowed = $user->isAllowed('deleterevision'); // for changes 00121 $this->setHeaders(); 00122 $this->outputHeader(); 00123 $request = $this->getRequest(); 00124 $this->submitClicked = $request->wasPosted() && $request->getBool( 'wpSubmit' ); 00125 # Handle our many different possible input types. 00126 $ids = $request->getVal( 'ids' ); 00127 if ( !is_null( $ids ) ) { 00128 # Allow CSV, for backwards compatibility, or a single ID for show/hide links 00129 $this->ids = explode( ',', $ids ); 00130 } else { 00131 # Array input 00132 $this->ids = array_keys( $request->getArray('ids',array()) ); 00133 } 00134 // $this->ids = array_map( 'intval', $this->ids ); 00135 $this->ids = array_unique( array_filter( $this->ids ) ); 00136 00137 if ( $request->getVal( 'action' ) == 'historysubmit' || $request->getVal( 'action' ) == 'revisiondelete' ) { 00138 // For show/hide form submission from history page 00139 // Since we are access through index.php?title=XXX&action=historysubmit 00140 // getFullTitle() will contain the target title and not our title 00141 $this->targetObj = $this->getFullTitle(); 00142 $this->typeName = 'revision'; 00143 } else { 00144 $this->typeName = $request->getVal( 'type' ); 00145 $this->targetObj = Title::newFromText( $request->getText( 'target' ) ); 00146 } 00147 00148 # For reviewing deleted files... 00149 $this->archiveName = $request->getVal( 'file' ); 00150 $this->token = $request->getVal( 'token' ); 00151 if ( $this->archiveName && $this->targetObj ) { 00152 $this->tryShowFile( $this->archiveName ); 00153 return; 00154 } 00155 00156 if ( isset( self::$deprecatedTypeMap[$this->typeName] ) ) { 00157 $this->typeName = self::$deprecatedTypeMap[$this->typeName]; 00158 } 00159 00160 # No targets? 00161 if( !isset( self::$allowedTypes[$this->typeName] ) || count( $this->ids ) == 0 ) { 00162 $output->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' ); 00163 return; 00164 } 00165 $this->typeInfo = self::$allowedTypes[$this->typeName]; 00166 00167 # If we have revisions, get the title from the first one 00168 # since they should all be from the same page. This allows 00169 # for more flexibility with page moves... 00170 if( $this->typeName == 'revision' ) { 00171 $rev = Revision::newFromId( $this->ids[0] ); 00172 $this->targetObj = $rev ? $rev->getTitle() : $this->targetObj; 00173 } 00174 00175 $this->otherReason = $request->getVal( 'wpReason' ); 00176 # We need a target page! 00177 if( is_null($this->targetObj) ) { 00178 $output->addWikiMsg( 'undelete-header' ); 00179 return; 00180 } 00181 # Give a link to the logs/hist for this page 00182 $this->showConvenienceLinks(); 00183 00184 # Initialise checkboxes 00185 $this->checks = array( 00186 array( $this->typeInfo['check-label'], 'wpHidePrimary', $this->typeInfo['deletion-bits'] ), 00187 array( 'revdelete-hide-comment', 'wpHideComment', Revision::DELETED_COMMENT ), 00188 array( 'revdelete-hide-user', 'wpHideUser', Revision::DELETED_USER ) 00189 ); 00190 if( $user->isAllowed('suppressrevision') ) { 00191 $this->checks[] = array( 'revdelete-hide-restricted', 00192 'wpHideRestricted', Revision::DELETED_RESTRICTED ); 00193 } 00194 00195 # Either submit or create our form 00196 if( $this->mIsAllowed && $this->submitClicked ) { 00197 $this->submit( $request ); 00198 } else { 00199 $this->showForm(); 00200 } 00201 00202 $qc = $this->getLogQueryCond(); 00203 # Show relevant lines from the deletion log 00204 $output->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" ); 00205 LogEventsList::showLogExtract( $output, 'delete', 00206 $this->targetObj, '', array( 'lim' => 25, 'conds' => $qc ) ); 00207 # Show relevant lines from the suppression log 00208 if( $user->isAllowed( 'suppressionlog' ) ) { 00209 $output->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'suppress' ) ) . "</h2>\n" ); 00210 LogEventsList::showLogExtract( $output, 'suppress', 00211 $this->targetObj, '', array( 'lim' => 25, 'conds' => $qc ) ); 00212 } 00213 } 00214 00218 protected function showConvenienceLinks() { 00219 # Give a link to the logs/hist for this page 00220 if( $this->targetObj ) { 00221 $links = array(); 00222 $links[] = Linker::linkKnown( 00223 SpecialPage::getTitleFor( 'Log' ), 00224 wfMsgHtml( 'viewpagelogs' ), 00225 array(), 00226 array( 'page' => $this->targetObj->getPrefixedText() ) 00227 ); 00228 if ( !$this->targetObj->isSpecialPage() ) { 00229 # Give a link to the page history 00230 $links[] = Linker::linkKnown( 00231 $this->targetObj, 00232 wfMsgHtml( 'pagehist' ), 00233 array(), 00234 array( 'action' => 'history' ) 00235 ); 00236 # Link to deleted edits 00237 if( $this->getUser()->isAllowed('undelete') ) { 00238 $undelete = SpecialPage::getTitleFor( 'Undelete' ); 00239 $links[] = Linker::linkKnown( 00240 $undelete, 00241 wfMsgHtml( 'deletedhist' ), 00242 array(), 00243 array( 'target' => $this->targetObj->getPrefixedDBkey() ) 00244 ); 00245 } 00246 } 00247 # Logs themselves don't have histories or archived revisions 00248 $this->getOutput()->addSubtitle( $this->getLanguage()->pipeList( $links ) ); 00249 } 00250 } 00251 00255 protected function getLogQueryCond() { 00256 $conds = array(); 00257 // Revision delete logs for these item 00258 $conds['log_type'] = array( 'delete', 'suppress' ); 00259 $conds['log_action'] = $this->getList()->getLogAction(); 00260 $conds['ls_field'] = RevisionDeleter::getRelationType( $this->typeName ); 00261 $conds['ls_value'] = $this->ids; 00262 return $conds; 00263 } 00264 00269 protected function tryShowFile( $archiveName ) { 00270 $repo = RepoGroup::singleton()->getLocalRepo(); 00271 $oimage = $repo->newFromArchiveName( $this->targetObj, $archiveName ); 00272 $oimage->load(); 00273 // Check if user is allowed to see this file 00274 if ( !$oimage->exists() ) { 00275 $this->getOutput()->addWikiMsg( 'revdelete-no-file' ); 00276 return; 00277 } 00278 if( !$oimage->userCan( File::DELETED_FILE, $this->getUser() ) ) { 00279 if( $oimage->isDeleted( File::DELETED_RESTRICTED ) ) { 00280 $this->getOutput()->permissionRequired( 'suppressrevision' ); 00281 } else { 00282 $this->getOutput()->permissionRequired( 'deletedtext' ); 00283 } 00284 return; 00285 } 00286 if ( !$this->getUser()->matchEditToken( $this->token, $archiveName ) ) { 00287 $this->getOutput()->addWikiMsg( 'revdelete-show-file-confirm', 00288 $this->targetObj->getText(), 00289 $this->getLanguage()->date( $oimage->getTimestamp() ), 00290 $this->getLanguage()->time( $oimage->getTimestamp() ) ); 00291 $this->getOutput()->addHTML( 00292 Xml::openElement( 'form', array( 00293 'method' => 'POST', 00294 'action' => $this->getTitle()->getLocalUrl( 00295 'target=' . urlencode( $oimage->getName() ) . 00296 '&file=' . urlencode( $archiveName ) . 00297 '&token=' . urlencode( $this->getUser()->getEditToken( $archiveName ) ) ) 00298 ) 00299 ) . 00300 Xml::submitButton( wfMsg( 'revdelete-show-file-submit' ) ) . 00301 '</form>' 00302 ); 00303 return; 00304 } 00305 $this->getOutput()->disable(); 00306 # We mustn't allow the output to be Squid cached, otherwise 00307 # if an admin previews a deleted image, and it's cached, then 00308 # a user without appropriate permissions can toddle off and 00309 # nab the image, and Squid will serve it 00310 $this->getRequest()->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' ); 00311 $this->getRequest()->response()->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' ); 00312 $this->getRequest()->response()->header( 'Pragma: no-cache' ); 00313 00314 $key = $oimage->getStorageKey(); 00315 $path = $repo->getZonePath( 'deleted' ) . '/' . $repo->getDeletedHashPath( $key ) . $key; 00316 $repo->streamFile( $path ); 00317 } 00318 00322 protected function getList() { 00323 if ( is_null( $this->list ) ) { 00324 $class = $this->typeInfo['list-class']; 00325 $this->list = new $class( $this->getContext(), $this->targetObj, $this->ids ); 00326 } 00327 return $this->list; 00328 } 00329 00334 protected function showForm() { 00335 $UserAllowed = true; 00336 00337 if ( $this->typeName == 'logging' ) { 00338 $this->getOutput()->addWikiMsg( 'logdelete-selected', $this->getLanguage()->formatNum( count($this->ids) ) ); 00339 } else { 00340 $this->getOutput()->addWikiMsg( 'revdelete-selected', 00341 $this->targetObj->getPrefixedText(), count( $this->ids ) ); 00342 } 00343 00344 $this->getOutput()->addHTML( "<ul>" ); 00345 00346 $numRevisions = 0; 00347 // Live revisions... 00348 $list = $this->getList(); 00349 for ( $list->reset(); $list->current(); $list->next() ) { 00350 $item = $list->current(); 00351 if ( !$item->canView() ) { 00352 if( !$this->submitClicked ) { 00353 $this->getOutput()->permissionRequired( 'suppressrevision' ); 00354 return; 00355 } 00356 $UserAllowed = false; 00357 } 00358 $numRevisions++; 00359 $this->getOutput()->addHTML( $item->getHTML() ); 00360 } 00361 00362 if( !$numRevisions ) { 00363 $this->getOutput()->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' ); 00364 return; 00365 } 00366 00367 $this->getOutput()->addHTML( "</ul>" ); 00368 // Explanation text 00369 $this->addUsageText(); 00370 00371 // Normal sysops can always see what they did, but can't always change it 00372 if( !$UserAllowed ) return; 00373 00374 // Show form if the user can submit 00375 if( $this->mIsAllowed ) { 00376 $out = Xml::openElement( 'form', array( 'method' => 'post', 00377 'action' => $this->getTitle()->getLocalUrl( array( 'action' => 'submit' ) ), 00378 'id' => 'mw-revdel-form-revisions' ) ) . 00379 Xml::fieldset( wfMsg( 'revdelete-legend' ) ) . 00380 $this->buildCheckBoxes() . 00381 Xml::openElement( 'table' ) . 00382 "<tr>\n" . 00383 '<td class="mw-label">' . 00384 Xml::label( wfMsg( 'revdelete-log' ), 'wpRevDeleteReasonList' ) . 00385 '</td>' . 00386 '<td class="mw-input">' . 00387 Xml::listDropDown( 'wpRevDeleteReasonList', 00388 wfMsgForContent( 'revdelete-reason-dropdown' ), 00389 wfMsgForContent( 'revdelete-reasonotherlist' ), '', 'wpReasonDropDown', 1 00390 ) . 00391 '</td>' . 00392 "</tr><tr>\n" . 00393 '<td class="mw-label">' . 00394 Xml::label( wfMsg( 'revdelete-otherreason' ), 'wpReason' ) . 00395 '</td>' . 00396 '<td class="mw-input">' . 00397 Xml::input( 'wpReason', 60, $this->otherReason, array( 'id' => 'wpReason', 'maxlength' => 100 ) ) . 00398 '</td>' . 00399 "</tr><tr>\n" . 00400 '<td></td>' . 00401 '<td class="mw-submit">' . 00402 Xml::submitButton( wfMsgExt('revdelete-submit','parsemag',$numRevisions), 00403 array( 'name' => 'wpSubmit' ) ) . 00404 '</td>' . 00405 "</tr>\n" . 00406 Xml::closeElement( 'table' ) . 00407 Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() ) . 00408 Html::hidden( 'target', $this->targetObj->getPrefixedText() ) . 00409 Html::hidden( 'type', $this->typeName ) . 00410 Html::hidden( 'ids', implode( ',', $this->ids ) ) . 00411 Xml::closeElement( 'fieldset' ) . "\n"; 00412 } else { 00413 $out = ''; 00414 } 00415 if( $this->mIsAllowed ) { 00416 $out .= Xml::closeElement( 'form' ) . "\n"; 00417 // Show link to edit the dropdown reasons 00418 if( $this->getUser()->isAllowed( 'editinterface' ) ) { 00419 $title = Title::makeTitle( NS_MEDIAWIKI, 'revdelete-reason-dropdown' ); 00420 $link = Linker::link( 00421 $title, 00422 wfMsgHtml( 'revdelete-edit-reasonlist' ), 00423 array(), 00424 array( 'action' => 'edit' ) 00425 ); 00426 $out .= Xml::tags( 'p', array( 'class' => 'mw-revdel-editreasons' ), $link ) . "\n"; 00427 } 00428 } 00429 $this->getOutput()->addHTML( $out ); 00430 } 00431 00436 protected function addUsageText() { 00437 $this->getOutput()->addWikiMsg( 'revdelete-text' ); 00438 if( $this->getUser()->isAllowed( 'suppressrevision' ) ) { 00439 $this->getOutput()->addWikiMsg( 'revdelete-suppress-text' ); 00440 } 00441 if( $this->mIsAllowed ) { 00442 $this->getOutput()->addWikiMsg( 'revdelete-confirm' ); 00443 } 00444 } 00445 00449 protected function buildCheckBoxes() { 00450 $html = '<table>'; 00451 // If there is just one item, use checkboxes 00452 $list = $this->getList(); 00453 if( $list->length() == 1 ) { 00454 $list->reset(); 00455 $bitfield = $list->current()->getBits(); // existing field 00456 if( $this->submitClicked ) { 00457 $bitfield = $this->extractBitfield( $this->extractBitParams(), $bitfield ); 00458 } 00459 foreach( $this->checks as $item ) { 00460 list( $message, $name, $field ) = $item; 00461 $innerHTML = Xml::checkLabel( wfMsg($message), $name, $name, $bitfield & $field ); 00462 if( $field == Revision::DELETED_RESTRICTED ) 00463 $innerHTML = "<b>$innerHTML</b>"; 00464 $line = Xml::tags( 'td', array( 'class' => 'mw-input' ), $innerHTML ); 00465 $html .= "<tr>$line</tr>\n"; 00466 } 00467 // Otherwise, use tri-state radios 00468 } else { 00469 $html .= '<tr>'; 00470 $html .= '<th class="mw-revdel-checkbox">'.wfMsgHtml('revdelete-radio-same').'</th>'; 00471 $html .= '<th class="mw-revdel-checkbox">'.wfMsgHtml('revdelete-radio-unset').'</th>'; 00472 $html .= '<th class="mw-revdel-checkbox">'.wfMsgHtml('revdelete-radio-set').'</th>'; 00473 $html .= "<th></th></tr>\n"; 00474 foreach( $this->checks as $item ) { 00475 list( $message, $name, $field ) = $item; 00476 // If there are several items, use third state by default... 00477 if( $this->submitClicked ) { 00478 $selected = $this->getRequest()->getInt( $name, 0 /* unchecked */ ); 00479 } else { 00480 $selected = -1; // use existing field 00481 } 00482 $line = '<td class="mw-revdel-checkbox">' . Xml::radio( $name, -1, $selected == -1 ) . '</td>'; 00483 $line .= '<td class="mw-revdel-checkbox">' . Xml::radio( $name, 0, $selected == 0 ) . '</td>'; 00484 $line .= '<td class="mw-revdel-checkbox">' . Xml::radio( $name, 1, $selected == 1 ) . '</td>'; 00485 $label = wfMsgHtml($message); 00486 if( $field == Revision::DELETED_RESTRICTED ) { 00487 $label = "<b>$label</b>"; 00488 } 00489 $line .= "<td>$label</td>"; 00490 $html .= "<tr>$line</tr>\n"; 00491 } 00492 } 00493 00494 $html .= '</table>'; 00495 return $html; 00496 } 00497 00501 protected function submit() { 00502 # Check edit token on submission 00503 $token = $this->getRequest()->getVal('wpEditToken'); 00504 if( $this->submitClicked && !$this->getUser()->matchEditToken( $token ) ) { 00505 $this->getOutput()->addWikiMsg( 'sessionfailure' ); 00506 return false; 00507 } 00508 $bitParams = $this->extractBitParams(); 00509 $listReason = $this->getRequest()->getText( 'wpRevDeleteReasonList', 'other' ); // from dropdown 00510 $comment = $listReason; 00511 if( $comment != 'other' && $this->otherReason != '' ) { 00512 // Entry from drop down menu + additional comment 00513 $comment .= wfMsgForContent( 'colon-separator' ) . $this->otherReason; 00514 } elseif( $comment == 'other' ) { 00515 $comment = $this->otherReason; 00516 } 00517 # Can the user set this field? 00518 if( $bitParams[Revision::DELETED_RESTRICTED]==1 && !$this->getUser()->isAllowed('suppressrevision') ) { 00519 $this->getOutput()->permissionRequired( 'suppressrevision' ); 00520 return false; 00521 } 00522 # If the save went through, go to success message... 00523 $status = $this->save( $bitParams, $comment, $this->targetObj ); 00524 if ( $status->isGood() ) { 00525 $this->success(); 00526 return true; 00527 # ...otherwise, bounce back to form... 00528 } else { 00529 $this->failure( $status ); 00530 } 00531 return false; 00532 } 00533 00537 protected function success() { 00538 $this->getOutput()->setPageTitle( $this->msg( 'actioncomplete' ) ); 00539 $this->getOutput()->wrapWikiMsg( "<span class=\"success\">\n$1\n</span>", $this->typeInfo['success'] ); 00540 $this->list->reloadFromMaster(); 00541 $this->showForm(); 00542 } 00543 00547 protected function failure( $status ) { 00548 $this->getOutput()->setPageTitle( $this->msg( 'actionfailed' ) ); 00549 $this->getOutput()->addWikiText( $status->getWikiText( $this->typeInfo['failure'] ) ); 00550 $this->showForm(); 00551 } 00552 00558 protected function extractBitParams() { 00559 $bitfield = array(); 00560 foreach( $this->checks as $item ) { 00561 list( /* message */ , $name, $field ) = $item; 00562 $val = $this->getRequest()->getInt( $name, 0 /* unchecked */ ); 00563 if( $val < -1 || $val > 1) { 00564 $val = -1; // -1 for existing value 00565 } 00566 $bitfield[$field] = $val; 00567 } 00568 if( !isset($bitfield[Revision::DELETED_RESTRICTED]) ) { 00569 $bitfield[Revision::DELETED_RESTRICTED] = 0; 00570 } 00571 return $bitfield; 00572 } 00573 00580 public static function extractBitfield( $bitPars, $oldfield ) { 00581 // Build the actual new rev_deleted bitfield 00582 $newBits = 0; 00583 foreach( $bitPars as $const => $val ) { 00584 if( $val == 1 ) { 00585 $newBits |= $const; // $const is the *_deleted const 00586 } elseif( $val == -1 ) { 00587 $newBits |= ($oldfield & $const); // use existing 00588 } 00589 } 00590 return $newBits; 00591 } 00592 00596 protected function save( $bitfield, $reason, $title ) { 00597 return $this->getList()->setVisibility( 00598 array( 'value' => $bitfield, 'comment' => $reason ) 00599 ); 00600 } 00601 } 00602