MediaWiki
REL1_24
|
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' 00136 || $request->getVal( 'action' ) == 'revisiondelete' 00137 ) { 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 00154 return; 00155 } 00156 00157 $this->typeName = RevisionDeleter::getCanonicalTypeName( $this->typeName ); 00158 00159 # No targets? 00160 if ( !$this->typeName || count( $this->ids ) == 0 ) { 00161 throw new ErrorPageError( 'revdelete-nooldid-title', 'revdelete-nooldid-text' ); 00162 } 00163 00164 # Allow the list type to adjust the passed target 00165 $this->targetObj = RevisionDeleter::suggestTarget( 00166 $this->typeName, 00167 $this->targetObj, 00168 $this->ids 00169 ); 00170 00171 $this->typeLabels = self::$UILabels[$this->typeName]; 00172 $list = $this->getList(); 00173 $list->reset(); 00174 $bitfield = $list->current()->getBits(); 00175 $this->mIsAllowed = $user->isAllowed( RevisionDeleter::getRestriction( $this->typeName ) ); 00176 $canViewSuppressedOnly = $this->getUser()->isAllowed( 'viewsuppressed' ) && 00177 !$this->getUser()->isAllowed( 'suppressrevision' ); 00178 $pageIsSuppressed = $bitfield & Revision::DELETED_RESTRICTED; 00179 $this->mIsAllowed = $this->mIsAllowed && !( $canViewSuppressedOnly && $pageIsSuppressed ); 00180 00181 $this->otherReason = $request->getVal( 'wpReason' ); 00182 # We need a target page! 00183 if ( is_null( $this->targetObj ) ) { 00184 $output->addWikiMsg( 'undelete-header' ); 00185 00186 return; 00187 } 00188 # Give a link to the logs/hist for this page 00189 $this->showConvenienceLinks(); 00190 00191 # Initialise checkboxes 00192 $this->checks = array( 00193 # Messages: revdelete-hide-text, revdelete-hide-image, revdelete-hide-name 00194 array( $this->typeLabels['check-label'], 'wpHidePrimary', 00195 RevisionDeleter::getRevdelConstant( $this->typeName ) 00196 ), 00197 array( 'revdelete-hide-comment', 'wpHideComment', Revision::DELETED_COMMENT ), 00198 array( 'revdelete-hide-user', 'wpHideUser', Revision::DELETED_USER ) 00199 ); 00200 if ( $user->isAllowed( 'suppressrevision' ) ) { 00201 $this->checks[] = array( 'revdelete-hide-restricted', 00202 'wpHideRestricted', Revision::DELETED_RESTRICTED ); 00203 } 00204 00205 # Either submit or create our form 00206 if ( $this->mIsAllowed && $this->submitClicked ) { 00207 $this->submit( $request ); 00208 } else { 00209 $this->showForm(); 00210 } 00211 00212 $qc = $this->getLogQueryCond(); 00213 # Show relevant lines from the deletion log 00214 $deleteLogPage = new LogPage( 'delete' ); 00215 $output->addHTML( "<h2>" . $deleteLogPage->getName()->escaped() . "</h2>\n" ); 00216 LogEventsList::showLogExtract( 00217 $output, 00218 'delete', 00219 $this->targetObj, 00220 '', /* user */ 00221 array( 'lim' => 25, 'conds' => $qc, 'useMaster' => $this->wasSaved ) 00222 ); 00223 # Show relevant lines from the suppression log 00224 if ( $user->isAllowed( 'suppressionlog' ) ) { 00225 $suppressLogPage = new LogPage( 'suppress' ); 00226 $output->addHTML( "<h2>" . $suppressLogPage->getName()->escaped() . "</h2>\n" ); 00227 LogEventsList::showLogExtract( 00228 $output, 00229 'suppress', 00230 $this->targetObj, 00231 '', 00232 array( 'lim' => 25, 'conds' => $qc, 'useMaster' => $this->wasSaved ) 00233 ); 00234 } 00235 } 00236 00240 protected function showConvenienceLinks() { 00241 # Give a link to the logs/hist for this page 00242 if ( $this->targetObj ) { 00243 // Also set header tabs to be for the target. 00244 $this->getSkin()->setRelevantTitle( $this->targetObj ); 00245 00246 $links = array(); 00247 $links[] = Linker::linkKnown( 00248 SpecialPage::getTitleFor( 'Log' ), 00249 $this->msg( 'viewpagelogs' )->escaped(), 00250 array(), 00251 array( 'page' => $this->targetObj->getPrefixedText() ) 00252 ); 00253 if ( !$this->targetObj->isSpecialPage() ) { 00254 # Give a link to the page history 00255 $links[] = Linker::linkKnown( 00256 $this->targetObj, 00257 $this->msg( 'pagehist' )->escaped(), 00258 array(), 00259 array( 'action' => 'history' ) 00260 ); 00261 # Link to deleted edits 00262 if ( $this->getUser()->isAllowed( 'undelete' ) ) { 00263 $undelete = SpecialPage::getTitleFor( 'Undelete' ); 00264 $links[] = Linker::linkKnown( 00265 $undelete, 00266 $this->msg( 'deletedhist' )->escaped(), 00267 array(), 00268 array( 'target' => $this->targetObj->getPrefixedDBkey() ) 00269 ); 00270 } 00271 } 00272 # Logs themselves don't have histories or archived revisions 00273 $this->getOutput()->addSubtitle( $this->getLanguage()->pipeList( $links ) ); 00274 } 00275 } 00276 00281 protected function getLogQueryCond() { 00282 $conds = array(); 00283 // Revision delete logs for these item 00284 $conds['log_type'] = array( 'delete', 'suppress' ); 00285 $conds['log_action'] = $this->getList()->getLogAction(); 00286 $conds['ls_field'] = RevisionDeleter::getRelationType( $this->typeName ); 00287 $conds['ls_value'] = $this->ids; 00288 00289 return $conds; 00290 } 00291 00297 protected function tryShowFile( $archiveName ) { 00298 $repo = RepoGroup::singleton()->getLocalRepo(); 00299 $oimage = $repo->newFromArchiveName( $this->targetObj, $archiveName ); 00300 $oimage->load(); 00301 // Check if user is allowed to see this file 00302 if ( !$oimage->exists() ) { 00303 $this->getOutput()->addWikiMsg( 'revdelete-no-file' ); 00304 00305 return; 00306 } 00307 $user = $this->getUser(); 00308 if ( !$oimage->userCan( File::DELETED_FILE, $user ) ) { 00309 if ( $oimage->isDeleted( File::DELETED_RESTRICTED ) ) { 00310 throw new PermissionsError( 'suppressrevision' ); 00311 } else { 00312 throw new PermissionsError( 'deletedtext' ); 00313 } 00314 } 00315 if ( !$user->matchEditToken( $this->token, $archiveName ) ) { 00316 $lang = $this->getLanguage(); 00317 $this->getOutput()->addWikiMsg( 'revdelete-show-file-confirm', 00318 $this->targetObj->getText(), 00319 $lang->userDate( $oimage->getTimestamp(), $user ), 00320 $lang->userTime( $oimage->getTimestamp(), $user ) ); 00321 $this->getOutput()->addHTML( 00322 Xml::openElement( 'form', array( 00323 'method' => 'POST', 00324 'action' => $this->getPageTitle()->getLocalURL( array( 00325 'target' => $this->targetObj->getPrefixedDBkey(), 00326 'file' => $archiveName, 00327 'token' => $user->getEditToken( $archiveName ), 00328 ) ) 00329 ) 00330 ) . 00331 Xml::submitButton( $this->msg( 'revdelete-show-file-submit' )->text() ) . 00332 '</form>' 00333 ); 00334 00335 return; 00336 } 00337 $this->getOutput()->disable(); 00338 # We mustn't allow the output to be Squid cached, otherwise 00339 # if an admin previews a deleted image, and it's cached, then 00340 # a user without appropriate permissions can toddle off and 00341 # nab the image, and Squid will serve it 00342 $this->getRequest()->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' ); 00343 $this->getRequest()->response()->header( 00344 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' 00345 ); 00346 $this->getRequest()->response()->header( 'Pragma: no-cache' ); 00347 00348 $key = $oimage->getStorageKey(); 00349 $path = $repo->getZonePath( 'deleted' ) . '/' . $repo->getDeletedHashPath( $key ) . $key; 00350 $repo->streamFile( $path ); 00351 } 00352 00357 protected function getList() { 00358 if ( is_null( $this->revDelList ) ) { 00359 $this->revDelList = RevisionDeleter::createList( 00360 $this->typeName, $this->getContext(), $this->targetObj, $this->ids 00361 ); 00362 } 00363 00364 return $this->revDelList; 00365 } 00366 00371 protected function showForm() { 00372 $userAllowed = true; 00373 00374 // Messages: revdelete-selected-text, revdelete-selected-file, logdelete-selected 00375 $this->getOutput()->wrapWikiMsg( "<strong>$1</strong>", array( $this->typeLabels['selected'], 00376 $this->getLanguage()->formatNum( count( $this->ids ) ), $this->targetObj->getPrefixedText() ) ); 00377 00378 $this->getOutput()->addHTML( "<ul>" ); 00379 00380 $numRevisions = 0; 00381 // Live revisions... 00382 $list = $this->getList(); 00383 // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed 00384 for ( $list->reset(); $list->current(); $list->next() ) { 00385 // @codingStandardsIgnoreEnd 00386 $item = $list->current(); 00387 00388 if ( !$item->canView() ) { 00389 if ( !$this->submitClicked ) { 00390 throw new PermissionsError( 'suppressrevision' ); 00391 } 00392 $userAllowed = false; 00393 } 00394 00395 $numRevisions++; 00396 $this->getOutput()->addHTML( $item->getHTML() ); 00397 } 00398 00399 if ( !$numRevisions ) { 00400 throw new ErrorPageError( 'revdelete-nooldid-title', 'revdelete-nooldid-text' ); 00401 } 00402 00403 $this->getOutput()->addHTML( "</ul>" ); 00404 // Explanation text 00405 $this->addUsageText(); 00406 00407 // Normal sysops can always see what they did, but can't always change it 00408 if ( !$userAllowed ) { 00409 return; 00410 } 00411 00412 // Show form if the user can submit 00413 if ( $this->mIsAllowed ) { 00414 $out = Xml::openElement( 'form', array( 'method' => 'post', 00415 'action' => $this->getPageTitle()->getLocalURL( array( 'action' => 'submit' ) ), 00416 'id' => 'mw-revdel-form-revisions' ) ) . 00417 Xml::fieldset( $this->msg( 'revdelete-legend' )->text() ) . 00418 $this->buildCheckBoxes() . 00419 Xml::openElement( 'table' ) . 00420 "<tr>\n" . 00421 '<td class="mw-label">' . 00422 Xml::label( $this->msg( 'revdelete-log' )->text(), 'wpRevDeleteReasonList' ) . 00423 '</td>' . 00424 '<td class="mw-input">' . 00425 Xml::listDropDown( 'wpRevDeleteReasonList', 00426 $this->msg( 'revdelete-reason-dropdown' )->inContentLanguage()->text(), 00427 $this->msg( 'revdelete-reasonotherlist' )->inContentLanguage()->text(), 00428 $this->getRequest()->getText( 'wpRevDeleteReasonList', 'other' ), 'wpReasonDropDown' 00429 ) . 00430 '</td>' . 00431 "</tr><tr>\n" . 00432 '<td class="mw-label">' . 00433 Xml::label( $this->msg( 'revdelete-otherreason' )->text(), 'wpReason' ) . 00434 '</td>' . 00435 '<td class="mw-input">' . 00436 Xml::input( 00437 'wpReason', 00438 60, 00439 $this->otherReason, 00440 array( 'id' => 'wpReason', 'maxlength' => 100 ) 00441 ) . 00442 '</td>' . 00443 "</tr><tr>\n" . 00444 '<td></td>' . 00445 '<td class="mw-submit">' . 00446 Xml::submitButton( $this->msg( 'revdelete-submit', $numRevisions )->text(), 00447 array( 'name' => 'wpSubmit' ) ) . 00448 '</td>' . 00449 "</tr>\n" . 00450 Xml::closeElement( 'table' ) . 00451 Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() ) . 00452 Html::hidden( 'target', $this->targetObj->getPrefixedText() ) . 00453 Html::hidden( 'type', $this->typeName ) . 00454 Html::hidden( 'ids', implode( ',', $this->ids ) ) . 00455 Xml::closeElement( 'fieldset' ) . "\n" . 00456 Xml::closeElement( 'form' ) . "\n"; 00457 // Show link to edit the dropdown reasons 00458 if ( $this->getUser()->isAllowed( 'editinterface' ) ) { 00459 $title = Title::makeTitle( NS_MEDIAWIKI, 'Revdelete-reason-dropdown' ); 00460 $link = Linker::link( 00461 $title, 00462 $this->msg( 'revdelete-edit-reasonlist' )->escaped(), 00463 array(), 00464 array( 'action' => 'edit' ) 00465 ); 00466 $out .= Xml::tags( 'p', array( 'class' => 'mw-revdel-editreasons' ), $link ) . "\n"; 00467 } 00468 } else { 00469 $out = ''; 00470 } 00471 $this->getOutput()->addHTML( $out ); 00472 } 00473 00478 protected function addUsageText() { 00479 // Messages: revdelete-text-text, revdelete-text-file, logdelete-text 00480 $this->getOutput()->wrapWikiMsg( 00481 "<strong>$1</strong>\n$2", $this->typeLabels['text'], 00482 'revdelete-text-others' 00483 ); 00484 00485 if ( $this->getUser()->isAllowed( 'suppressrevision' ) ) { 00486 $this->getOutput()->addWikiMsg( 'revdelete-suppress-text' ); 00487 } 00488 00489 if ( $this->mIsAllowed ) { 00490 $this->getOutput()->addWikiMsg( 'revdelete-confirm' ); 00491 } 00492 } 00493 00497 protected function buildCheckBoxes() { 00498 $html = '<table>'; 00499 // If there is just one item, use checkboxes 00500 $list = $this->getList(); 00501 if ( $list->length() == 1 ) { 00502 $list->reset(); 00503 $bitfield = $list->current()->getBits(); // existing field 00504 00505 if ( $this->submitClicked ) { 00506 $bitfield = RevisionDeleter::extractBitfield( $this->extractBitParams(), $bitfield ); 00507 } 00508 00509 foreach ( $this->checks as $item ) { 00510 // Messages: revdelete-hide-text, revdelete-hide-image, revdelete-hide-name, 00511 // revdelete-hide-comment, revdelete-hide-user, revdelete-hide-restricted 00512 list( $message, $name, $field ) = $item; 00513 $innerHTML = Xml::checkLabel( 00514 $this->msg( $message )->text(), 00515 $name, 00516 $name, 00517 $bitfield & $field 00518 ); 00519 00520 if ( $field == Revision::DELETED_RESTRICTED ) { 00521 $innerHTML = "<b>$innerHTML</b>"; 00522 } 00523 00524 $line = Xml::tags( 'td', array( 'class' => 'mw-input' ), $innerHTML ); 00525 $html .= "<tr>$line</tr>\n"; 00526 } 00527 } else { 00528 // Otherwise, use tri-state radios 00529 $html .= '<tr>'; 00530 $html .= '<th class="mw-revdel-checkbox">' 00531 . $this->msg( 'revdelete-radio-same' )->escaped() . '</th>'; 00532 $html .= '<th class="mw-revdel-checkbox">' 00533 . $this->msg( 'revdelete-radio-unset' )->escaped() . '</th>'; 00534 $html .= '<th class="mw-revdel-checkbox">' 00535 . $this->msg( 'revdelete-radio-set' )->escaped() . '</th>'; 00536 $html .= "<th></th></tr>\n"; 00537 foreach ( $this->checks as $item ) { 00538 // Messages: revdelete-hide-text, revdelete-hide-image, revdelete-hide-name, 00539 // revdelete-hide-comment, revdelete-hide-user, revdelete-hide-restricted 00540 list( $message, $name, $field ) = $item; 00541 // If there are several items, use third state by default... 00542 if ( $this->submitClicked ) { 00543 $selected = $this->getRequest()->getInt( $name, 0 /* unchecked */ ); 00544 } else { 00545 $selected = -1; // use existing field 00546 } 00547 $line = '<td class="mw-revdel-checkbox">' . Xml::radio( $name, -1, $selected == -1 ) . '</td>'; 00548 $line .= '<td class="mw-revdel-checkbox">' . Xml::radio( $name, 0, $selected == 0 ) . '</td>'; 00549 $line .= '<td class="mw-revdel-checkbox">' . Xml::radio( $name, 1, $selected == 1 ) . '</td>'; 00550 $label = $this->msg( $message )->escaped(); 00551 if ( $field == Revision::DELETED_RESTRICTED ) { 00552 $label = "<b>$label</b>"; 00553 } 00554 $line .= "<td>$label</td>"; 00555 $html .= "<tr>$line</tr>\n"; 00556 } 00557 } 00558 00559 $html .= '</table>'; 00560 00561 return $html; 00562 } 00563 00569 protected function submit() { 00570 # Check edit token on submission 00571 $token = $this->getRequest()->getVal( 'wpEditToken' ); 00572 if ( $this->submitClicked && !$this->getUser()->matchEditToken( $token ) ) { 00573 $this->getOutput()->addWikiMsg( 'sessionfailure' ); 00574 00575 return false; 00576 } 00577 $bitParams = $this->extractBitParams(); 00578 // from dropdown 00579 $listReason = $this->getRequest()->getText( 'wpRevDeleteReasonList', 'other' ); 00580 $comment = $listReason; 00581 if ( $comment === 'other' ) { 00582 $comment = $this->otherReason; 00583 } elseif ( $this->otherReason !== '' ) { 00584 // Entry from drop down menu + additional comment 00585 $comment .= $this->msg( 'colon-separator' )->inContentLanguage()->text() 00586 . $this->otherReason; 00587 } 00588 # Can the user set this field? 00589 if ( $bitParams[Revision::DELETED_RESTRICTED] == 1 00590 && !$this->getUser()->isAllowed( 'suppressrevision' ) 00591 ) { 00592 throw new PermissionsError( 'suppressrevision' ); 00593 } 00594 # If the save went through, go to success message... 00595 $status = $this->save( $bitParams, $comment, $this->targetObj ); 00596 if ( $status->isGood() ) { 00597 $this->success(); 00598 00599 return true; 00600 } else { 00601 # ...otherwise, bounce back to form... 00602 $this->failure( $status ); 00603 } 00604 00605 return false; 00606 } 00607 00611 protected function success() { 00612 // Messages: revdelete-success, logdelete-success 00613 $this->getOutput()->setPageTitle( $this->msg( 'actioncomplete' ) ); 00614 $this->getOutput()->wrapWikiMsg( 00615 "<span class=\"success\">\n$1\n</span>", 00616 $this->typeLabels['success'] 00617 ); 00618 $this->wasSaved = true; 00619 $this->revDelList->reloadFromMaster(); 00620 $this->showForm(); 00621 } 00622 00627 protected function failure( $status ) { 00628 // Messages: revdelete-failure, logdelete-failure 00629 $this->getOutput()->setPageTitle( $this->msg( 'actionfailed' ) ); 00630 $this->getOutput()->addWikiText( $status->getWikiText( $this->typeLabels['failure'] ) ); 00631 $this->showForm(); 00632 } 00633 00639 protected function extractBitParams() { 00640 $bitfield = array(); 00641 foreach ( $this->checks as $item ) { 00642 list( /* message */, $name, $field ) = $item; 00643 $val = $this->getRequest()->getInt( $name, 0 /* unchecked */ ); 00644 if ( $val < -1 || $val > 1 ) { 00645 $val = -1; // -1 for existing value 00646 } 00647 $bitfield[$field] = $val; 00648 } 00649 if ( !isset( $bitfield[Revision::DELETED_RESTRICTED] ) ) { 00650 $bitfield[Revision::DELETED_RESTRICTED] = 0; 00651 } 00652 00653 return $bitfield; 00654 } 00655 00663 protected function save( $bitfield, $reason, $title ) { 00664 return $this->getList()->setVisibility( 00665 array( 'value' => $bitfield, 'comment' => $reason ) 00666 ); 00667 } 00668 00669 protected function getGroupName() { 00670 return 'pagetools'; 00671 } 00672 }