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