MediaWiki  REL1_24
SpecialRevisiondelete.php
Go to the documentation of this file.
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 }