getText( 'target' ); $oldid = $wgRequest->getArray( 'oldid' ); $artimestamp = $wgRequest->getArray( 'artimestamp' ); $logid = $wgAllowLogDeletion ? $wgRequest->getArray( 'logid' ) : ''; $img = $wgRequest->getArray( 'oldimage' ); $fileid = $wgRequest->getArray( 'fileid' ); # For reviewing deleted files... $file = $wgRequest->getVal( 'file' ); # If this is a revision, then we need a target page $page = Title::newFromUrl( $target ); if( is_null($page) && is_null($logid) ) { $wgOut->addWikiText( wfMsgHtml( 'undelete-header' ) ); return; } # Only one target set at a time please! $i = (bool)$file + (bool)$oldid + (bool)$logid + (bool)$artimestamp + (bool)$fileid + (bool)$img; if( $i !== 1 ) { $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' ); return; } # Either submit or create our form $form = new RevisionDeleteForm( $page, $oldid, $logid, $artimestamp, $fileid, $img, $file ); if( $wgRequest->wasPosted() ) { $form->submit( $wgRequest ); } else if( $oldid || $artimestamp ) { $form->showRevs( $wgRequest ); } else if( $fileid || $img ) { $form->showImages( $wgRequest ); } else if( $logid ) { $form->showLogItems( $wgRequest ); } # Show relevant lines from the deletion log. This will show even if said ID # does not exist...might be helpful if( !is_null($page) ) { $wgOut->addHTML( "
' . $item . '
' ); } foreach( $hidden as $item ) { $wgOut->addHtml( $item ); } $wgOut->addHtml( '' ); } /** * This lets a user set restrictions for archived images * @param WebRequest $request */ function showImages( $request ) { global $wgOut, $wgUser, $action; $UserAllowed = true; $count = ($this->deleteKey=='oldimage') ? count($this->ofiles) : count($this->afiles); $wgOut->addWikiText( wfMsgExt( 'revdelete-selected', array('parsemag'), $this->page->getPrefixedText(), $count ) ); $bitfields = 0; $wgOut->addHtml( "' . $item . '
' ); } foreach( $hidden as $item ) { $wgOut->addHtml( $item ); } $wgOut->addHtml( '' ); } /** * This lets a user set restrictions for log items * @param WebRequest $request */ function showLogItems( $request ) { global $wgOut, $wgUser, $action; $UserAllowed = true; $wgOut->addWikiText( wfMsgExt( 'logdelete-selected', array('parsemag'), count($this->events) ) ); $bitfields = 0; $wgOut->addHtml( "' . $item . '
' ); } foreach( $hidden as $item ) { $wgOut->addHtml( $item ); } $wgOut->addHtml( '' ); } /** * @param Revision $rev * @returns string */ private function historyLine( $rev ) { global $wgContLang; $date = $wgContLang->timeanddate( $rev->getTimestamp() ); $difflink=''; $del = ''; // Live revisions if( $this->deleteKey=='oldid' ) { $difflink = '(' . $this->skin->makeKnownLinkObj( $this->page, wfMsgHtml('diff'), 'diff=' . $rev->getId() . '&oldid=prev' ) . ')'; $revlink = $this->skin->makeLinkObj( $this->page, $date, 'oldid=' . $rev->getId() ); } else { // Archived revisions $undelete = SpecialPage::getTitleFor( 'Undelete' ); $target = $this->page->getPrefixedText(); $revlink = $this->skin->makeLinkObj( $undelete, $date, "target=$target×tamp=" . $rev->getTimestamp() ); } if( $rev->isDeleted(Revision::DELETED_TEXT) ) { $revlink = ''.$revlink.''; $del = ' ' . wfMsgHtml( 'deletedrev' ) . ''; if( !$rev->userCan(Revision::DELETED_TEXT) ) { $revlink = ''.$date.''; } } return "'.$histlink.' / '.$loglink.' / '.$dellink.'
' ); if( $this->deleteKey=='logid' ) { $wgOut->addWikiText( Xml::element( 'span', array( 'class' => 'success' ), wfMsg( 'logdelete-success' ) ), false ); $this->showLogItems( $request ); } else if( $this->deleteKey=='oldid' || $this->deleteKey=='artimestamp' ) { $wgOut->addWikiText( Xml::element( 'span', array( 'class' => 'success' ), wfMsg( 'revdelete-success' ) ), false ); $this->showRevs( $request ); } else if( $this->deleteKey=='fileid' ) { $wgOut->addWikiText( Xml::element( 'span', array( 'class' => 'success' ), wfMsg( 'revdelete-success' ) ), false ); $this->showImages( $request ); } else if( $this->deleteKey=='oldimage' ) { $this->showImages( $request ); } } /** * Put together a rev_deleted bitfield from the submitted checkboxes * @param WebRequest $request * @return int */ private function extractBitfield( $request ) { $bitfield = 0; foreach( $this->checks as $item ) { list( /* message */ , $name, $field ) = $item; if( $request->getCheck( $name ) ) { $bitfield |= $field; } } return $bitfield; } private function save( $bitfield, $reason, $title ) { $dbw = wfGetDB( DB_MASTER ); // Don't allow simply locking the interface for no reason if( $bitfield == Revision::DELETED_RESTRICTED ) { $bitfield = 0; } $deleter = new RevisionDeleter( $dbw ); // By this point, only one of the below should be set if( isset($this->revisions) ) { return $deleter->setRevVisibility( $title, $this->revisions, $bitfield, $reason ); } else if( isset($this->archrevs) ) { return $deleter->setArchiveVisibility( $title, $this->archrevs, $bitfield, $reason ); } else if( isset($this->ofiles) ) { return $deleter->setOldImgVisibility( $title, $this->ofiles, $bitfield, $reason ); } else if( isset($this->afiles) ) { return $deleter->setArchFileVisibility( $title, $this->afiles, $bitfield, $reason ); } else if( isset($this->events) ) { return $deleter->setEventVisibility( $this->events, $bitfield, $reason ); } } } /** * Implements the actions for Revision Deletion. * @addtogroup SpecialPage */ class RevisionDeleter { function __construct( $db ) { $this->dbw = $db; } /** * @param $title, the page these events apply to * @param array $items list of revision ID numbers * @param int $bitfield new rev_deleted value * @param string $comment Comment for log records */ function setRevVisibility( $title, $items, $bitfield, $comment ) { global $wgOut; $userAllowedAll = $success = true; $revIDs = array(); $revCount = 0; // Run through and pull all our data in one query foreach( $items as $revid ) { $where[] = intval($revid); } $whereClause = 'rev_id IN(' . implode(',',$where) . ')'; $result = $this->dbw->select( 'revision', '*', array( 'rev_page' => $title->getArticleID(), $whereClause ), __METHOD__ ); while( $row = $this->dbw->fetchObject( $result ) ) { $revObjs[$row->rev_id] = new Revision( $row ); } // To work! foreach( $items as $revid ) { if( !isset($revObjs[$revid]) || $revObjs[$revid]->isCurrent() ) { $success = false; continue; // Must exist } else if( !$revObjs[$revid]->userCan(Revision::DELETED_RESTRICTED) ) { $userAllowedAll=false; continue; } // For logging, maintain a count of revisions if( $revObjs[$revid]->mDeleted != $bitfield ) { $revCount++; $revIDs[]=$revid; $this->updateRevision( $revObjs[$revid], $bitfield ); $this->updateRecentChangesEdits( $revObjs[$revid], $bitfield, false ); } } // Clear caches... // Don't log or touch if nothing changed if( $revCount > 0 ) { $this->updateLog( $title, $revCount, $bitfield, $revObjs[$revid]->mDeleted, $comment, $title, 'oldid', $revIDs ); $this->updatePage( $title ); } // Where all revs allowed to be set? if( !$userAllowedAll ) { //FIXME: still might be confusing??? $wgOut->permissionRequired( 'hiderevision' ); return false; } return $success; } /** * @param $title, the page these events apply to * @param array $items list of revision ID numbers * @param int $bitfield new rev_deleted value * @param string $comment Comment for log records */ function setArchiveVisibility( $title, $items, $bitfield, $comment ) { global $wgOut; $userAllowedAll = $success = true; $count = 0; $Id_set = array(); // Run through and pull all our data in one query foreach( $items as $timestamp ) { $where[] = $this->dbw->addQuotes( $timestamp ); } $whereClause = 'ar_timestamp IN(' . implode(',',$where) . ')'; $result = $this->dbw->select( 'archive', '*', array( 'ar_namespace' => $title->getNamespace(), 'ar_title' => $title->getDBKey(), $whereClause ), __METHOD__ ); while( $row = $this->dbw->fetchObject( $result ) ) { $revObjs[$row->ar_timestamp] = new Revision( array( 'page' => $title->getArticleId(), 'id' => $row->ar_rev_id, 'text' => $row->ar_text_id, 'comment' => $row->ar_comment, 'user' => $row->ar_user, 'user_text' => $row->ar_user_text, 'timestamp' => $row->ar_timestamp, 'minor_edit' => $row->ar_minor_edit, 'text_id' => $row->ar_text_id, 'deleted' => $row->ar_deleted, 'len' => $row->ar_len) ); } // To work! foreach( $items as $timestamp ) { // This will only select the first revision with this timestamp. // Since they are all selected/deleted at once, we can just check the // permissions of one. UPDATE is done via timestamp, so all revs are set. if( !is_object($revObjs[$timestamp]) ) { $success = false; continue; // Must exist } else if( !$revObjs[$timestamp]->userCan(Revision::DELETED_RESTRICTED) ) { $userAllowedAll=false; continue; } // Which revisions did we change anything about? if( $revObjs[$timestamp]->mDeleted != $bitfield ) { $Id_set[]=$timestamp; $count++; $this->updateArchive( $revObjs[$timestamp], $bitfield ); } } // For logging, maintain a count of revisions if( $count > 0 ) { $this->updateLog( $title, $count, $bitfield, $revObjs[$timestamp]->mDeleted, $comment, $title, 'artimestamp', $Id_set ); } // Where all revs allowed to be set? if( !$userAllowedAll ) { $wgOut->permissionRequired( 'hiderevision' ); return false; } return $success; } /** * @param $title, the page these events apply to * @param array $items list of revision ID numbers * @param int $bitfield new rev_deleted value * @param string $comment Comment for log records */ function setOldImgVisibility( $title, $items, $bitfield, $comment ) { global $wgOut; $userAllowedAll = $success = true; $count = 0; $set = array(); // Run through and pull all our data in one query foreach( $items as $timestamp ) { $where[] = $this->dbw->addQuotes( $timestamp.'!'.$title->getDbKey() ); } $whereClause = 'oi_archive_name IN(' . implode(',',$where) . ')'; $result = $this->dbw->select( 'oldimage', '*', array( 'oi_name' => $title->getDbKey(), $whereClause ), __METHOD__ ); while( $row = $this->dbw->fetchObject( $result ) ) { $filesObjs[$row->oi_archive_name] = RepoGroup::singleton()->getLocalRepo()->newFileFromRow( $row ); $filesObjs[$row->oi_archive_name]->user = $row->oi_user; $filesObjs[$row->oi_archive_name]->user_text = $row->oi_user_text; } // To work! foreach( $items as $timestamp ) { $archivename = $timestamp.'!'.$title->getDbKey(); if( !isset($filesObjs[$archivename]) ) { $success = false; continue; // Must exist } else if( !$filesObjs[$archivename]->userCan(File::DELETED_RESTRICTED) ) { $userAllowedAll=false; continue; } $transaction = true; // Which revisions did we change anything about? if( $filesObjs[$archivename]->deleted != $bitfield ) { $count++; $this->dbw->begin(); $this->updateOldFiles( $filesObjs[$archivename], $bitfield ); // If this image is currently hidden... if( $filesObjs[$archivename]->deleted & File::DELETED_FILE ) { if( $bitfield & File::DELETED_FILE ) { # Leave it alone if we are not changing this... $set[]=$archivename; $transaction = true; } else { # We are moving this out $transaction = $this->makeOldImagePublic( $filesObjs[$archivename] ); $set[]=$transaction; } // Is it just now becoming hidden? } else if( $bitfield & File::DELETED_FILE ) { $transaction = $this->makeOldImagePrivate( $filesObjs[$archivename] ); $set[]=$transaction; } else { $set[]=$timestamp; } // If our file operations fail, then revert back the db if( $transaction==false ) { $this->dbw->rollback(); return false; } $this->dbw->commit(); } } // Log if something was changed if( $count > 0 ) { $this->updateLog( $title, $count, $bitfield, $filesObjs[$archivename]->deleted, $comment, $title, 'oldimage', $set ); # Purge page/history $file = wfLocalFile( $title ); $file->purgeCache(); $file->purgeHistory(); # Invalidate cache for all pages using this file $update = new HTMLCacheUpdate( $title, 'imagelinks' ); $update->doUpdate(); } // Where all revs allowed to be set? if( !$userAllowedAll ) { $wgOut->permissionRequired( 'hiderevision' ); return false; } return $success; } /** * @param $title, the page these events apply to * @param array $items list of revision ID numbers * @param int $bitfield new rev_deleted value * @param string $comment Comment for log records */ function setArchFileVisibility( $title, $items, $bitfield, $comment ) { global $wgOut; $userAllowedAll = $success = true; $count = 0; $Id_set = array(); // Run through and pull all our data in one query foreach( $items as $id ) { $where[] = intval($id); } $whereClause = 'fa_id IN(' . implode(',',$where) . ')'; $result = $this->dbw->select( 'filearchive', '*', array( 'fa_name' => $title->getDbKey(), $whereClause ), __METHOD__ ); while( $row = $this->dbw->fetchObject( $result ) ) { $filesObjs[$row->fa_id] = ArchivedFile::newFromRow( $row ); } // To work! foreach( $items as $fileid ) { if( !isset($filesObjs[$fileid]) ) { $success = false; continue; // Must exist } else if( !$filesObjs[$fileid]->userCan(File::DELETED_RESTRICTED) ) { $userAllowedAll=false; continue; } // Which revisions did we change anything about? if( $filesObjs[$fileid]->deleted != $bitfield ) { $Id_set[]=$fileid; $count++; $this->updateArchFiles( $filesObjs[$fileid], $bitfield ); } } // Log if something was changed if( $count > 0 ) { $this->updateLog( $title, $count, $bitfield, $comment, $filesObjs[$fileid]->deleted, $title, 'fileid', $Id_set ); } // Where all revs allowed to be set? if( !$userAllowedAll ) { $wgOut->permissionRequired( 'hiderevision' ); return false; } return $success; } /** * @param array $items list of log ID numbers * @param int $bitfield new log_deleted value * @param string $comment Comment for log records */ function setEventVisibility( $items, $bitfield, $comment ) { global $wgOut; $userAllowedAll = $success = true; $logs_count = array(); $logs_Ids = array(); // Run through and pull all our data in one query foreach( $items as $logid ) { $where[] = intval($logid); } $whereClause = 'log_id IN(' . implode(',',$where) . ')'; $result = $this->dbw->select( 'logging', '*', array( $whereClause ), __METHOD__ ); while( $row = $this->dbw->fetchObject( $result ) ) { $logRows[$row->log_id] = $row; } // To work! foreach( $items as $logid ) { if( !isset($logRows[$logid]) ) { $success = false; continue; // Must exist } else if( !LogPage::userCan($logRows[$logid], Revision::DELETED_RESTRICTED) || $logRows[$logid]->log_type=='oversight' ) { // Don't hide from oversight log!!! $userAllowedAll=false; continue; } $logtype = $logRows[$logid]->log_type; // For logging, maintain a count of events per log type if( !isset( $logs_count[$logtype] ) ) { $logs_count[$logtype]=0; $logs_Ids[$logtype]=array(); } // Which logs did we change anything about? if( $logRows[$logid]->log_deleted != $bitfield ) { $logs_Ids[$logtype][]=$logid; $logs_count[$logtype]++; $this->updateLogs( $logRows[$logid], $bitfield ); $this->updateRecentChangesLog( $logRows[$logid], $bitfield, true ); } } foreach( $logs_count as $logtype => $count ) { // Don't log or touch if nothing changed if( $count > 0 ) { $target = SpecialPage::getTitleFor( 'Log', $logtype ); $this->updateLog( $target, $count, $bitfield, $logRows[$logid]->log_deleted, $comment, $target, 'logid', $logs_Ids[$logtype] ); } } // Where all revs allowed to be set? if( !$userAllowedAll ) { $wgOut->permissionRequired( 'hiderevision' ); return false; } return $success; } /** * Moves an image to a safe private location * Caller is responsible for clearing caches * @param File $oimage * @returns mixed, timestamp string on success, false on failure */ function makeOldImagePrivate( $oimage ) { global $wgFileStore, $wgUseSquid; $transaction = new FSTransaction(); if( !FileStore::lock() ) { wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" ); return false; } $oldpath = $oimage->getArchivePath() . DIRECTORY_SEPARATOR . $oimage->archive_name; // Dupe the file into the file store if( file_exists( $oldpath ) ) { // Is our directory configured? if( $store = FileStore::get( 'deleted' ) ) { if( !$oimage->sha1 ) { $oimage->upgradeRow(); // sha1 may be missing } $key = $oimage->sha1 . '.' . $oimage->getExtension(); $transaction->add( $store->insert( $key, $oldpath, FileStore::DELETE_ORIGINAL ) ); } else { $group = null; $key = null; $transaction = false; // Return an error and do nothing } } else { wfDebug( __METHOD__." deleting already-missing '$oldpath'; moving on to database\n" ); $group = null; $key = ''; $transaction = new FSTransaction(); // empty } if( $transaction === false ) { // Fail to restore? wfDebug( __METHOD__.": import to file store failed, aborting\n" ); throw new MWException( "Could not archive and delete file $oldpath" ); return false; } wfDebug( __METHOD__.": set db items, applying file transactions\n" ); $transaction->commit(); FileStore::unlock(); $m = explode('!',$oimage->archive_name,2); $timestamp = $m[0]; return $timestamp; } /** * Moves an image from a safe private location * Caller is responsible for clearing caches * @param File $oimage * @returns mixed, string timestamp on success, false on failure */ function makeOldImagePublic( $oimage ) { global $wgFileStore; $transaction = new FSTransaction(); if( !FileStore::lock() ) { wfDebug( __METHOD__." could not acquire filestore lock\n" ); return false; } $store = FileStore::get( 'deleted' ); if( !$store ) { wfDebug( __METHOD__.": skipping row with no file.\n" ); return false; } $key = $oimage->sha1.'.'.$oimage->getExtension(); $destDir = $oimage->getArchivePath(); if( !is_dir( $destDir ) ) { wfMkdirParents( $destDir ); } $destPath = $destDir . DIRECTORY_SEPARATOR . $oimage->archive_name; // Check if any other stored revisions use this file; // if so, we shouldn't remove the file from the hidden // archives so they will still work. Check hidden files first. $useCount = $this->dbw->selectField( 'oldimage', '1', array( 'oi_sha1' => $oimage->sha1, 'oi_deleted & '.File::DELETED_FILE => File::DELETED_FILE ), __METHOD__, array( 'FOR UPDATE' ) ); // Check the rest of the deleted archives too. // (these are the ones that don't show in the image history) if( !$useCount ) { $useCount = $this->dbw->selectField( 'filearchive', '1', array( 'fa_storage_group' => 'deleted', 'fa_storage_key' => $key ), __METHOD__, array( 'FOR UPDATE' ) ); } if( $useCount == 0 ) { wfDebug( __METHOD__.": nothing else using {$oimage->sha1}, will deleting after\n" ); $flags = FileStore::DELETE_ORIGINAL; } else { $flags = 0; } $transaction->add( $store->export( $key, $destPath, $flags ) ); wfDebug( __METHOD__.": set db items, applying file transactions\n" ); $transaction->commit(); FileStore::unlock(); $m = explode('!',$oimage->archive_name,2); $timestamp = $m[0]; return $timestamp; } /** * Update the revision's rev_deleted field * @param Revision $rev * @param int $bitfield new rev_deleted bitfield value */ function updateRevision( $rev, $bitfield ) { $this->dbw->update( 'revision', array( 'rev_deleted' => $bitfield ), array( 'rev_id' => $rev->getId() ), __METHOD__ ); } /** * Update the revision's rev_deleted field * @param Revision $rev * @param int $bitfield new rev_deleted bitfield value */ function updateArchive( $rev, $bitfield ) { $this->dbw->update( 'archive', array( 'ar_deleted' => $bitfield ), array( 'ar_rev_id' => $rev->getId() ), __METHOD__ ); } /** * Update the images's oi_deleted field * @param File $oimage * @param int $bitfield new rev_deleted bitfield value */ function updateOldFiles( $oimage, $bitfield ) { $this->dbw->update( 'oldimage', array( 'oi_deleted' => $bitfield ), array( 'oi_archive_name' => $oimage->archive_name ), __METHOD__ ); } /** * Update the images's fa_deleted field * @param ArchivedFile $file * @param int $bitfield new rev_deleted bitfield value */ function updateArchFiles( $file, $bitfield ) { $this->dbw->update( 'filearchive', array( 'fa_deleted' => $bitfield ), array( 'fa_id' => $file->getID() ), __METHOD__ ); } /** * Update the logging log_deleted field * @param Row $row * @param int $bitfield new rev_deleted bitfield value */ function updateLogs( $row, $bitfield ) { $this->dbw->update( 'logging', array( 'log_deleted' => $bitfield ), array( 'log_id' => $row->log_id ), __METHOD__ ); } /** * Update the revision's recentchanges record if fields have been hidden * @param Revision $rev * @param int $bitfield new rev_deleted bitfield value */ function updateRecentChangesEdits( $rev, $bitfield ) { $this->dbw->update( 'recentchanges', array( 'rc_deleted' => $bitfield, 'rc_patrolled' => 1 ), array( 'rc_this_oldid' => $rev->getId() ), __METHOD__ ); } /** * Update the revision's recentchanges record if fields have been hidden * @param Row $row * @param int $bitfield new rev_deleted bitfield value */ function updateRecentChangesLog( $row, $bitfield ) { $this->dbw->update( 'recentchanges', array( 'rc_deleted' => $bitfield, 'rc_patrolled' => 1 ), array( 'rc_logid' => $row->log_id ), __METHOD__ ); } /** * Touch the page's cache invalidation timestamp; this forces cached * history views to refresh, so any newly hidden or shown fields will * update properly. * @param Title $title */ function updatePage( $title ) { $title->invalidateCache(); $title->purgeSquid(); // Extensions that require referencing previous revisions may need this wfRunHooks( 'ArticleRevisionVisiblitySet', array( &$title ) ); } /** * Record a log entry on the action * @param Title $title, page where item was removed from * @param int $count the number of revisions altered for this page * @param int $nbitfield the new _deleted value * @param int $obitfield the old _deleted value * @param string $comment * @param Title $target, the relevant page * @param string $param, URL param * @param Array $items */ function updateLog( $title, $count, $nbitfield, $obitfield, $comment, $target, $param, $items = array() ) { // Put things hidden from sysops in the oversight log $logtype = ( ($nbitfield | $obitfield) & Revision::DELETED_RESTRICTED ) ? 'oversight' : 'delete'; $log = new LogPage( $logtype ); // FIXME: do this better if( $param=='logid' ) { $params = array( implode( ',', $items) ); $reason = wfMsgExt('logdelete-logaction', array('parsemag'), $count, $nbitfield ); if($comment) $reason .= ": $comment"; $log->addEntry( 'event', $title, $reason, $params ); } else { // Add params for effected page and ids $params = array( $target->getPrefixedText(), $param, implode( ',', $items) ); $reason = wfMsgExt('revdelete-logaction', array('parsemag'), $count, $nbitfield ); if($comment) $reason .= ": $comment"; $log->addEntry( 'revision', $title, $reason, $params ); } } }