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