diff options
author | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
---|---|---|
committer | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
commit | fc7369835258467bf97eb64f184b93691f9a9fd5 (patch) | |
tree | daabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/includes/revisiondelete |
first commit
Diffstat (limited to 'www/wiki/includes/revisiondelete')
15 files changed, 2474 insertions, 0 deletions
diff --git a/www/wiki/includes/revisiondelete/RevDelArchiveItem.php b/www/wiki/includes/revisiondelete/RevDelArchiveItem.php new file mode 100644 index 00000000..679acc64 --- /dev/null +++ b/www/wiki/includes/revisiondelete/RevDelArchiveItem.php @@ -0,0 +1,109 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup RevisionDelete + */ + +/** + * Item class for a archive table row + */ +class RevDelArchiveItem extends RevDelRevisionItem { + public function __construct( $list, $row ) { + RevDelItem::__construct( $list, $row ); + $this->revision = Revision::newFromArchiveRow( $row, + [ 'page' => $this->list->title->getArticleID() ] ); + } + + public function getIdField() { + return 'ar_timestamp'; + } + + public function getTimestampField() { + return 'ar_timestamp'; + } + + public function getAuthorIdField() { + return 'ar_user'; + } + + public function getAuthorNameField() { + return 'ar_user_text'; + } + + public function getAuthorActorField() { + return 'ar_actor'; + } + + public function getId() { + # Convert DB timestamp to MW timestamp + return $this->revision->getTimestamp(); + } + + public function setBits( $bits ) { + $dbw = wfGetDB( DB_MASTER ); + $dbw->update( 'archive', + [ 'ar_deleted' => $bits ], + [ + 'ar_namespace' => $this->list->title->getNamespace(), + 'ar_title' => $this->list->title->getDBkey(), + // use timestamp for index + 'ar_timestamp' => $this->row->ar_timestamp, + 'ar_rev_id' => $this->row->ar_rev_id, + 'ar_deleted' => $this->getBits() + ], + __METHOD__ ); + + return (bool)$dbw->affectedRows(); + } + + protected function getRevisionLink() { + $date = $this->list->getLanguage()->userTimeAndDate( + $this->revision->getTimestamp(), $this->list->getUser() ); + + if ( $this->isDeleted() && !$this->canViewContent() ) { + return htmlspecialchars( $date ); + } + + return $this->getLinkRenderer()->makeLink( + SpecialPage::getTitleFor( 'Undelete' ), + $date, + [], + [ + 'target' => $this->list->title->getPrefixedText(), + 'timestamp' => $this->revision->getTimestamp() + ] + ); + } + + protected function getDiffLink() { + if ( $this->isDeleted() && !$this->canViewContent() ) { + return $this->list->msg( 'diff' )->escaped(); + } + + return $this->getLinkRenderer()->makeLink( + SpecialPage::getTitleFor( 'Undelete' ), + $this->list->msg( 'diff' )->text(), + [], + [ + 'target' => $this->list->title->getPrefixedText(), + 'diff' => 'prev', + 'timestamp' => $this->revision->getTimestamp() + ] + ); + } +} diff --git a/www/wiki/includes/revisiondelete/RevDelArchiveList.php b/www/wiki/includes/revisiondelete/RevDelArchiveList.php new file mode 100644 index 00000000..4f66cdae --- /dev/null +++ b/www/wiki/includes/revisiondelete/RevDelArchiveList.php @@ -0,0 +1,86 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup RevisionDelete + */ + +use Wikimedia\Rdbms\IDatabase; + +/** + * List for archive table items, i.e. revisions deleted via action=delete + */ +class RevDelArchiveList extends RevDelRevisionList { + public function getType() { + return 'archive'; + } + + public static function getRelationType() { + return 'ar_timestamp'; + } + + /** + * @param IDatabase $db + * @return mixed + */ + public function doQuery( $db ) { + $timestamps = []; + foreach ( $this->ids as $id ) { + $timestamps[] = $db->timestamp( $id ); + } + + $arQuery = Revision::getArchiveQueryInfo(); + $tables = $arQuery['tables']; + $fields = $arQuery['fields']; + $conds = [ + 'ar_namespace' => $this->title->getNamespace(), + 'ar_title' => $this->title->getDBkey(), + 'ar_timestamp' => $timestamps, + ]; + $join_conds = $arQuery['joins']; + $options = [ 'ORDER BY' => 'ar_timestamp DESC' ]; + + ChangeTags::modifyDisplayQuery( + $tables, + $fields, + $conds, + $join_conds, + $options, + '' + ); + + return $db->select( $tables, + $fields, + $conds, + __METHOD__, + $options, + $join_conds + ); + } + + public function newItem( $row ) { + return new RevDelArchiveItem( $this, $row ); + } + + public function doPreCommitUpdates() { + return Status::newGood(); + } + + public function doPostCommitUpdates( array $visibilityChangeMap ) { + return Status::newGood(); + } +} diff --git a/www/wiki/includes/revisiondelete/RevDelArchivedFileItem.php b/www/wiki/includes/revisiondelete/RevDelArchivedFileItem.php new file mode 100644 index 00000000..d36fac92 --- /dev/null +++ b/www/wiki/includes/revisiondelete/RevDelArchivedFileItem.php @@ -0,0 +1,146 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup RevisionDelete + */ + +/** + * Item class for a filearchive table row + */ +class RevDelArchivedFileItem extends RevDelFileItem { + /** @var RevDelArchivedFileList $list */ + /** @var ArchivedFile $file */ + /** @var LocalFile */ + protected $lockFile; + + public function __construct( $list, $row ) { + RevDelItem::__construct( $list, $row ); + $this->file = ArchivedFile::newFromRow( $row ); + $this->lockFile = RepoGroup::singleton()->getLocalRepo()->newFile( $row->fa_name ); + } + + public function getIdField() { + return 'fa_id'; + } + + public function getTimestampField() { + return 'fa_timestamp'; + } + + public function getAuthorIdField() { + return 'fa_user'; + } + + public function getAuthorNameField() { + return 'fa_user_text'; + } + + public function getAuthorActorField() { + return 'fa_actor'; + } + + public function getId() { + return $this->row->fa_id; + } + + public function setBits( $bits ) { + $dbw = wfGetDB( DB_MASTER ); + $dbw->update( 'filearchive', + [ 'fa_deleted' => $bits ], + [ + 'fa_id' => $this->row->fa_id, + 'fa_deleted' => $this->getBits(), + ], + __METHOD__ + ); + + return (bool)$dbw->affectedRows(); + } + + protected function getLink() { + $date = $this->list->getLanguage()->userTimeAndDate( + $this->file->getTimestamp(), $this->list->getUser() ); + + # Hidden files... + if ( !$this->canViewContent() ) { + $link = htmlspecialchars( $date ); + } else { + $undelete = SpecialPage::getTitleFor( 'Undelete' ); + $key = $this->file->getKey(); + $link = $this->getLinkRenderer()->makeLink( $undelete, $date, [], + [ + 'target' => $this->list->title->getPrefixedText(), + 'file' => $key, + 'token' => $this->list->getUser()->getEditToken( $key ) + ] + ); + } + if ( $this->isDeleted() ) { + $link = '<span class="history-deleted">' . $link . '</span>'; + } + + return $link; + } + + public function getApiData( ApiResult $result ) { + $file = $this->file; + $user = $this->list->getUser(); + $ret = [ + 'title' => $this->list->title->getPrefixedText(), + 'timestamp' => wfTimestamp( TS_ISO_8601, $file->getTimestamp() ), + 'width' => $file->getWidth(), + 'height' => $file->getHeight(), + 'size' => $file->getSize(), + 'userhidden' => (bool)$file->isDeleted( Revision::DELETED_USER ), + 'commenthidden' => (bool)$file->isDeleted( Revision::DELETED_COMMENT ), + 'contenthidden' => (bool)$this->isDeleted(), + ]; + if ( $this->canViewContent() ) { + $ret += [ + 'url' => SpecialPage::getTitleFor( 'Revisiondelete' )->getLinkURL( + [ + 'target' => $this->list->title->getPrefixedText(), + 'file' => $file->getKey(), + 'token' => $user->getEditToken( $file->getKey() ) + ] + ), + ]; + } + if ( $file->userCan( Revision::DELETED_USER, $user ) ) { + $ret += [ + 'userid' => $file->getUser( 'id' ), + 'user' => $file->getUser( 'text' ), + ]; + } + if ( $file->userCan( Revision::DELETED_COMMENT, $user ) ) { + $ret += [ + 'comment' => $file->getRawDescription(), + ]; + } + + return $ret; + } + + public function lock() { + return $this->lockFile->acquireFileLock(); + } + + public function unlock() { + return $this->lockFile->releaseFileLock(); + } +} diff --git a/www/wiki/includes/revisiondelete/RevDelArchivedFileList.php b/www/wiki/includes/revisiondelete/RevDelArchivedFileList.php new file mode 100644 index 00000000..1ed87263 --- /dev/null +++ b/www/wiki/includes/revisiondelete/RevDelArchivedFileList.php @@ -0,0 +1,60 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup RevisionDelete + */ + +use Wikimedia\Rdbms\IDatabase; + +/** + * List for filearchive table items + */ +class RevDelArchivedFileList extends RevDelFileList { + public function getType() { + return 'filearchive'; + } + + public static function getRelationType() { + return 'fa_id'; + } + + /** + * @param IDatabase $db + * @return mixed + */ + public function doQuery( $db ) { + $ids = array_map( 'intval', $this->ids ); + + $fileQuery = ArchivedFile::getQueryInfo(); + return $db->select( + $fileQuery['tables'], + $fileQuery['fields'], + [ + 'fa_name' => $this->title->getDBkey(), + 'fa_id' => $ids + ], + __METHOD__, + [ 'ORDER BY' => 'fa_id DESC' ], + $fileQuery['joins'] + ); + } + + public function newItem( $row ) { + return new RevDelArchivedFileItem( $this, $row ); + } +} diff --git a/www/wiki/includes/revisiondelete/RevDelArchivedRevisionItem.php b/www/wiki/includes/revisiondelete/RevDelArchivedRevisionItem.php new file mode 100644 index 00000000..d839fcfc --- /dev/null +++ b/www/wiki/includes/revisiondelete/RevDelArchivedRevisionItem.php @@ -0,0 +1,53 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup RevisionDelete + */ + +/** + * Item class for a archive table row by ar_rev_id -- actually + * used via RevDelRevisionList. + */ +class RevDelArchivedRevisionItem extends RevDelArchiveItem { + public function __construct( $list, $row ) { + RevDelItem::__construct( $list, $row ); + + $this->revision = Revision::newFromArchiveRow( $row, + [ 'page' => $this->list->title->getArticleID() ] ); + } + + public function getIdField() { + return 'ar_rev_id'; + } + + public function getId() { + return $this->revision->getId(); + } + + public function setBits( $bits ) { + $dbw = wfGetDB( DB_MASTER ); + $dbw->update( 'archive', + [ 'ar_deleted' => $bits ], + [ 'ar_rev_id' => $this->row->ar_rev_id, + 'ar_deleted' => $this->getBits() + ], + __METHOD__ ); + + return (bool)$dbw->affectedRows(); + } +} diff --git a/www/wiki/includes/revisiondelete/RevDelFileItem.php b/www/wiki/includes/revisiondelete/RevDelFileItem.php new file mode 100644 index 00000000..0ca84d7f --- /dev/null +++ b/www/wiki/includes/revisiondelete/RevDelFileItem.php @@ -0,0 +1,250 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup RevisionDelete + */ + +/** + * Item class for an oldimage table row + */ +class RevDelFileItem extends RevDelItem { + /** @var RevDelFileList */ + protected $list; + /** @var OldLocalFile */ + protected $file; + + public function __construct( $list, $row ) { + parent::__construct( $list, $row ); + $this->file = RepoGroup::singleton()->getLocalRepo()->newFileFromRow( $row ); + } + + public function getIdField() { + return 'oi_archive_name'; + } + + public function getTimestampField() { + return 'oi_timestamp'; + } + + public function getAuthorIdField() { + return 'oi_user'; + } + + public function getAuthorNameField() { + return 'oi_user_text'; + } + + public function getAuthorActorField() { + return 'oi_actor'; + } + + public function getId() { + $parts = explode( '!', $this->row->oi_archive_name ); + + return $parts[0]; + } + + public function canView() { + return $this->file->userCan( File::DELETED_RESTRICTED, $this->list->getUser() ); + } + + public function canViewContent() { + return $this->file->userCan( File::DELETED_FILE, $this->list->getUser() ); + } + + public function getBits() { + return $this->file->getVisibility(); + } + + public function setBits( $bits ) { + # Queue the file op + # @todo FIXME: Move to LocalFile.php + if ( $this->isDeleted() ) { + if ( $bits & File::DELETED_FILE ) { + # Still deleted + } else { + # Newly undeleted + $key = $this->file->getStorageKey(); + $srcRel = $this->file->repo->getDeletedHashPath( $key ) . $key; + $this->list->storeBatch[] = [ + $this->file->repo->getVirtualUrl( 'deleted' ) . '/' . $srcRel, + 'public', + $this->file->getRel() + ]; + $this->list->cleanupBatch[] = $key; + } + } elseif ( $bits & File::DELETED_FILE ) { + # Newly deleted + $key = $this->file->getStorageKey(); + $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key; + $this->list->deleteBatch[] = [ $this->file->getRel(), $dstRel ]; + } + + # Do the database operations + $dbw = wfGetDB( DB_MASTER ); + $dbw->update( 'oldimage', + [ 'oi_deleted' => $bits ], + [ + 'oi_name' => $this->row->oi_name, + 'oi_timestamp' => $this->row->oi_timestamp, + 'oi_deleted' => $this->getBits() + ], + __METHOD__ + ); + + return (bool)$dbw->affectedRows(); + } + + public function isDeleted() { + return $this->file->isDeleted( File::DELETED_FILE ); + } + + /** + * Get the link to the file. + * Overridden by RevDelArchivedFileItem. + * @return string + */ + protected function getLink() { + $date = $this->list->getLanguage()->userTimeAndDate( + $this->file->getTimestamp(), $this->list->getUser() ); + + if ( !$this->isDeleted() ) { + # Regular files... + return Html::element( 'a', [ 'href' => $this->file->getUrl() ], $date ); + } + + # Hidden files... + if ( !$this->canViewContent() ) { + $link = htmlspecialchars( $date ); + } else { + $link = $this->getLinkRenderer()->makeLink( + SpecialPage::getTitleFor( 'Revisiondelete' ), + $date, + [], + [ + 'target' => $this->list->title->getPrefixedText(), + 'file' => $this->file->getArchiveName(), + 'token' => $this->list->getUser()->getEditToken( + $this->file->getArchiveName() ) + ] + ); + } + + return '<span class="history-deleted">' . $link . '</span>'; + } + + /** + * Generate a user tool link cluster if the current user is allowed to view it + * @return string HTML + */ + protected function getUserTools() { + if ( $this->file->userCan( Revision::DELETED_USER, $this->list->getUser() ) ) { + $uid = $this->file->getUser( 'id' ); + $name = $this->file->getUser( 'text' ); + $link = Linker::userLink( $uid, $name ) . Linker::userToolLinks( $uid, $name ); + } else { + $link = $this->list->msg( 'rev-deleted-user' )->escaped(); + } + if ( $this->file->isDeleted( Revision::DELETED_USER ) ) { + return '<span class="history-deleted">' . $link . '</span>'; + } + + return $link; + } + + /** + * Wrap and format the file's comment block, if the current + * user is allowed to view it. + * + * @return string HTML + */ + protected function getComment() { + if ( $this->file->userCan( File::DELETED_COMMENT, $this->list->getUser() ) ) { + $block = Linker::commentBlock( $this->file->getDescription() ); + } else { + $block = ' ' . $this->list->msg( 'rev-deleted-comment' )->escaped(); + } + if ( $this->file->isDeleted( File::DELETED_COMMENT ) ) { + return "<span class=\"history-deleted\">$block</span>"; + } + + return $block; + } + + public function getHTML() { + $data = + $this->list->msg( 'widthheight' )->numParams( + $this->file->getWidth(), $this->file->getHeight() )->text() . + ' (' . $this->list->msg( 'nbytes' )->numParams( $this->file->getSize() )->text() . ')'; + + return '<li>' . $this->getLink() . ' ' . $this->getUserTools() . ' ' . + $data . ' ' . $this->getComment() . '</li>'; + } + + public function getApiData( ApiResult $result ) { + $file = $this->file; + $user = $this->list->getUser(); + $ret = [ + 'title' => $this->list->title->getPrefixedText(), + 'archivename' => $file->getArchiveName(), + 'timestamp' => wfTimestamp( TS_ISO_8601, $file->getTimestamp() ), + 'width' => $file->getWidth(), + 'height' => $file->getHeight(), + 'size' => $file->getSize(), + 'userhidden' => (bool)$file->isDeleted( Revision::DELETED_USER ), + 'commenthidden' => (bool)$file->isDeleted( Revision::DELETED_COMMENT ), + 'contenthidden' => (bool)$this->isDeleted(), + ]; + if ( !$this->isDeleted() ) { + $ret += [ + 'url' => $file->getUrl(), + ]; + } elseif ( $this->canViewContent() ) { + $ret += [ + 'url' => SpecialPage::getTitleFor( 'Revisiondelete' )->getLinkURL( + [ + 'target' => $this->list->title->getPrefixedText(), + 'file' => $file->getArchiveName(), + 'token' => $user->getEditToken( $file->getArchiveName() ) + ] + ), + ]; + } + if ( $file->userCan( Revision::DELETED_USER, $user ) ) { + $ret += [ + 'userid' => $file->user, + 'user' => $file->user_text, + ]; + } + if ( $file->userCan( Revision::DELETED_COMMENT, $user ) ) { + $ret += [ + 'comment' => $file->description, + ]; + } + + return $ret; + } + + public function lock() { + return $this->file->acquireFileLock(); + } + + public function unlock() { + return $this->file->releaseFileLock(); + } +} diff --git a/www/wiki/includes/revisiondelete/RevDelFileList.php b/www/wiki/includes/revisiondelete/RevDelFileList.php new file mode 100644 index 00000000..6a6b86c0 --- /dev/null +++ b/www/wiki/includes/revisiondelete/RevDelFileList.php @@ -0,0 +1,134 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup RevisionDelete + */ + +use Wikimedia\Rdbms\IDatabase; + +/** + * List for oldimage table items + */ +class RevDelFileList extends RevDelList { + /** @var array */ + public $storeBatch; + + /** @var array */ + public $deleteBatch; + + /** @var array */ + public $cleanupBatch; + + public function getType() { + return 'oldimage'; + } + + public static function getRelationType() { + return 'oi_archive_name'; + } + + public static function getRestriction() { + return 'deleterevision'; + } + + public static function getRevdelConstant() { + return File::DELETED_FILE; + } + + /** + * @param IDatabase $db + * @return mixed + */ + public function doQuery( $db ) { + $archiveNames = []; + foreach ( $this->ids as $timestamp ) { + $archiveNames[] = $timestamp . '!' . $this->title->getDBkey(); + } + + $oiQuery = OldLocalFile::getQueryInfo(); + return $db->select( + $oiQuery['tables'], + $oiQuery['fields'], + [ + 'oi_name' => $this->title->getDBkey(), + 'oi_archive_name' => $archiveNames + ], + __METHOD__, + [ 'ORDER BY' => 'oi_timestamp DESC' ], + $oiQuery['joins'] + ); + } + + public function newItem( $row ) { + return new RevDelFileItem( $this, $row ); + } + + public function clearFileOps() { + $this->deleteBatch = []; + $this->storeBatch = []; + $this->cleanupBatch = []; + } + + public function doPreCommitUpdates() { + $status = Status::newGood(); + $repo = RepoGroup::singleton()->getLocalRepo(); + if ( $this->storeBatch ) { + $status->merge( $repo->storeBatch( $this->storeBatch, FileRepo::OVERWRITE_SAME ) ); + } + if ( !$status->isOK() ) { + return $status; + } + if ( $this->deleteBatch ) { + $status->merge( $repo->deleteBatch( $this->deleteBatch ) ); + } + if ( !$status->isOK() ) { + // Running cleanupDeletedBatch() after a failed storeBatch() with the DB already + // modified (but destined for rollback) causes data loss + return $status; + } + if ( $this->cleanupBatch ) { + $status->merge( $repo->cleanupDeletedBatch( $this->cleanupBatch ) ); + } + + return $status; + } + + public function doPostCommitUpdates( array $visibilityChangeMap ) { + $file = wfLocalFile( $this->title ); + $file->purgeCache(); + $file->purgeDescription(); + + // Purge full images from cache + $purgeUrls = []; + foreach ( $this->ids as $timestamp ) { + $archiveName = $timestamp . '!' . $this->title->getDBkey(); + $file->purgeOldThumbnails( $archiveName ); + $purgeUrls[] = $file->getArchiveUrl( $archiveName ); + } + DeferredUpdates::addUpdate( + new CdnCacheUpdate( $purgeUrls ), + DeferredUpdates::PRESEND + ); + + return Status::newGood(); + } + + public function getSuppressBit() { + return File::DELETED_RESTRICTED; + } +} diff --git a/www/wiki/includes/revisiondelete/RevDelItem.php b/www/wiki/includes/revisiondelete/RevDelItem.php new file mode 100644 index 00000000..47620852 --- /dev/null +++ b/www/wiki/includes/revisiondelete/RevDelItem.php @@ -0,0 +1,82 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup RevisionDelete + */ + +/** + * Abstract base class for deletable items + */ +abstract class RevDelItem extends RevisionItemBase { + /** + * Returns true if the item is "current", and the operation to set the given + * bits can't be executed for that reason + * STUB + * @param int $newBits + * @return bool + */ + public function isHideCurrentOp( $newBits ) { + return false; + } + + /** + * Get the current deletion bitfield value + * + * @return int + */ + abstract public function getBits(); + + /** + * Set the visibility of the item. This should do any necessary DB queries. + * + * The DB update query should have a condition which forces it to only update + * if the value in the DB matches the value fetched earlier with the SELECT. + * If the update fails because it did not match, the function should return + * false. This prevents concurrency problems. + * + * @param int $newBits + * @return bool Success + */ + abstract public function setBits( $newBits ); + + /** + * Get the return information about the revision for the API + * @since 1.23 + * @param ApiResult $result + * @return array Data for the API result + */ + abstract public function getApiData( ApiResult $result ); + + /** + * Lock the item against changes outside of the DB + * @return Status + * @since 1.28 + */ + public function lock() { + return Status::newGood(); + } + + /** + * Unlock the item against changes outside of the DB + * @return Status + * @since 1.28 + */ + public function unlock() { + return Status::newGood(); + } +} diff --git a/www/wiki/includes/revisiondelete/RevDelList.php b/www/wiki/includes/revisiondelete/RevDelList.php new file mode 100644 index 00000000..89025bc6 --- /dev/null +++ b/www/wiki/includes/revisiondelete/RevDelList.php @@ -0,0 +1,440 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup RevisionDelete + */ + +use MediaWiki\MediaWikiServices; + +/** + * Abstract base class for a list of deletable items. The list class + * needs to be able to make a query from a set of identifiers to pull + * relevant rows, to return RevDelItem subclasses wrapping them, and + * to wrap bulk update operations. + */ +abstract class RevDelList extends RevisionListBase { + function __construct( IContextSource $context, Title $title, array $ids ) { + parent::__construct( $context, $title ); + $this->ids = $ids; + } + + /** + * Get the DB field name associated with the ID list. + * This used to populate the log_search table for finding log entries. + * Override this function. + * @return string|null + */ + public static function getRelationType() { + return null; + } + + /** + * Get the user right required for this list type + * Override this function. + * @since 1.22 + * @return string|null + */ + public static function getRestriction() { + return null; + } + + /** + * Get the revision deletion constant for this list type + * Override this function. + * @since 1.22 + * @return int|null + */ + public static function getRevdelConstant() { + return null; + } + + /** + * Suggest a target for the revision deletion + * Optionally override this function. + * @since 1.22 + * @param Title|null $target User-supplied target + * @param array $ids + * @return Title|null + */ + public static function suggestTarget( $target, array $ids ) { + return $target; + } + + /** + * Indicate whether any item in this list is suppressed + * @since 1.25 + * @return bool + */ + public function areAnySuppressed() { + $bit = $this->getSuppressBit(); + + /** @var RevDelItem $item */ + foreach ( $this as $item ) { + if ( $item->getBits() & $bit ) { + return true; + } + } + + return false; + } + + /** + * Set the visibility for the revisions in this list. Logging and + * transactions are done here. + * + * @param array $params Associative array of parameters. Members are: + * value: ExtractBitParams() bitfield array + * comment: The log comment + * perItemStatus: Set if you want per-item status reports + * tags: The array of change tags to apply to the log entry + * @return Status + * @since 1.23 Added 'perItemStatus' param + */ + public function setVisibility( array $params ) { + global $wgActorTableSchemaMigrationStage; + + $status = Status::newGood(); + + $bitPars = $params['value']; + $comment = $params['comment']; + $perItemStatus = isset( $params['perItemStatus'] ) ? $params['perItemStatus'] : false; + + // CAS-style checks are done on the _deleted fields so the select + // does not need to use FOR UPDATE nor be in the atomic section + $dbw = wfGetDB( DB_MASTER ); + $this->res = $this->doQuery( $dbw ); + + $status->merge( $this->acquireItemLocks() ); + if ( !$status->isGood() ) { + return $status; + } + + $dbw->startAtomic( __METHOD__ ); + $dbw->onTransactionResolution( + function () { + // Release locks on commit or error + $this->releaseItemLocks(); + }, + __METHOD__ + ); + + $missing = array_flip( $this->ids ); + $this->clearFileOps(); + $idsForLog = []; + $authorIds = $authorIPs = $authorActors = []; + + if ( $perItemStatus ) { + $status->itemStatuses = []; + } + + // For multi-item deletions, set the old/new bitfields in log_params such that "hid X" + // shows in logs if field X was hidden from ANY item and likewise for "unhid Y". Note the + // form does not let the same field get hidden and unhidden in different items at once. + $virtualOldBits = 0; + $virtualNewBits = 0; + $logType = 'delete'; + + // Will be filled with id => [old, new bits] information and + // passed to doPostCommitUpdates(). + $visibilityChangeMap = []; + + /** @var RevDelItem $item */ + foreach ( $this as $item ) { + unset( $missing[$item->getId()] ); + + if ( $perItemStatus ) { + $itemStatus = Status::newGood(); + $status->itemStatuses[$item->getId()] = $itemStatus; + } else { + $itemStatus = $status; + } + + $oldBits = $item->getBits(); + // Build the actual new rev_deleted bitfield + $newBits = RevisionDeleter::extractBitfield( $bitPars, $oldBits ); + + if ( $oldBits == $newBits ) { + $itemStatus->warning( + 'revdelete-no-change', $item->formatDate(), $item->formatTime() ); + $status->failCount++; + continue; + } elseif ( $oldBits == 0 && $newBits != 0 ) { + $opType = 'hide'; + } elseif ( $oldBits != 0 && $newBits == 0 ) { + $opType = 'show'; + } else { + $opType = 'modify'; + } + + if ( $item->isHideCurrentOp( $newBits ) ) { + // Cannot hide current version text + $itemStatus->error( + 'revdelete-hide-current', $item->formatDate(), $item->formatTime() ); + $status->failCount++; + continue; + } elseif ( !$item->canView() ) { + // Cannot access this revision + $msg = ( $opType == 'show' ) ? + 'revdelete-show-no-access' : 'revdelete-modify-no-access'; + $itemStatus->error( $msg, $item->formatDate(), $item->formatTime() ); + $status->failCount++; + continue; + // Cannot just "hide from Sysops" without hiding any fields + } elseif ( $newBits == Revision::DELETED_RESTRICTED ) { + $itemStatus->warning( + 'revdelete-only-restricted', $item->formatDate(), $item->formatTime() ); + $status->failCount++; + continue; + } + + // Update the revision + $ok = $item->setBits( $newBits ); + + if ( $ok ) { + $idsForLog[] = $item->getId(); + // If any item field was suppressed or unsupressed + if ( ( $oldBits | $newBits ) & $this->getSuppressBit() ) { + $logType = 'suppress'; + } + // Track which fields where (un)hidden for each item + $addedBits = ( $oldBits ^ $newBits ) & $newBits; + $removedBits = ( $oldBits ^ $newBits ) & $oldBits; + $virtualNewBits |= $addedBits; + $virtualOldBits |= $removedBits; + + $status->successCount++; + if ( $wgActorTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ) { + if ( $item->getAuthorId() > 0 ) { + $authorIds[] = $item->getAuthorId(); + } elseif ( IP::isIPAddress( $item->getAuthorName() ) ) { + $authorIPs[] = $item->getAuthorName(); + } + } + if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) { + $authorActors[] = $item->getAuthorActor(); + } + + // Save the old and new bits in $visibilityChangeMap for + // later use. + $visibilityChangeMap[$item->getId()] = [ + 'oldBits' => $oldBits, + 'newBits' => $newBits, + ]; + } else { + $itemStatus->error( + 'revdelete-concurrent-change', $item->formatDate(), $item->formatTime() ); + $status->failCount++; + } + } + + // Handle missing revisions + foreach ( $missing as $id => $unused ) { + if ( $perItemStatus ) { + $status->itemStatuses[$id] = Status::newFatal( 'revdelete-modify-missing', $id ); + } else { + $status->error( 'revdelete-modify-missing', $id ); + } + $status->failCount++; + } + + if ( $status->successCount == 0 ) { + $dbw->endAtomic( __METHOD__ ); + return $status; + } + + // Save success count + $successCount = $status->successCount; + + // Move files, if there are any + $status->merge( $this->doPreCommitUpdates() ); + if ( !$status->isOK() ) { + // Fatal error, such as no configured archive directory or I/O failures + $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + $lbFactory->rollbackMasterChanges( __METHOD__ ); + return $status; + } + + // Log it + $authorFields = []; + if ( $wgActorTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ) { + $authorFields['authorIds'] = $authorIds; + $authorFields['authorIPs'] = $authorIPs; + } + if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) { + $authorFields['authorActors'] = $authorActors; + } + $this->updateLog( + $logType, + [ + 'title' => $this->title, + 'count' => $successCount, + 'newBits' => $virtualNewBits, + 'oldBits' => $virtualOldBits, + 'comment' => $comment, + 'ids' => $idsForLog, + 'tags' => isset( $params['tags'] ) ? $params['tags'] : [], + ] + $authorFields + ); + + // Clear caches after commit + DeferredUpdates::addCallableUpdate( + function () use ( $visibilityChangeMap ) { + $this->doPostCommitUpdates( $visibilityChangeMap ); + }, + DeferredUpdates::PRESEND, + $dbw + ); + + $dbw->endAtomic( __METHOD__ ); + + return $status; + } + + final protected function acquireItemLocks() { + $status = Status::newGood(); + /** @var RevDelItem $item */ + foreach ( $this as $item ) { + $status->merge( $item->lock() ); + } + + return $status; + } + + final protected function releaseItemLocks() { + $status = Status::newGood(); + /** @var RevDelItem $item */ + foreach ( $this as $item ) { + $status->merge( $item->unlock() ); + } + + return $status; + } + + /** + * Reload the list data from the master DB. This can be done after setVisibility() + * to allow $item->getHTML() to show the new data. + */ + function reloadFromMaster() { + $dbw = wfGetDB( DB_MASTER ); + $this->res = $this->doQuery( $dbw ); + } + + /** + * Record a log entry on the action + * @param string $logType One of (delete,suppress) + * @param array $params Associative array of parameters: + * newBits: The new value of the *_deleted bitfield + * oldBits: The old value of the *_deleted bitfield. + * title: The target title + * ids: The ID list + * comment: The log comment + * authorIds: The array of the user IDs of the offenders + * authorIPs: The array of the IP/anon user offenders + * authorActors: The array of the actor IDs of the offenders + * tags: The array of change tags to apply to the log entry + * @throws MWException + */ + private function updateLog( $logType, $params ) { + // Get the URL param's corresponding DB field + $field = RevisionDeleter::getRelationType( $this->getType() ); + if ( !$field ) { + throw new MWException( "Bad log URL param type!" ); + } + // Add params for affected page and ids + $logParams = $this->getLogParams( $params ); + // Actually add the deletion log entry + $logEntry = new ManualLogEntry( $logType, $this->getLogAction() ); + $logEntry->setTarget( $params['title'] ); + $logEntry->setComment( $params['comment'] ); + $logEntry->setParameters( $logParams ); + $logEntry->setPerformer( $this->getUser() ); + // Allow for easy searching of deletion log items for revision/log items + $relations = [ + $field => $params['ids'], + ]; + if ( isset( $params['authorIds'] ) ) { + $relations += [ + 'target_author_id' => $params['authorIds'], + 'target_author_ip' => $params['authorIPs'], + ]; + } + if ( isset( $params['authorActors'] ) ) { + $relations += [ + 'target_author_actor' => $params['authorActors'], + ]; + } + $logEntry->setRelations( $relations ); + // Apply change tags to the log entry + $logEntry->setTags( $params['tags'] ); + $logId = $logEntry->insert(); + $logEntry->publish( $logId ); + } + + /** + * Get the log action for this list type + * @return string + */ + public function getLogAction() { + return 'revision'; + } + + /** + * Get log parameter array. + * @param array $params Associative array of log parameters, same as updateLog() + * @return array + */ + public function getLogParams( $params ) { + return [ + '4::type' => $this->getType(), + '5::ids' => $params['ids'], + '6::ofield' => $params['oldBits'], + '7::nfield' => $params['newBits'], + ]; + } + + /** + * Clear any data structures needed for doPreCommitUpdates() and doPostCommitUpdates() + * STUB + */ + public function clearFileOps() { + } + + /** + * A hook for setVisibility(): do batch updates pre-commit. + * STUB + * @return Status + */ + public function doPreCommitUpdates() { + return Status::newGood(); + } + + /** + * A hook for setVisibility(): do any necessary updates post-commit. + * STUB + * @param array $visibilityChangeMap [id => ['oldBits' => $oldBits, 'newBits' => $newBits], ... ] + * @return Status + */ + public function doPostCommitUpdates( array $visibilityChangeMap ) { + return Status::newGood(); + } + + /** + * Get the integer value of the flag used for suppression + */ + abstract public function getSuppressBit(); +} diff --git a/www/wiki/includes/revisiondelete/RevDelLogItem.php b/www/wiki/includes/revisiondelete/RevDelLogItem.php new file mode 100644 index 00000000..36198cd2 --- /dev/null +++ b/www/wiki/includes/revisiondelete/RevDelLogItem.php @@ -0,0 +1,150 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup RevisionDelete + */ + +/** + * Item class for a logging table row + */ +class RevDelLogItem extends RevDelItem { + public function getIdField() { + return 'log_id'; + } + + public function getTimestampField() { + return 'log_timestamp'; + } + + public function getAuthorIdField() { + return 'log_user'; + } + + public function getAuthorNameField() { + return 'log_user_text'; + } + + public function getAuthorActorField() { + return 'log_actor'; + } + + public function canView() { + return LogEventsList::userCan( $this->row, Revision::DELETED_RESTRICTED, $this->list->getUser() ); + } + + public function canViewContent() { + return true; // none + } + + public function getBits() { + return (int)$this->row->log_deleted; + } + + public function setBits( $bits ) { + $dbw = wfGetDB( DB_MASTER ); + + $dbw->update( 'logging', + [ 'log_deleted' => $bits ], + [ + 'log_id' => $this->row->log_id, + 'log_deleted' => $this->getBits() // cas + ], + __METHOD__ + ); + + if ( !$dbw->affectedRows() ) { + // Concurrent fail! + return false; + } + + $dbw->update( 'recentchanges', + [ + 'rc_deleted' => $bits, + 'rc_patrolled' => RecentChange::PRC_PATROLLED + ], + [ + 'rc_logid' => $this->row->log_id, + 'rc_timestamp' => $this->row->log_timestamp // index + ], + __METHOD__ + ); + + return true; + } + + public function getHTML() { + $date = htmlspecialchars( $this->list->getLanguage()->userTimeAndDate( + $this->row->log_timestamp, $this->list->getUser() ) ); + $title = Title::makeTitle( $this->row->log_namespace, $this->row->log_title ); + $formatter = LogFormatter::newFromRow( $this->row ); + $formatter->setContext( $this->list->getContext() ); + $formatter->setAudience( LogFormatter::FOR_THIS_USER ); + + // Log link for this page + $loglink = $this->getLinkRenderer()->makeLink( + SpecialPage::getTitleFor( 'Log' ), + $this->list->msg( 'log' )->text(), + [], + [ 'page' => $title->getPrefixedText() ] + ); + $loglink = $this->list->msg( 'parentheses' )->rawParams( $loglink )->escaped(); + // User links and action text + $action = $formatter->getActionText(); + // Comment + $comment = CommentStore::getStore()->getComment( 'log_comment', $this->row )->text; + $comment = $this->list->getLanguage()->getDirMark() + . Linker::commentBlock( $comment ); + + if ( LogEventsList::isDeleted( $this->row, LogPage::DELETED_COMMENT ) ) { + $comment = '<span class="history-deleted">' . $comment . '</span>'; + } + + return "<li>$loglink $date $action $comment</li>"; + } + + public function getApiData( ApiResult $result ) { + $logEntry = DatabaseLogEntry::newFromRow( $this->row ); + $user = $this->list->getUser(); + $ret = [ + 'id' => $logEntry->getId(), + 'type' => $logEntry->getType(), + 'action' => $logEntry->getSubtype(), + 'userhidden' => (bool)$logEntry->isDeleted( LogPage::DELETED_USER ), + 'commenthidden' => (bool)$logEntry->isDeleted( LogPage::DELETED_COMMENT ), + 'actionhidden' => (bool)$logEntry->isDeleted( LogPage::DELETED_ACTION ), + ]; + + if ( LogEventsList::userCan( $this->row, LogPage::DELETED_ACTION, $user ) ) { + $ret['params'] = LogFormatter::newFromEntry( $logEntry )->formatParametersForApi(); + } + if ( LogEventsList::userCan( $this->row, LogPage::DELETED_USER, $user ) ) { + $ret += [ + 'userid' => $this->row->log_user, + 'user' => $this->row->log_user_text, + ]; + } + if ( LogEventsList::userCan( $this->row, LogPage::DELETED_COMMENT, $user ) ) { + $ret += [ + 'comment' => CommentStore::getStore()->getComment( 'log_comment', $this->row ) + ->text, + ]; + } + + return $ret; + } +} diff --git a/www/wiki/includes/revisiondelete/RevDelLogList.php b/www/wiki/includes/revisiondelete/RevDelLogList.php new file mode 100644 index 00000000..b26fffd1 --- /dev/null +++ b/www/wiki/includes/revisiondelete/RevDelLogList.php @@ -0,0 +1,108 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup RevisionDelete + */ + +use Wikimedia\Rdbms\IDatabase; + +/** + * List for logging table items + */ +class RevDelLogList extends RevDelList { + public function getType() { + return 'logging'; + } + + public static function getRelationType() { + return 'log_id'; + } + + public static function getRestriction() { + return 'deletelogentry'; + } + + public static function getRevdelConstant() { + return LogPage::DELETED_ACTION; + } + + public static function suggestTarget( $target, array $ids ) { + $result = wfGetDB( DB_REPLICA )->select( 'logging', + 'log_type', + [ 'log_id' => $ids ], + __METHOD__, + [ 'DISTINCT' ] + ); + if ( $result->numRows() == 1 ) { + // If there's only one type, the target can be set to include it. + return SpecialPage::getTitleFor( 'Log', $result->current()->log_type ); + } + + return SpecialPage::getTitleFor( 'Log' ); + } + + /** + * @param IDatabase $db + * @return mixed + */ + public function doQuery( $db ) { + $ids = array_map( 'intval', $this->ids ); + + $commentQuery = CommentStore::getStore()->getJoin( 'log_comment' ); + $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' ); + + return $db->select( + [ 'logging' ] + $commentQuery['tables'] + $actorQuery['tables'], + [ + 'log_id', + 'log_type', + 'log_action', + 'log_timestamp', + 'log_namespace', + 'log_title', + 'log_page', + 'log_params', + 'log_deleted' + ] + $commentQuery['fields'] + $actorQuery['fields'], + [ 'log_id' => $ids ], + __METHOD__, + [ 'ORDER BY' => 'log_id DESC' ], + $commentQuery['joins'] + $actorQuery['joins'] + ); + } + + public function newItem( $row ) { + return new RevDelLogItem( $this, $row ); + } + + public function getSuppressBit() { + return Revision::DELETED_RESTRICTED; + } + + public function getLogAction() { + return 'event'; + } + + public function getLogParams( $params ) { + return [ + '4::ids' => $params['ids'], + '5::ofield' => $params['oldBits'], + '6::nfield' => $params['newBits'], + ]; + } +} diff --git a/www/wiki/includes/revisiondelete/RevDelRevisionItem.php b/www/wiki/includes/revisiondelete/RevDelRevisionItem.php new file mode 100644 index 00000000..7b5d130b --- /dev/null +++ b/www/wiki/includes/revisiondelete/RevDelRevisionItem.php @@ -0,0 +1,213 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup RevisionDelete + */ + +/** + * Item class for a live revision table row + */ +class RevDelRevisionItem extends RevDelItem { + /** @var Revision */ + public $revision; + + public function __construct( $list, $row ) { + parent::__construct( $list, $row ); + $this->revision = new Revision( $row ); + } + + public function getIdField() { + return 'rev_id'; + } + + public function getTimestampField() { + return 'rev_timestamp'; + } + + public function getAuthorIdField() { + return 'rev_user'; + } + + public function getAuthorNameField() { + return 'rev_user_text'; + } + + public function getAuthorActorField() { + return 'rev_actor'; + } + + public function canView() { + return $this->revision->userCan( Revision::DELETED_RESTRICTED, $this->list->getUser() ); + } + + public function canViewContent() { + return $this->revision->userCan( Revision::DELETED_TEXT, $this->list->getUser() ); + } + + public function getBits() { + return $this->revision->getVisibility(); + } + + public function setBits( $bits ) { + $dbw = wfGetDB( DB_MASTER ); + // Update revision table + $dbw->update( 'revision', + [ 'rev_deleted' => $bits ], + [ + 'rev_id' => $this->revision->getId(), + 'rev_page' => $this->revision->getPage(), + 'rev_deleted' => $this->getBits() // cas + ], + __METHOD__ + ); + if ( !$dbw->affectedRows() ) { + // Concurrent fail! + return false; + } + // Update recentchanges table + $dbw->update( 'recentchanges', + [ + 'rc_deleted' => $bits, + 'rc_patrolled' => RecentChange::PRC_PATROLLED + ], + [ + 'rc_this_oldid' => $this->revision->getId(), // condition + // non-unique timestamp index + 'rc_timestamp' => $dbw->timestamp( $this->revision->getTimestamp() ), + ], + __METHOD__ + ); + + return true; + } + + public function isDeleted() { + return $this->revision->isDeleted( Revision::DELETED_TEXT ); + } + + public function isHideCurrentOp( $newBits ) { + return ( $newBits & Revision::DELETED_TEXT ) + && $this->list->getCurrent() == $this->getId(); + } + + /** + * Get the HTML link to the revision text. + * Overridden by RevDelArchiveItem. + * @return string + */ + protected function getRevisionLink() { + $date = $this->list->getLanguage()->userTimeAndDate( + $this->revision->getTimestamp(), $this->list->getUser() ); + + if ( $this->isDeleted() && !$this->canViewContent() ) { + return htmlspecialchars( $date ); + } + + return $this->getLinkRenderer()->makeKnownLink( + $this->list->title, + $date, + [], + [ + 'oldid' => $this->revision->getId(), + 'unhide' => 1 + ] + ); + } + + /** + * Get the HTML link to the diff. + * Overridden by RevDelArchiveItem + * @return string + */ + protected function getDiffLink() { + if ( $this->isDeleted() && !$this->canViewContent() ) { + return $this->list->msg( 'diff' )->escaped(); + } else { + return $this->getLinkRenderer()->makeKnownLink( + $this->list->title, + $this->list->msg( 'diff' )->text(), + [], + [ + 'diff' => $this->revision->getId(), + 'oldid' => 'prev', + 'unhide' => 1 + ] + ); + } + } + + /** + * @return string A HTML <li> element representing this revision, showing + * change tags and everything + */ + public function getHTML() { + $difflink = $this->list->msg( 'parentheses' ) + ->rawParams( $this->getDiffLink() )->escaped(); + $revlink = $this->getRevisionLink(); + $userlink = Linker::revUserLink( $this->revision ); + $comment = Linker::revComment( $this->revision ); + if ( $this->isDeleted() ) { + $revlink = "<span class=\"history-deleted\">$revlink</span>"; + } + $content = "$difflink $revlink $userlink $comment"; + $attribs = []; + $tags = $this->getTags(); + if ( $tags ) { + list( $tagSummary, $classes ) = ChangeTags::formatSummaryRow( + $tags, + 'revisiondelete', + $this->list->getContext() + ); + $content .= " $tagSummary"; + $attribs['class'] = implode( ' ', $classes ); + } + return Xml::tags( 'li', $attribs, $content ); + } + + /** + * @return string Comma-separated list of tags + */ + public function getTags() { + return $this->row->ts_tags; + } + + public function getApiData( ApiResult $result ) { + $rev = $this->revision; + $user = $this->list->getUser(); + $ret = [ + 'id' => $rev->getId(), + 'timestamp' => wfTimestamp( TS_ISO_8601, $rev->getTimestamp() ), + 'userhidden' => (bool)$rev->isDeleted( Revision::DELETED_USER ), + 'commenthidden' => (bool)$rev->isDeleted( Revision::DELETED_COMMENT ), + 'texthidden' => (bool)$rev->isDeleted( Revision::DELETED_TEXT ), + ]; + if ( $rev->userCan( Revision::DELETED_USER, $user ) ) { + $ret += [ + 'userid' => $rev->getUser( Revision::FOR_THIS_USER ), + 'user' => $rev->getUserText( Revision::FOR_THIS_USER ), + ]; + } + if ( $rev->userCan( Revision::DELETED_COMMENT, $user ) ) { + $ret += [ + 'comment' => $rev->getComment( Revision::FOR_THIS_USER ), + ]; + } + + return $ret; + } +} diff --git a/www/wiki/includes/revisiondelete/RevDelRevisionList.php b/www/wiki/includes/revisiondelete/RevDelRevisionList.php new file mode 100644 index 00000000..07362c42 --- /dev/null +++ b/www/wiki/includes/revisiondelete/RevDelRevisionList.php @@ -0,0 +1,184 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup RevisionDelete + */ + +use Wikimedia\Rdbms\FakeResultWrapper; +use Wikimedia\Rdbms\IDatabase; + +/** + * List for revision table items + * + * This will check both the 'revision' table for live revisions and the + * 'archive' table for traditionally-deleted revisions that have an + * ar_rev_id saved. + * + * See RevDelRevisionItem and RevDelArchivedRevisionItem for items. + */ +class RevDelRevisionList extends RevDelList { + /** @var int */ + public $currentRevId; + + public function getType() { + return 'revision'; + } + + public static function getRelationType() { + return 'rev_id'; + } + + public static function getRestriction() { + return 'deleterevision'; + } + + public static function getRevdelConstant() { + return Revision::DELETED_TEXT; + } + + public static function suggestTarget( $target, array $ids ) { + $rev = Revision::newFromId( $ids[0] ); + return $rev ? $rev->getTitle() : $target; + } + + /** + * @param IDatabase $db + * @return mixed + */ + public function doQuery( $db ) { + $ids = array_map( 'intval', $this->ids ); + $revQuery = Revision::getQueryInfo( [ 'page', 'user' ] ); + $queryInfo = [ + 'tables' => $revQuery['tables'], + 'fields' => $revQuery['fields'], + 'conds' => [ + 'rev_page' => $this->title->getArticleID(), + 'rev_id' => $ids, + ], + 'options' => [ + 'ORDER BY' => 'rev_id DESC', + 'USE INDEX' => [ 'revision' => 'PRIMARY' ] // workaround for MySQL bug (T104313) + ], + 'join_conds' => $revQuery['joins'], + ]; + ChangeTags::modifyDisplayQuery( + $queryInfo['tables'], + $queryInfo['fields'], + $queryInfo['conds'], + $queryInfo['join_conds'], + $queryInfo['options'], + '' + ); + + $live = $db->select( + $queryInfo['tables'], + $queryInfo['fields'], + $queryInfo['conds'], + __METHOD__, + $queryInfo['options'], + $queryInfo['join_conds'] + ); + if ( $live->numRows() >= count( $ids ) ) { + // All requested revisions are live, keeps things simple! + return $live; + } + + $arQuery = Revision::getArchiveQueryInfo(); + $archiveQueryInfo = [ + 'tables' => $arQuery['tables'], + 'fields' => $arQuery['fields'], + 'conds' => [ + 'ar_rev_id' => $ids, + ], + 'options' => [ 'ORDER BY' => 'ar_rev_id DESC' ], + 'join_conds' => $arQuery['joins'], + ]; + + ChangeTags::modifyDisplayQuery( + $archiveQueryInfo['tables'], + $archiveQueryInfo['fields'], + $archiveQueryInfo['conds'], + $archiveQueryInfo['join_conds'], + $archiveQueryInfo['options'], + '' + ); + + // Check if any requested revisions are available fully deleted. + $archived = $db->select( + $archiveQueryInfo['tables'], + $archiveQueryInfo['fields'], + $archiveQueryInfo['conds'], + __METHOD__, + $archiveQueryInfo['options'], + $archiveQueryInfo['join_conds'] + ); + + if ( $archived->numRows() == 0 ) { + return $live; + } elseif ( $live->numRows() == 0 ) { + return $archived; + } else { + // Combine the two! Whee + $rows = []; + foreach ( $live as $row ) { + $rows[$row->rev_id] = $row; + } + foreach ( $archived as $row ) { + $rows[$row->ar_rev_id] = $row; + } + krsort( $rows ); + return new FakeResultWrapper( array_values( $rows ) ); + } + } + + public function newItem( $row ) { + if ( isset( $row->rev_id ) ) { + return new RevDelRevisionItem( $this, $row ); + } elseif ( isset( $row->ar_rev_id ) ) { + return new RevDelArchivedRevisionItem( $this, $row ); + } else { + // This shouldn't happen. :) + throw new MWException( 'Invalid row type in RevDelRevisionList' ); + } + } + + public function getCurrent() { + if ( is_null( $this->currentRevId ) ) { + $dbw = wfGetDB( DB_MASTER ); + $this->currentRevId = $dbw->selectField( + 'page', 'page_latest', $this->title->pageCond(), __METHOD__ ); + } + return $this->currentRevId; + } + + public function getSuppressBit() { + return Revision::DELETED_RESTRICTED; + } + + public function doPreCommitUpdates() { + $this->title->invalidateCache(); + return Status::newGood(); + } + + public function doPostCommitUpdates( array $visibilityChangeMap ) { + $this->title->purgeSquid(); + // Extensions that require referencing previous revisions may need this + Hooks::run( 'ArticleRevisionVisibilitySet', [ $this->title, $this->ids, $visibilityChangeMap ] ); + return Status::newGood(); + } +} diff --git a/www/wiki/includes/revisiondelete/RevisionDeleteUser.php b/www/wiki/includes/revisiondelete/RevisionDeleteUser.php new file mode 100644 index 00000000..6291e8d9 --- /dev/null +++ b/www/wiki/includes/revisiondelete/RevisionDeleteUser.php @@ -0,0 +1,208 @@ +<?php +/** + * Backend functions for suppressing and unsuppressing all references to a given user. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup RevisionDelete + */ + +use Wikimedia\Rdbms\IDatabase; + +/** + * Backend functions for suppressing and unsuppressing all references to a given user, + * used when blocking with HideUser enabled. This was spun out of SpecialBlockip.php + * in 1.18; at some point it needs to be rewritten to either use RevisionDelete abstraction, + * or at least schema abstraction. + * + * @ingroup RevisionDelete + */ +class RevisionDeleteUser { + + /** + * Update *_deleted bitfields in various tables to hide or unhide usernames + * @param string $name Username + * @param int $userId User id + * @param string $op Operator '|' or '&' + * @param null|IDatabase $dbw If you happen to have one lying around + * @return bool + */ + private static function setUsernameBitfields( $name, $userId, $op, $dbw ) { + global $wgActorTableSchemaMigrationStage; + + if ( !$userId || ( $op !== '|' && $op !== '&' ) ) { + return false; // sanity check + } + if ( !$dbw instanceof IDatabase ) { + $dbw = wfGetDB( DB_MASTER ); + } + + # To suppress, we OR the current bitfields with Revision::DELETED_USER + # to put a 1 in the username *_deleted bit. To unsuppress we AND the + # current bitfields with the inverse of Revision::DELETED_USER. The + # username bit is made to 0 (x & 0 = 0), while others are unchanged (x & 1 = x). + # The same goes for the sysop-restricted *_deleted bit. + $delUser = Revision::DELETED_USER | Revision::DELETED_RESTRICTED; + $delAction = LogPage::DELETED_ACTION | Revision::DELETED_RESTRICTED; + if ( $op == '&' ) { + $delUser = $dbw->bitNot( $delUser ); + $delAction = $dbw->bitNot( $delAction ); + } + + # Normalize user name + $userTitle = Title::makeTitleSafe( NS_USER, $name ); + $userDbKey = $userTitle->getDBkey(); + + if ( $wgActorTableSchemaMigrationStage < MIGRATION_NEW ) { + # Hide name from live edits + $dbw->update( + 'revision', + [ self::buildSetBitDeletedField( 'rev_deleted', $op, $delUser, $dbw ) ], + [ 'rev_user' => $userId ], + __METHOD__ ); + + # Hide name from deleted edits + $dbw->update( + 'archive', + [ self::buildSetBitDeletedField( 'ar_deleted', $op, $delUser, $dbw ) ], + [ 'ar_user_text' => $name ], + __METHOD__ + ); + + # Hide name from logs + $dbw->update( + 'logging', + [ self::buildSetBitDeletedField( 'log_deleted', $op, $delUser, $dbw ) ], + [ 'log_user' => $userId, 'log_type != ' . $dbw->addQuotes( 'suppress' ) ], + __METHOD__ + ); + + # Hide name from RC + $dbw->update( + 'recentchanges', + [ self::buildSetBitDeletedField( 'rc_deleted', $op, $delUser, $dbw ) ], + [ 'rc_user_text' => $name ], + __METHOD__ + ); + + # Hide name from live images + $dbw->update( + 'oldimage', + [ self::buildSetBitDeletedField( 'oi_deleted', $op, $delUser, $dbw ) ], + [ 'oi_user_text' => $name ], + __METHOD__ + ); + + # Hide name from deleted images + $dbw->update( + 'filearchive', + [ self::buildSetBitDeletedField( 'fa_deleted', $op, $delUser, $dbw ) ], + [ 'fa_user_text' => $name ], + __METHOD__ + ); + } + + if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) { + $actorId = $dbw->selectField( 'actor', 'actor_id', [ 'actor_name' => $name ], __METHOD__ ); + if ( $actorId ) { + # Hide name from live edits + $subquery = $dbw->selectSQLText( + 'revision_actor_temp', 'revactor_rev', [ 'revactor_actor' => $actorId ], __METHOD__ + ); + $dbw->update( + 'revision', + [ self::buildSetBitDeletedField( 'rev_deleted', $op, $delUser, $dbw ) ], + [ "rev_id IN ($subquery)" ], + __METHOD__ ); + + # Hide name from deleted edits + $dbw->update( + 'archive', + [ self::buildSetBitDeletedField( 'ar_deleted', $op, $delUser, $dbw ) ], + [ 'ar_actor' => $actorId ], + __METHOD__ + ); + + # Hide name from logs + $dbw->update( + 'logging', + [ self::buildSetBitDeletedField( 'log_deleted', $op, $delUser, $dbw ) ], + [ 'log_actor' => $actorId, 'log_type != ' . $dbw->addQuotes( 'suppress' ) ], + __METHOD__ + ); + + # Hide name from RC + $dbw->update( + 'recentchanges', + [ self::buildSetBitDeletedField( 'rc_deleted', $op, $delUser, $dbw ) ], + [ 'rc_actor' => $actorId ], + __METHOD__ + ); + + # Hide name from live images + $dbw->update( + 'oldimage', + [ self::buildSetBitDeletedField( 'oi_deleted', $op, $delUser, $dbw ) ], + [ 'oi_actor' => $actorId ], + __METHOD__ + ); + + # Hide name from deleted images + $dbw->update( + 'filearchive', + [ self::buildSetBitDeletedField( 'fa_deleted', $op, $delUser, $dbw ) ], + [ 'fa_actor' => $actorId ], + __METHOD__ + ); + } + } + + # Hide log entries pointing to the user page + $dbw->update( + 'logging', + [ self::buildSetBitDeletedField( 'log_deleted', $op, $delAction, $dbw ) ], + [ 'log_namespace' => NS_USER, 'log_title' => $userDbKey, + 'log_type != ' . $dbw->addQuotes( 'suppress' ) ], + __METHOD__ + ); + + # Hide RC entries pointing to the user page + $dbw->update( + 'recentchanges', + [ self::buildSetBitDeletedField( 'rc_deleted', $op, $delAction, $dbw ) ], + [ 'rc_namespace' => NS_USER, 'rc_title' => $userDbKey, 'rc_logid > 0' ], + __METHOD__ + ); + + # Done! + return true; + } + + private static function buildSetBitDeletedField( $field, $op, $value, $dbw ) { + return $field . ' = ' . ( $op === '&' + ? $dbw->bitAnd( $field, $value ) + : $dbw->bitOr( $field, $value ) ); + } + + public static function suppressUserName( $name, $userId, $dbw = null ) { + return self::setUsernameBitfields( $name, $userId, '|', $dbw ); + } + + public static function unsuppressUserName( $name, $userId, $dbw = null ) { + return self::setUsernameBitfields( $name, $userId, '&', $dbw ); + } +} diff --git a/www/wiki/includes/revisiondelete/RevisionDeleter.php b/www/wiki/includes/revisiondelete/RevisionDeleter.php new file mode 100644 index 00000000..7b2147a5 --- /dev/null +++ b/www/wiki/includes/revisiondelete/RevisionDeleter.php @@ -0,0 +1,251 @@ +<?php +/** + * Revision/log/file deletion backend + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup RevisionDelete + */ + +/** + * General controller for RevDel, used by both SpecialRevisiondelete and + * ApiRevisionDelete. + * @ingroup RevisionDelete + */ +class RevisionDeleter { + /** List of known revdel types, with their corresponding list classes */ + private static $allowedTypes = [ + 'revision' => RevDelRevisionList::class, + 'archive' => RevDelArchiveList::class, + 'oldimage' => RevDelFileList::class, + 'filearchive' => RevDelArchivedFileList::class, + 'logging' => RevDelLogList::class, + ]; + + /** Type map to support old log entries */ + private static $deprecatedTypeMap = [ + 'oldid' => 'revision', + 'artimestamp' => 'archive', + 'oldimage' => 'oldimage', + 'fileid' => 'filearchive', + 'logid' => 'logging', + ]; + + /** + * Lists the valid possible types for revision deletion. + * + * @since 1.22 + * @return array + */ + public static function getTypes() { + return array_keys( self::$allowedTypes ); + } + + /** + * Gets the canonical type name, if any. + * + * @since 1.22 + * @param string $typeName + * @return string|null + */ + public static function getCanonicalTypeName( $typeName ) { + if ( isset( self::$deprecatedTypeMap[$typeName] ) ) { + $typeName = self::$deprecatedTypeMap[$typeName]; + } + return isset( self::$allowedTypes[$typeName] ) ? $typeName : null; + } + + /** + * Instantiate the appropriate list class for a given list of IDs. + * + * @since 1.22 + * @param string $typeName RevDel type, see RevisionDeleter::getTypes() + * @param IContextSource $context + * @param Title $title + * @param array $ids + * @return RevDelList + * @throws MWException + */ + public static function createList( $typeName, IContextSource $context, Title $title, array $ids ) { + $typeName = self::getCanonicalTypeName( $typeName ); + if ( !$typeName ) { + throw new MWException( __METHOD__ . ": Unknown RevDel type '$typeName'" ); + } + $class = self::$allowedTypes[$typeName]; + return new $class( $context, $title, $ids ); + } + + /** + * Checks for a change in the bitfield for a certain option and updates the + * provided array accordingly. + * + * @param string $desc Description to add to the array if the option was + * enabled / disabled. + * @param int $field The bitmask describing the single option. + * @param int $diff The xor of the old and new bitfields. + * @param int $new The new bitfield + * @param array &$arr The array to update. + */ + protected static function checkItem( $desc, $field, $diff, $new, &$arr ) { + if ( $diff & $field ) { + $arr[( $new & $field ) ? 0 : 1][] = $desc; + } + } + + /** + * Gets an array of message keys describing the changes made to the + * visibility of the revision. + * + * If the resulting array is $arr, then $arr[0] will contain an array of + * keys describing the items that were hidden, $arr[1] will contain + * an array of keys describing the items that were unhidden, and $arr[2] + * will contain an array with a single message key, which can be one of + * "revdelete-restricted", "revdelete-unrestricted" indicating (un)suppression + * or null to indicate nothing in particular. + * You can turn the keys in $arr[0] and $arr[1] into message keys by + * appending -hid and -unhid to the keys respectively. + * + * @param int $n The new bitfield. + * @param int $o The old bitfield. + * @return array An array as described above. + * @since 1.19 public + */ + public static function getChanges( $n, $o ) { + $diff = $n ^ $o; + $ret = [ 0 => [], 1 => [], 2 => [] ]; + // Build bitfield changes in language + self::checkItem( 'revdelete-content', + Revision::DELETED_TEXT, $diff, $n, $ret ); + self::checkItem( 'revdelete-summary', + Revision::DELETED_COMMENT, $diff, $n, $ret ); + self::checkItem( 'revdelete-uname', + Revision::DELETED_USER, $diff, $n, $ret ); + // Restriction application to sysops + if ( $diff & Revision::DELETED_RESTRICTED ) { + if ( $n & Revision::DELETED_RESTRICTED ) { + $ret[2][] = 'revdelete-restricted'; + } else { + $ret[2][] = 'revdelete-unrestricted'; + } + } + return $ret; + } + + /** Get DB field name for URL param... + * Future code for other things may also track + * other types of revision-specific changes. + * @param string $typeName + * @return string One of log_id/rev_id/fa_id/ar_timestamp/oi_archive_name + */ + public static function getRelationType( $typeName ) { + $typeName = self::getCanonicalTypeName( $typeName ); + if ( !$typeName ) { + return null; + } + return call_user_func( [ self::$allowedTypes[$typeName], 'getRelationType' ] ); + } + + /** + * Get the user right required for the RevDel type + * @since 1.22 + * @param string $typeName + * @return string User right + */ + public static function getRestriction( $typeName ) { + $typeName = self::getCanonicalTypeName( $typeName ); + if ( !$typeName ) { + return null; + } + return call_user_func( [ self::$allowedTypes[$typeName], 'getRestriction' ] ); + } + + /** + * Get the revision deletion constant for the RevDel type + * @since 1.22 + * @param string $typeName + * @return int RevDel constant + */ + public static function getRevdelConstant( $typeName ) { + $typeName = self::getCanonicalTypeName( $typeName ); + if ( !$typeName ) { + return null; + } + return call_user_func( [ self::$allowedTypes[$typeName], 'getRevdelConstant' ] ); + } + + /** + * Suggest a target for the revision deletion + * @since 1.22 + * @param string $typeName + * @param Title|null $target User-supplied target + * @param array $ids + * @return Title|null + */ + public static function suggestTarget( $typeName, $target, array $ids ) { + $typeName = self::getCanonicalTypeName( $typeName ); + if ( !$typeName ) { + return $target; + } + return call_user_func( [ self::$allowedTypes[$typeName], 'suggestTarget' ], $target, $ids ); + } + + /** + * Checks if a revision still exists in the revision table. + * If it doesn't, returns the corresponding ar_timestamp field + * so that this key can be used instead. + * + * @param Title $title + * @param int $revid + * @return bool|mixed + */ + public static function checkRevisionExistence( $title, $revid ) { + $dbr = wfGetDB( DB_REPLICA ); + $exists = $dbr->selectField( 'revision', '1', + [ 'rev_id' => $revid ], __METHOD__ ); + + if ( $exists ) { + return true; + } + + $timestamp = $dbr->selectField( 'archive', 'ar_timestamp', + [ 'ar_namespace' => $title->getNamespace(), + 'ar_title' => $title->getDBkey(), + 'ar_rev_id' => $revid ], __METHOD__ ); + + return $timestamp; + } + + /** + * Put together a rev_deleted bitfield + * @since 1.22 + * @param array $bitPars ExtractBitParams() params + * @param int $oldfield Current bitfield + * @return int + */ + public static function extractBitfield( array $bitPars, $oldfield ) { + // Build the actual new rev_deleted bitfield + $newBits = 0; + foreach ( $bitPars as $const => $val ) { + if ( $val == 1 ) { + $newBits |= $const; // $const is the *_deleted const + } elseif ( $val == -1 ) { + $newBits |= ( $oldfield & $const ); // use existing + } + } + return $newBits; + } +} |