From 07842be379ca3d4d0bc0608c217dd0e8cd7cbe4b Mon Sep 17 00:00:00 2001 From: Brad Jorsch Date: Fri, 16 Feb 2018 13:23:45 -0500 Subject: [PATCH] API: Update query modules for MCR MCR deprecated the Revision class in favor of the broadly similar RevisionRecord, and more interestingly added the concept of multiple content "slots" to revisions. Thus, prop=revisions, prop=deletedrevisions, and so on gain a parameter to specify which slots are wanted. When this new parameter is not specified (and any content-related props are specified), a warning about the legacy format will be issued. The rest of the modules just needed to call methods or use constants on RevisionRecord instead of Revision. ApiQueryDeletedrevs wasn't touched, since it has been deprecated since 1.25 anyway. This also updates a few non-query modules that don't depend on details of editing, diffing, or viewing MCR revisions that haven't been figured out yet. Bug: T200568 Change-Id: I1327d1784f5cedb006cd74df834cf9a560a77a5d --- RELEASE-NOTES-1.32 | 20 + includes/Storage/RevisionRecord.php | 2 +- includes/api/ApiExpandTemplates.php | 8 +- includes/api/ApiFeedContributions.php | 37 +- includes/api/ApiPatrol.php | 7 +- includes/api/ApiQueryAllDeletedRevisions.php | 14 +- includes/api/ApiQueryAllRevisions.php | 14 +- includes/api/ApiQueryContributors.php | 9 +- includes/api/ApiQueryDeletedRevisions.php | 12 +- includes/api/ApiQueryFilearchive.php | 6 +- includes/api/ApiQueryRecentChanges.php | 24 +- includes/api/ApiQueryRevisions.php | 17 +- includes/api/ApiQueryRevisionsBase.php | 433 +++++++++++++----- includes/api/ApiQueryUserContribs.php | 24 +- includes/api/ApiQueryWatchlist.php | 15 +- includes/api/ApiRevisionDelete.php | 12 +- includes/api/ApiSetNotificationTimestamp.php | 18 +- includes/api/ApiTag.php | 10 +- includes/api/i18n/en.json | 12 +- includes/api/i18n/qqq.json | 6 +- .../api/query/ApiQueryRevisionsTest.php | 7 +- 21 files changed, 507 insertions(+), 200 deletions(-) diff --git a/RELEASE-NOTES-1.32 b/RELEASE-NOTES-1.32 index e24ab83cfc..ce0b5c51b2 100644 --- a/RELEASE-NOTES-1.32 +++ b/RELEASE-NOTES-1.32 @@ -100,12 +100,32 @@ production. * 'missingparam' errors will now use the prefixed parameter name in the code and error text, e.g. "noxxfoo" and "The 'xxfoo' parameter must be set" rather than "nofoo" and "The 'foo' parameter must be set". +* action=query&prop=revisions now takes a 'rvslots' parameter to indicate the + multi-content revision slots for which content should be returned. It also + has a new rvprop, 'roles', to indicate which roles have slots. A deprecation + warning will be issued if rvprop=content or rvprop=contentmodel are used + without rvslots. +* The rvcontentformat parameter to action=query&prop=revisions has been + deprecated. Clients should be prepared to deal with the default format for + relevant models. +* Use of the deprecated parameters rvexpandtemplates, rvgeneratexml, rvparse, + rvdiffto, rvdifftotext, rvdifftotextpst, rvcontentformat, or the deprecated + rvprop=parsetree is forbidden with the new 'rvslots' parameter. +* action=query&prop=deletedrevisions, action=query&list=allrevisions, and + action=query&list=alldeletedrevisions are changed similarly to + &prop=revisions (see the three previous items). === Action API internal changes in 1.32 === * Added 'ApiParseMakeOutputPage' hook. * Parameter names may no longer contain '{' or '}', as these are now used for templated parameters. * (T194950) Added 'ApiMaxLagInfo' hook. +* Added 'ApiParseMakeOutputPage' hook. +* The following methods now take a RevisionRecord rather than a Revision. No + external callers are known. + * ApiFeedContributions::feedItemAuthor() + * ApiFeedContributions::feedItemDesc() + * ApiQueryRevisionsBase::extractRevisionInfo() === Languages updated in 1.32 === MediaWiki supports over 350 languages. Many localisations are updated regularly. diff --git a/includes/Storage/RevisionRecord.php b/includes/Storage/RevisionRecord.php index 7d1b4778d4..17c56ea0ea 100644 --- a/includes/Storage/RevisionRecord.php +++ b/includes/Storage/RevisionRecord.php @@ -453,7 +453,7 @@ abstract class RevisionRecord { * * @return bool */ - protected function audienceCan( $field, $audience, User $user = null ) { + public function audienceCan( $field, $audience, User $user = null ) { if ( $audience == self::FOR_PUBLIC && $this->isDeleted( $field ) ) { return false; } elseif ( $audience == self::FOR_THIS_USER ) { diff --git a/includes/api/ApiExpandTemplates.php b/includes/api/ApiExpandTemplates.php index fe49b25937..562bcdf9da 100644 --- a/includes/api/ApiExpandTemplates.php +++ b/includes/api/ApiExpandTemplates.php @@ -20,6 +20,8 @@ * @file */ +use MediaWiki\MediaWikiServices; + /** * API module that functions as a shortcut to the wikitext preprocessor. Expands * any templates in a provided string, and returns the result of this expansion @@ -48,7 +50,7 @@ class ApiExpandTemplates extends ApiBase { if ( $params['prop'] === null ) { $this->addDeprecation( - 'apiwarn-deprecation-expandtemplates-prop', 'action=expandtemplates&!prop' + [ 'apiwarn-deprecation-missingparam', 'prop' ], 'action=expandtemplates&!prop' ); $prop = []; } else { @@ -63,12 +65,12 @@ class ApiExpandTemplates extends ApiBase { // Get title and revision ID for parser $revid = $params['revid']; if ( $revid !== null ) { - $rev = Revision::newFromId( $revid ); + $rev = MediaWikiServices::getInstance()->getRevisionStore()->getRevisionById( $revid ); if ( !$rev ) { $this->dieWithError( [ 'apierror-nosuchrevid', $revid ] ); } $pTitleObj = $titleObj; - $titleObj = $rev->getTitle(); + $titleObj = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() ); if ( $titleProvided ) { if ( !$titleObj->equals( $pTitleObj ) ) { $this->addWarning( [ 'apierror-revwrongpage', $rev->getId(), diff --git a/includes/api/ApiFeedContributions.php b/includes/api/ApiFeedContributions.php index 61a9035895..92d504e67b 100644 --- a/includes/api/ApiFeedContributions.php +++ b/includes/api/ApiFeedContributions.php @@ -20,11 +20,19 @@ * @file */ +use MediaWiki\MediaWikiServices; +use MediaWiki\Storage\RevisionAccessException; +use MediaWiki\Storage\RevisionRecord; +use MediaWiki\Storage\RevisionStore; + /** * @ingroup API */ class ApiFeedContributions extends ApiBase { + /** @var RevisionStore */ + private $revisionStore; + /** * This module uses a custom feed wrapper printer. * @@ -35,6 +43,8 @@ class ApiFeedContributions extends ApiBase { } public function execute() { + $this->revisionStore = MediaWikiServices::getInstance()->getRevisionStore(); + $params = $this->extractRequestParams(); $config = $this->getConfig(); @@ -130,7 +140,7 @@ class ApiFeedContributions extends ApiBase { if ( $title && $title->userCan( 'read', $this->getUser() ) ) { $date = $row->rev_timestamp; $comments = $title->getTalkPage()->getFullURL(); - $revision = Revision::newFromRow( $row ); + $revision = $this->revisionStore->newRevisionFromRow( $row ); return new FeedItem( $title->getPrefixedText(), @@ -146,21 +156,28 @@ class ApiFeedContributions extends ApiBase { } /** - * @param Revision $revision + * @since 1.32, takes a RevisionRecord instead of a Revision + * @param RevisionRecord $revision * @return string */ - protected function feedItemAuthor( $revision ) { - return $revision->getUserText(); + protected function feedItemAuthor( RevisionRecord $revision ) { + $user = $revision->getUser(); + return $user ? $user->getName() : ''; } /** - * @param Revision $revision + * @since 1.32, takes a RevisionRecord instead of a Revision + * @param RevisionRecord $revision * @return string */ - protected function feedItemDesc( $revision ) { + protected function feedItemDesc( RevisionRecord $revision ) { if ( $revision ) { $msg = wfMessage( 'colon-separator' )->inContentLanguage()->text(); - $content = $revision->getContent(); + try { + $content = $revision->getContent( 'main' ); + } catch ( RevisionAccessException $e ) { + $content = null; + } if ( $content instanceof TextContent ) { // only textual content has a "source view". @@ -173,8 +190,10 @@ class ApiFeedContributions extends ApiBase { $html = ''; } - return '

' . htmlspecialchars( $revision->getUserText() ) . $msg . - htmlspecialchars( FeedItem::stripComment( $revision->getComment() ) ) . + $comment = $revision->getComment(); + + return '

' . htmlspecialchars( $this->feedItemAuthor( $revision ) ) . $msg . + htmlspecialchars( FeedItem::stripComment( $comment ? $comment->text : '' ) ) . "

\n
\n
" . $html . '
'; } diff --git a/includes/api/ApiPatrol.php b/includes/api/ApiPatrol.php index a20aca4db3..2b65f95128 100644 --- a/includes/api/ApiPatrol.php +++ b/includes/api/ApiPatrol.php @@ -22,6 +22,8 @@ * @file */ +use MediaWiki\MediaWikiServices; + /** * Allows user to patrol pages * @ingroup API @@ -41,11 +43,12 @@ class ApiPatrol extends ApiBase { $this->dieWithError( [ 'apierror-nosuchrcid', $params['rcid'] ] ); } } else { - $rev = Revision::newFromId( $params['revid'] ); + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $rev = $store->getRevisionById( $params['revid'] ); if ( !$rev ) { $this->dieWithError( [ 'apierror-nosuchrevid', $params['revid'] ] ); } - $rc = $rev->getRecentChange(); + $rc = $store->getRecentChange( $rev ); if ( !$rc ) { $this->dieWithError( [ 'apierror-notpatrollable', $params['revid'] ] ); } diff --git a/includes/api/ApiQueryAllDeletedRevisions.php b/includes/api/ApiQueryAllDeletedRevisions.php index 87f99dde9e..50afc7de3d 100644 --- a/includes/api/ApiQueryAllDeletedRevisions.php +++ b/includes/api/ApiQueryAllDeletedRevisions.php @@ -23,6 +23,9 @@ * @file */ +use MediaWiki\MediaWikiServices; +use MediaWiki\Storage\RevisionRecord; + /** * Query module to enumerate all deleted revisions. * @@ -45,6 +48,7 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase { $user = $this->getUser(); $db = $this->getDB(); $params = $this->extractRequestParams( false ); + $revisionStore = MediaWikiServices::getInstance()->getRevisionStore(); $result = $this->getResult(); @@ -103,7 +107,7 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase { if ( $resultPageSet === null ) { $this->parseParameters( $params ); - $arQuery = Revision::getArchiveQueryInfo(); + $arQuery = $revisionStore->getArchiveQueryInfo(); $this->addTables( $arQuery['tables'] ); $this->addJoinConds( $arQuery['joins'] ); $this->addFields( $arQuery['fields'] ); @@ -235,9 +239,9 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase { // (shouldn't be able to get here without 'deletedhistory', but // check it again just in case) if ( !$user->isAllowed( 'deletedhistory' ) ) { - $bitmask = Revision::DELETED_USER; + $bitmask = RevisionRecord::DELETED_USER; } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { - $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED; + $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED; } else { $bitmask = 0; } @@ -343,13 +347,13 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase { $generated[] = $row->ar_rev_id; } } else { - $revision = Revision::newFromArchiveRow( $row ); + $revision = $revisionStore->newRevisionFromArchiveRow( $row ); $rev = $this->extractRevisionInfo( $revision, $row ); if ( !isset( $pageMap[$row->ar_namespace][$row->ar_title] ) ) { $index = $nextIndex++; $pageMap[$row->ar_namespace][$row->ar_title] = $index; - $title = $revision->getTitle(); + $title = Title::newFromLinkTarget( $revision->getPageAsLinkTarget() ); $a = [ 'pageid' => $title->getArticleID(), 'revisions' => [ $rev ], diff --git a/includes/api/ApiQueryAllRevisions.php b/includes/api/ApiQueryAllRevisions.php index a0e71a571f..833e2e4a1a 100644 --- a/includes/api/ApiQueryAllRevisions.php +++ b/includes/api/ApiQueryAllRevisions.php @@ -20,6 +20,9 @@ * @file */ +use MediaWiki\MediaWikiServices; +use MediaWiki\Storage\RevisionRecord; + /** * Query module to enumerate all revisions. * @@ -39,6 +42,7 @@ class ApiQueryAllRevisions extends ApiQueryRevisionsBase { protected function run( ApiPageSet $resultPageSet = null ) { $db = $this->getDB(); $params = $this->extractRequestParams( false ); + $revisionStore = MediaWikiServices::getInstance()->getRevisionStore(); $result = $this->getResult(); @@ -63,7 +67,7 @@ class ApiQueryAllRevisions extends ApiQueryRevisionsBase { if ( $resultPageSet === null ) { $this->parseParameters( $params ); - $revQuery = Revision::getQueryInfo( + $revQuery = $revisionStore->getQueryInfo( $this->fetchContent ? [ 'page', 'text' ] : [ 'page' ] ); $this->addTables( $revQuery['tables'] ); @@ -120,9 +124,9 @@ class ApiQueryAllRevisions extends ApiQueryRevisionsBase { if ( $params['user'] !== null || $params['excludeuser'] !== null ) { // Paranoia: avoid brute force searches (T19342) if ( !$this->getUser()->isAllowed( 'deletedhistory' ) ) { - $bitmask = Revision::DELETED_USER; + $bitmask = RevisionRecord::DELETED_USER; } elseif ( !$this->getUser()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { - $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED; + $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED; } else { $bitmask = 0; } @@ -185,13 +189,13 @@ class ApiQueryAllRevisions extends ApiQueryRevisionsBase { $generated[] = $row->rev_id; } } else { - $revision = Revision::newFromRow( $row ); + $revision = $revisionStore->newRevisionFromRow( $row ); $rev = $this->extractRevisionInfo( $revision, $row ); if ( !isset( $pageMap[$row->rev_page] ) ) { $index = $nextIndex++; $pageMap[$row->rev_page] = $index; - $title = $revision->getTitle(); + $title = Title::newFromLinkTarget( $revision->getPageAsLinkTarget() ); $a = [ 'pageid' => $title->getArticleID(), 'revisions' => [ $rev ], diff --git a/includes/api/ApiQueryContributors.php b/includes/api/ApiQueryContributors.php index 6848fcbd62..e39afac6cf 100644 --- a/includes/api/ApiQueryContributors.php +++ b/includes/api/ApiQueryContributors.php @@ -23,6 +23,9 @@ * @since 1.23 */ +use MediaWiki\MediaWikiServices; +use MediaWiki\Storage\RevisionRecord; + /** * A query module to show contributors to a page * @@ -75,7 +78,7 @@ class ApiQueryContributors extends ApiQueryBase { } $result = $this->getResult(); - $revQuery = Revision::getQueryInfo(); + $revQuery = MediaWikiServices::getInstance()->getRevisionStore()->getQueryInfo(); // For MIGRATION_NEW, target indexes on the revision_actor_temp table. // Otherwise, revision is fine because it'll have to check all revision rows anyway. @@ -94,7 +97,7 @@ class ApiQueryContributors extends ApiQueryBase { ] ); $this->addWhereFld( $pageField, $pages ); $this->addWhere( ActorMigration::newMigration()->isAnon( $revQuery['fields']['rev_user'] ) ); - $this->addWhere( $db->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ); + $this->addWhere( $db->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER ) . ' = 0' ); $this->addOption( 'GROUP BY', $pageField ); $res = $this->select( __METHOD__ ); foreach ( $res as $row ) { @@ -126,7 +129,7 @@ class ApiQueryContributors extends ApiQueryBase { ] ); $this->addWhereFld( $pageField, $pages ); $this->addWhere( ActorMigration::newMigration()->isNotAnon( $revQuery['fields']['rev_user'] ) ); - $this->addWhere( $db->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ); + $this->addWhere( $db->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER ) . ' = 0' ); $this->addOption( 'GROUP BY', [ $pageField, $idField ] ); $this->addOption( 'LIMIT', $params['limit'] + 1 ); diff --git a/includes/api/ApiQueryDeletedRevisions.php b/includes/api/ApiQueryDeletedRevisions.php index 1a1e8f7ac5..c3af71bff8 100644 --- a/includes/api/ApiQueryDeletedRevisions.php +++ b/includes/api/ApiQueryDeletedRevisions.php @@ -23,6 +23,9 @@ * @file */ +use MediaWiki\MediaWikiServices; +use MediaWiki\Storage\RevisionRecord; + /** * Query module to enumerate deleted revisions for pages. * @@ -55,12 +58,13 @@ class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase { $params = $this->extractRequestParams( false ); $db = $this->getDB(); + $revisionStore = MediaWikiServices::getInstance()->getRevisionStore(); $this->requireMaxOneParameter( $params, 'user', 'excludeuser' ); if ( $resultPageSet === null ) { $this->parseParameters( $params ); - $arQuery = Revision::getArchiveQueryInfo(); + $arQuery = $revisionStore->getArchiveQueryInfo(); $this->addTables( $arQuery['tables'] ); $this->addFields( $arQuery['fields'] ); $this->addJoinConds( $arQuery['joins'] ); @@ -132,9 +136,9 @@ class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase { // (shouldn't be able to get here without 'deletedhistory', but // check it again just in case) if ( !$user->isAllowed( 'deletedhistory' ) ) { - $bitmask = Revision::DELETED_USER; + $bitmask = RevisionRecord::DELETED_USER; } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { - $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED; + $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED; } else { $bitmask = 0; } @@ -234,7 +238,7 @@ class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase { $fit = $this->addPageSubItem( $pageMap[$row->ar_namespace][$row->ar_title], - $this->extractRevisionInfo( Revision::newFromArchiveRow( $row ), $row ), + $this->extractRevisionInfo( $revisionStore->newRevisionFromArchiveRow( $row ), $row ), 'rev' ); if ( !$fit ) { diff --git a/includes/api/ApiQueryFilearchive.php b/includes/api/ApiQueryFilearchive.php index ebffb151c8..a6a325132a 100644 --- a/includes/api/ApiQueryFilearchive.php +++ b/includes/api/ApiQueryFilearchive.php @@ -24,6 +24,8 @@ * @file */ +use MediaWiki\Storage\RevisionRecord; + /** * Query module to enumerate all deleted files. * @@ -153,7 +155,7 @@ class ApiQueryFilearchive extends ApiQueryBase { self::addTitleInfo( $file, $title ); if ( $fld_description && - Revision::userCanBitfield( $row->fa_deleted, File::DELETED_COMMENT, $user ) + RevisionRecord::userCanBitfield( $row->fa_deleted, File::DELETED_COMMENT, $user ) ) { $file['description'] = $commentStore->getComment( 'fa_description', $row )->text; if ( isset( $prop['parseddescription'] ) ) { @@ -162,7 +164,7 @@ class ApiQueryFilearchive extends ApiQueryBase { } } if ( $fld_user && - Revision::userCanBitfield( $row->fa_deleted, File::DELETED_USER, $user ) + RevisionRecord::userCanBitfield( $row->fa_deleted, File::DELETED_USER, $user ) ) { $file['userid'] = (int)$row->fa_user; $file['user'] = $row->fa_user_text; diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php index f870d45d60..a5be58b7ef 100644 --- a/includes/api/ApiQueryRecentChanges.php +++ b/includes/api/ApiQueryRecentChanges.php @@ -20,6 +20,8 @@ * @file */ +use MediaWiki\Storage\RevisionRecord; + /** * A query action to enumerate the recent changes that were done to the wiki. * Various filters are supported. @@ -365,9 +367,9 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { // Paranoia: avoid brute force searches (T19342) if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) { if ( !$user->isAllowed( 'deletedhistory' ) ) { - $bitmask = Revision::DELETED_USER; + $bitmask = RevisionRecord::DELETED_USER; } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { - $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED; + $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED; } else { $bitmask = 0; } @@ -507,11 +509,11 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { /* Add user data and 'anon' flag, if user is anonymous. */ if ( $this->fld_user || $this->fld_userid ) { - if ( $row->rc_deleted & Revision::DELETED_USER ) { + if ( $row->rc_deleted & RevisionRecord::DELETED_USER ) { $vals['userhidden'] = true; $anyHidden = true; } - if ( Revision::userCanBitfield( $row->rc_deleted, Revision::DELETED_USER, $user ) ) { + if ( RevisionRecord::userCanBitfield( $row->rc_deleted, RevisionRecord::DELETED_USER, $user ) ) { if ( $this->fld_user ) { $vals['user'] = $row->rc_user_text; } @@ -546,11 +548,13 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { /* Add edit summary / log summary. */ if ( $this->fld_comment || $this->fld_parsedcomment ) { - if ( $row->rc_deleted & Revision::DELETED_COMMENT ) { + if ( $row->rc_deleted & RevisionRecord::DELETED_COMMENT ) { $vals['commenthidden'] = true; $anyHidden = true; } - if ( Revision::userCanBitfield( $row->rc_deleted, Revision::DELETED_COMMENT, $user ) ) { + if ( RevisionRecord::userCanBitfield( + $row->rc_deleted, RevisionRecord::DELETED_COMMENT, $user + ) ) { $comment = $this->commentStore->getComment( 'rc_comment', $row )->text; if ( $this->fld_comment ) { $vals['comment'] = $comment; @@ -597,11 +601,13 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { } if ( $this->fld_sha1 && $row->rev_sha1 !== null ) { - if ( $row->rev_deleted & Revision::DELETED_TEXT ) { + if ( $row->rev_deleted & RevisionRecord::DELETED_TEXT ) { $vals['sha1hidden'] = true; $anyHidden = true; } - if ( Revision::userCanBitfield( $row->rev_deleted, Revision::DELETED_TEXT, $user ) ) { + if ( RevisionRecord::userCanBitfield( + $row->rev_deleted, RevisionRecord::DELETED_TEXT, $user + ) ) { if ( $row->rev_sha1 !== '' ) { $vals['sha1'] = Wikimedia\base_convert( $row->rev_sha1, 36, 16, 40 ); } else { @@ -623,7 +629,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { } } - if ( $anyHidden && ( $row->rc_deleted & Revision::DELETED_RESTRICTED ) ) { + if ( $anyHidden && ( $row->rc_deleted & RevisionRecord::DELETED_RESTRICTED ) ) { $vals['suppressed'] = true; } diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index 5858bc726d..5e7b864b54 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -20,6 +20,9 @@ * @file */ +use MediaWiki\MediaWikiServices; +use MediaWiki\Storage\RevisionRecord; + /** * A query action to enumerate revisions of a given page, or show top revisions * of multiple pages. Various pieces of information may be shown - flags, @@ -81,6 +84,7 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase { protected function run( ApiPageSet $resultPageSet = null ) { $params = $this->extractRequestParams( false ); + $revisionStore = MediaWikiServices::getInstance()->getRevisionStore(); // If any of those parameters are used, work in 'enumeration' mode. // Enum mode can only be used when exactly one page is provided. @@ -139,7 +143,7 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase { if ( $this->fld_user ) { $opts[] = 'user'; } - $revQuery = Revision::getQueryInfo( $opts ); + $revQuery = $revisionStore->getQueryInfo( $opts ); $this->addTables( $revQuery['tables'] ); $this->addFields( $revQuery['fields'] ); $this->addJoinConds( $revQuery['joins'] ); @@ -301,9 +305,9 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase { if ( $params['user'] !== null || $params['excludeuser'] !== null ) { // Paranoia: avoid brute force searches (T19342) if ( !$this->getUser()->isAllowed( 'deletedhistory' ) ) { - $bitmask = Revision::DELETED_USER; + $bitmask = RevisionRecord::DELETED_USER; } elseif ( !$this->getUser()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { - $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED; + $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED; } else { $bitmask = 0; } @@ -382,14 +386,15 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase { if ( $resultPageSet !== null ) { $generated[] = $row->rev_id; } else { - $revision = new Revision( $row ); + $revision = $revisionStore->newRevisionFromRow( $row ); $rev = $this->extractRevisionInfo( $revision, $row ); if ( $this->token !== null ) { - $title = $revision->getTitle(); + $title = Title::newFromLinkTarget( $revision->getPageAsLinkTarget() ); + $revisionCompat = new Revision( $revision ); $tokenFunctions = $this->getTokenFunctions(); foreach ( $this->token as $t ) { - $val = call_user_func( $tokenFunctions[$t], $title->getArticleID(), $title, $revision ); + $val = call_user_func( $tokenFunctions[$t], $title->getArticleID(), $title, $revisionCompat ); if ( $val === false ) { $this->addWarning( [ 'apiwarn-tokennotallowed', $t ] ); } else { diff --git a/includes/api/ApiQueryRevisionsBase.php b/includes/api/ApiQueryRevisionsBase.php index 87c6f9d98e..600c89e2cf 100644 --- a/includes/api/ApiQueryRevisionsBase.php +++ b/includes/api/ApiQueryRevisionsBase.php @@ -20,6 +20,11 @@ * @file */ +use MediaWiki\Storage\RevisionAccessException; +use MediaWiki\Storage\RevisionRecord; +use MediaWiki\Storage\SlotRecord; +use MediaWiki\MediaWikiServices; + /** * A base class for functions common to producing a list of revisions. * @@ -27,13 +32,27 @@ */ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase { + /** + * @name Constants for internal use. Don't use externally. + * @{ + */ + + // Bits to indicate the results of the revdel permission check on a revision, + // see self::checkRevDel() + const IS_DELETED = 1; // Whether the field is revision-deleted + const CANNOT_VIEW = 2; // Whether the user cannot view the field due to revdel + + /**@}*/ + protected $limit, $diffto, $difftotext, $difftotextpst, $expandTemplates, $generateXML, - $section, $parseContent, $fetchContent, $contentFormat, $setParsedLimit = true; + $section, $parseContent, $fetchContent, $contentFormat, $setParsedLimit = true, + $slotRoles = null, $needSlots; protected $fld_ids = false, $fld_flags = false, $fld_timestamp = false, - $fld_size = false, $fld_sha1 = false, $fld_comment = false, - $fld_parsedcomment = false, $fld_user = false, $fld_userid = false, - $fld_content = false, $fld_tags = false, $fld_contentmodel = false, $fld_parsetree = false; + $fld_size = false, $fld_slotsize = false, $fld_sha1 = false, $fld_slotsha1 = false, + $fld_comment = false, $fld_parsedcomment = false, $fld_user = false, $fld_userid = false, + $fld_content = false, $fld_tags = false, $fld_contentmodel = false, $fld_roles = false, + $fld_parsetree = false; public function execute() { $this->run(); @@ -55,6 +74,55 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase { * @param array $params */ protected function parseParameters( $params ) { + $prop = array_flip( $params['prop'] ); + + $this->fld_ids = isset( $prop['ids'] ); + $this->fld_flags = isset( $prop['flags'] ); + $this->fld_timestamp = isset( $prop['timestamp'] ); + $this->fld_comment = isset( $prop['comment'] ); + $this->fld_parsedcomment = isset( $prop['parsedcomment'] ); + $this->fld_size = isset( $prop['size'] ); + $this->fld_slotsize = isset( $prop['slotsize'] ); + $this->fld_sha1 = isset( $prop['sha1'] ); + $this->fld_slotsha1 = isset( $prop['slotsha1'] ); + $this->fld_content = isset( $prop['content'] ); + $this->fld_contentmodel = isset( $prop['contentmodel'] ); + $this->fld_userid = isset( $prop['userid'] ); + $this->fld_user = isset( $prop['user'] ); + $this->fld_tags = isset( $prop['tags'] ); + $this->fld_roles = isset( $prop['roles'] ); + $this->fld_parsetree = isset( $prop['parsetree'] ); + + $this->slotRoles = $params['slots']; + + if ( $this->slotRoles !== null ) { + if ( $this->fld_parsetree ) { + $this->dieWithError( [ + 'apierror-invalidparammix-cannotusewith', + $this->encodeParamName( 'prop=parsetree' ), + $this->encodeParamName( 'slots' ), + ], 'invalidparammix' ); + } + foreach ( [ + 'expandtemplates', 'generatexml', 'parse', 'diffto', 'difftotext', 'difftotextpst', + 'contentformat' + ] as $p ) { + if ( $params[$p] !== null && $params[$p] !== false ) { + $this->dieWithError( [ + 'apierror-invalidparammix-cannotusewith', + $this->encodeParamName( $p ), + $this->encodeParamName( 'slots' ), + ], 'invalidparammix' ); + } + } + } + + if ( !empty( $params['contentformat'] ) ) { + $this->contentFormat = $params['contentformat']; + } + + $this->limit = $params['limit']; + if ( !is_null( $params['difftotext'] ) ) { $this->difftotext = $params['difftotext']; $this->difftotextpst = $params['difftotextpst']; @@ -72,11 +140,13 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase { // DifferenceEngine returns a rather ambiguous empty // string if that's not the case if ( $params['diffto'] != 0 ) { - $difftoRev = Revision::newFromId( $params['diffto'] ); + $difftoRev = MediaWikiServices::getInstance()->getRevisionStore() + ->getRevisionById( $params['diffto'] ); if ( !$difftoRev ) { $this->dieWithError( [ 'apierror-nosuchrevid', $params['diffto'] ] ); } - if ( !$difftoRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) { + $revDel = $this->checkRevDel( $difftoRev, RevisionRecord::DELETED_TEXT ); + if ( $revDel & self::CANNOT_VIEW ) { $this->addWarning( [ 'apiwarn-difftohidden', $difftoRev->getId() ] ); $params['diffto'] = null; } @@ -84,39 +154,6 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase { $this->diffto = $params['diffto']; } - $prop = array_flip( $params['prop'] ); - - $this->fld_ids = isset( $prop['ids'] ); - $this->fld_flags = isset( $prop['flags'] ); - $this->fld_timestamp = isset( $prop['timestamp'] ); - $this->fld_comment = isset( $prop['comment'] ); - $this->fld_parsedcomment = isset( $prop['parsedcomment'] ); - $this->fld_size = isset( $prop['size'] ); - $this->fld_sha1 = isset( $prop['sha1'] ); - $this->fld_content = isset( $prop['content'] ); - $this->fld_contentmodel = isset( $prop['contentmodel'] ); - $this->fld_userid = isset( $prop['userid'] ); - $this->fld_user = isset( $prop['user'] ); - $this->fld_tags = isset( $prop['tags'] ); - $this->fld_parsetree = isset( $prop['parsetree'] ); - - if ( $this->fld_parsetree ) { - $encParam = $this->encodeParamName( 'prop' ); - $name = $this->getModuleName(); - $parent = $this->getParent(); - $parentParam = $parent->encodeParamName( $parent->getModuleManager()->getModuleGroup( $name ) ); - $this->addDeprecation( - [ 'apiwarn-deprecation-parameter', "{$encParam}=parsetree" ], - "action=query&{$parentParam}={$name}&{$encParam}=parsetree" - ); - } - - if ( !empty( $params['contentformat'] ) ) { - $this->contentFormat = $params['contentformat']; - } - - $this->limit = $params['limit']; - $this->fetchContent = $this->fld_content || !is_null( $this->diffto ) || !is_null( $this->difftotext ) || $this->fld_parsetree; @@ -152,18 +189,46 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase { $this->limit = 10; } $this->validateLimit( 'limit', $this->limit, 1, $userMax, $botMax ); + + $this->needSlots = $this->fetchContent || $this->fld_contentmodel || + $this->fld_slotsize || $this->fld_slotsha1; + if ( $this->needSlots && $this->slotRoles === null ) { + $encParam = $this->encodeParamName( 'slots' ); + $name = $this->getModuleName(); + $parent = $this->getParent(); + $parentParam = $parent->encodeParamName( $parent->getModuleManager()->getModuleGroup( $name ) ); + $this->addDeprecation( + [ 'apiwarn-deprecation-missingparam', $encParam ], + "action=query&{$parentParam}={$name}&!{$encParam}" + ); + } } /** - * Extract information from the Revision + * Test revision deletion status + * @param RevisionRecord $revision Revision to check + * @param int $field One of the RevisionRecord::DELETED_* constants + * @return int Revision deletion status flags. Bitwise OR of + * self::IS_DELETED and self::CANNOT_VIEW, as appropriate. + */ + private function checkRevDel( RevisionRecord $revision, $field ) { + $ret = $revision->isDeleted( $field ) ? self::IS_DELETED : 0; + if ( $ret ) { + $canSee = $revision->audienceCan( $field, RevisionRecord::FOR_THIS_USER, $this->getUser() ); + $ret = $ret | ( $canSee ? 0 : self::CANNOT_VIEW ); + } + return $ret; + } + + /** + * Extract information from the RevisionRecord * - * @param Revision $revision + * @since 1.32, takes a RevisionRecord instead of a Revision + * @param RevisionRecord $revision Revision * @param object $row Should have a field 'ts_tags' if $this->fld_tags is set * @return array */ - protected function extractRevisionInfo( Revision $revision, $row ) { - $title = $revision->getTitle(); - $user = $this->getUser(); + protected function extractRevisionInfo( RevisionRecord $revision, $row ) { $vals = []; $anyHidden = false; @@ -179,15 +244,17 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase { } if ( $this->fld_user || $this->fld_userid ) { - if ( $revision->isDeleted( Revision::DELETED_USER ) ) { + $revDel = $this->checkRevDel( $revision, RevisionRecord::DELETED_USER ); + if ( ( $revDel & self::IS_DELETED ) ) { $vals['userhidden'] = true; $anyHidden = true; } - if ( $revision->userCan( Revision::DELETED_USER, $user ) ) { + if ( !( $revDel & self::CANNOT_VIEW ) ) { + $u = $revision->getUser( RevisionRecord::RAW ); if ( $this->fld_user ) { - $vals['user'] = $revision->getUserText( Revision::RAW ); + $vals['user'] = $u->getName(); } - $userid = $revision->getUser( Revision::RAW ); + $userid = $u->getId(); if ( !$userid ) { $vals['anon'] = true; } @@ -203,45 +270,115 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase { } if ( $this->fld_size ) { - if ( !is_null( $revision->getSize() ) ) { + try { $vals['size'] = intval( $revision->getSize() ); - } else { + } catch ( RevisionAccessException $e ) { + // Back compat: If there's no size, return 0. + // @todo: Gergő says to mention T198099 as a "todo" here. $vals['size'] = 0; } } if ( $this->fld_sha1 ) { - if ( $revision->isDeleted( Revision::DELETED_TEXT ) ) { + $revDel = $this->checkRevDel( $revision, RevisionRecord::DELETED_TEXT ); + if ( ( $revDel & self::IS_DELETED ) ) { $vals['sha1hidden'] = true; $anyHidden = true; } - if ( $revision->userCan( Revision::DELETED_TEXT, $user ) ) { - if ( $revision->getSha1() != '' ) { + if ( !( $revDel & self::CANNOT_VIEW ) ) { + try { $vals['sha1'] = Wikimedia\base_convert( $revision->getSha1(), 36, 16, 40 ); - } else { + } catch ( RevisionAccessException $e ) { + // Back compat: If there's no sha1, return emtpy string. + // @todo: Gergő says to mention T198099 as a "todo" here. $vals['sha1'] = ''; } } } - if ( $this->fld_contentmodel ) { - $vals['contentmodel'] = $revision->getContentModel(); + if ( $this->fld_roles ) { + $vals['roles'] = $revision->getSlotRoles(); + } + + if ( $this->needSlots ) { + $revDel = $this->checkRevDel( $revision, RevisionRecord::DELETED_TEXT ); + if ( ( $this->fld_slotsha1 || $this->fetchContent ) && ( $revDel & self::IS_DELETED ) ) { + $anyHidden = true; + } + if ( $this->slotRoles === null ) { + try { + $slot = $revision->getSlot( 'main', RevisionRecord::RAW ); + } catch ( RevisionAccessException $e ) { + // Back compat: If there's no slot, there's no content, so set 'textmissing' + // @todo: Gergő says to mention T198099 as a "todo" here. + $vals['textmissing'] = true; + $slot = null; + } + + if ( $slot ) { + $content = null; + $vals += $this->extractSlotInfo( $slot, $revDel, $content ); + if ( !empty( $vals['nosuchsection'] ) ) { + $this->dieWithError( + [ + 'apierror-nosuchsection-what', + wfEscapeWikiText( $this->section ), + $this->msg( 'revid', $revision->getId() ) + ], + 'nosuchsection' + ); + } + if ( $content ) { + $vals += $this->extractDeprecatedContent( $content, $revision ); + } + } + } else { + $roles = array_intersect( $this->slotRoles, $revision->getSlotRoles() ); + $vals['slots'] = [ + ApiResult::META_KVP_MERGE => true, + ]; + foreach ( $roles as $role ) { + try { + $slot = $revision->getSlot( $role, RevisionRecord::RAW ); + } catch ( RevisionAccessException $e ) { + // Don't error out here so the client can still process other slots/revisions. + // @todo: Gergő says to mention T198099 as a "todo" here. + $vals['slots'][$role]['missing'] = true; + continue; + } + $content = null; + $vals['slots'][$role] = $this->extractSlotInfo( $slot, $revDel, $content ); + // @todo Move this into extractSlotInfo() (and remove its $content parameter) + // when extractDeprecatedContent() is no more. + if ( $content ) { + $vals['slots'][$role]['contentmodel'] = $content->getModel(); + $vals['slots'][$role]['contentformat'] = $content->getDefaultFormat(); + ApiResult::setContentValue( $vals['slots'][$role], 'content', $content->serialize() ); + } + } + ApiResult::setArrayType( $vals['slots'], 'kvp', 'role' ); + ApiResult::setIndexedTagName( $vals['slots'], 'slot' ); + } } if ( $this->fld_comment || $this->fld_parsedcomment ) { - if ( $revision->isDeleted( Revision::DELETED_COMMENT ) ) { + $revDel = $this->checkRevDel( $revision, RevisionRecord::DELETED_COMMENT ); + if ( ( $revDel & self::IS_DELETED ) ) { $vals['commenthidden'] = true; $anyHidden = true; } - if ( $revision->userCan( Revision::DELETED_COMMENT, $user ) ) { - $comment = $revision->getComment( Revision::RAW ); + if ( !( $revDel & self::CANNOT_VIEW ) ) { + $comment = $revision->getComment( RevisionRecord::RAW ); + $comment = $comment ? $comment->text : ''; if ( $this->fld_comment ) { $vals['comment'] = $comment; } if ( $this->fld_parsedcomment ) { - $vals['parsedcomment'] = Linker::formatComment( $comment, $title ); + $vals['parsedcomment'] = Linker::formatComment( + $comment, Title::newFromLinkTarget( $revision->getPageAsLinkTarget() ) + ); } } } @@ -256,69 +393,119 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase { } } - $content = null; - global $wgParser; - if ( $this->fetchContent ) { - $content = $revision->getContent( Revision::FOR_THIS_USER, $this->getUser() ); - // Expand templates after getting section content because - // template-added sections don't count and Parser::preprocess() - // will have less input - if ( $content && $this->section !== false ) { - $content = $content->getSection( $this->section, false ); - if ( !$content ) { - $this->dieWithError( - [ - 'apierror-nosuchsection-what', - wfEscapeWikiText( $this->section ), - $this->msg( 'revid', $revision->getId() ) - ], - 'nosuchsection' - ); + if ( $anyHidden && $revision->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) { + $vals['suppressed'] = true; + } + + return $vals; + } + + /** + * Extract information from the SlotRecord + * + * @param SlotRecord $slot + * @param int $revDel Revdel status flags, from self::checkRevDel() + * @param Content|null &$content Set to the slot's content, if available + * and $this->fetchContent is true + * @return array + */ + private function extractSlotInfo( SlotRecord $slot, $revDel, &$content = null ) { + $vals = []; + ApiResult::setArrayType( $vals, 'assoc' ); + + if ( $this->fld_slotsize ) { + $vals['size'] = intval( $slot->getSize() ); + } + + if ( $this->fld_slotsha1 ) { + if ( ( $revDel & self::IS_DELETED ) ) { + $vals['sha1hidden'] = true; + } + if ( !( $revDel & self::CANNOT_VIEW ) ) { + if ( $slot->getSha1() != '' ) { + $vals['sha1'] = Wikimedia\base_convert( $slot->getSha1(), 36, 16, 40 ); + } else { + $vals['sha1'] = ''; } } - if ( $revision->isDeleted( Revision::DELETED_TEXT ) ) { + } + + if ( $this->fld_contentmodel ) { + $vals['contentmodel'] = $slot->getModel(); + } + + $content = null; + if ( $this->fetchContent ) { + if ( ( $revDel & self::IS_DELETED ) ) { $vals['texthidden'] = true; - $anyHidden = true; - } elseif ( !$content ) { - $vals['textmissing'] = true; + } + if ( !( $revDel & self::CANNOT_VIEW ) ) { + try { + $content = $slot->getContent(); + } catch ( RevisionAccessException $e ) { + // @todo: Gergő says to mention T198099 as a "todo" here. + $vals['textmissing'] = true; + } + // Expand templates after getting section content because + // template-added sections don't count and Parser::preprocess() + // will have less input + if ( $content && $this->section !== false ) { + $content = $content->getSection( $this->section, false ); + if ( !$content ) { + $vals['nosuchsection'] = true; + } + } } } + + return $vals; + } + + /** + * Format a Content using deprecated options + * @param Content $content Content to format + * @param RevisionRecord $revision Revision being processed + * @return array + */ + private function extractDeprecatedContent( Content $content, RevisionRecord $revision ) { + global $wgParser; + + $vals = []; + $title = Title::newFromLinkTarget( $revision->getPageAsLinkTarget() ); + if ( $this->fld_parsetree || ( $this->fld_content && $this->generateXML ) ) { - if ( $content ) { - if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) { - $t = $content->getNativeData(); # note: don't set $text + if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) { + $t = $content->getNativeData(); # note: don't set $text - $wgParser->startExternalParse( - $title, - ParserOptions::newFromContext( $this->getContext() ), - Parser::OT_PREPROCESS - ); - $dom = $wgParser->preprocessToDom( $t ); - if ( is_callable( [ $dom, 'saveXML' ] ) ) { - $xml = $dom->saveXML(); - } else { - $xml = $dom->__toString(); - } - $vals['parsetree'] = $xml; + $wgParser->startExternalParse( + $title, + ParserOptions::newFromContext( $this->getContext() ), + Parser::OT_PREPROCESS + ); + $dom = $wgParser->preprocessToDom( $t ); + if ( is_callable( [ $dom, 'saveXML' ] ) ) { + $xml = $dom->saveXML(); } else { - $vals['badcontentformatforparsetree'] = true; - $this->addWarning( - [ - 'apierror-parsetree-notwikitext-title', - wfEscapeWikiText( $title->getPrefixedText() ), - $content->getModel() - ], - 'parsetree-notwikitext' - ); + $xml = $dom->__toString(); } + $vals['parsetree'] = $xml; + } else { + $vals['badcontentformatforparsetree'] = true; + $this->addWarning( + [ + 'apierror-parsetree-notwikitext-title', + wfEscapeWikiText( $title->getPrefixedText() ), + $content->getModel() + ], + 'parsetree-notwikitext' + ); } } - if ( $this->fld_content && $content ) { + if ( $this->fld_content ) { $text = null; if ( $this->expandTemplates && !$this->parseContent ) { - # XXX: implement template expansion for all content types in ContentHandler? if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) { $text = $content->getNativeData(); @@ -376,7 +563,7 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase { $vals['diff'] = []; $context = new DerivativeContext( $this->getContext() ); $context->setTitle( $title ); - $handler = $revision->getContentHandler(); + $handler = $content->getContentHandler(); if ( !is_null( $this->difftotext ) ) { $model = $title->getContentModel(); @@ -398,7 +585,7 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase { if ( $this->difftotextpst ) { $popts = ParserOptions::newFromContext( $this->getContext() ); - $difftocontent = $difftocontent->preSaveTransform( $title, $user, $popts ); + $difftocontent = $difftocontent->preSaveTransform( $title, $this->getUser(), $popts ); } $engine = $handler->createDifferenceEngine( $context ); @@ -421,10 +608,6 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase { } } - if ( $anyHidden && $revision->isDeleted( Revision::DELETED_RESTRICTED ) ) { - $vals['suppressed'] = true; - } - return $vals; } @@ -437,6 +620,12 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase { } public function getAllowedParams() { + $slotRoles = MediaWikiServices::getInstance()->getSlotRoleStore()->getMap(); + if ( !in_array( 'main', $slotRoles, true ) ) { + $slotRoles[] = 'main'; + } + sort( $slotRoles, SORT_STRING ); + return [ 'prop' => [ ApiBase::PARAM_ISMULTI => true, @@ -448,12 +637,15 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase { 'user', 'userid', 'size', + 'slotsize', 'sha1', + 'slotsha1', 'contentmodel', 'comment', 'parsedcomment', 'content', 'tags', + 'roles', 'parsetree', ], ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-prop', @@ -464,15 +656,27 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase { 'user' => 'apihelp-query+revisions+base-paramvalue-prop-user', 'userid' => 'apihelp-query+revisions+base-paramvalue-prop-userid', 'size' => 'apihelp-query+revisions+base-paramvalue-prop-size', + 'slotsize' => 'apihelp-query+revisions+base-paramvalue-prop-slotsize', 'sha1' => 'apihelp-query+revisions+base-paramvalue-prop-sha1', + 'slotsha1' => 'apihelp-query+revisions+base-paramvalue-prop-slotsha1', 'contentmodel' => 'apihelp-query+revisions+base-paramvalue-prop-contentmodel', 'comment' => 'apihelp-query+revisions+base-paramvalue-prop-comment', 'parsedcomment' => 'apihelp-query+revisions+base-paramvalue-prop-parsedcomment', 'content' => 'apihelp-query+revisions+base-paramvalue-prop-content', 'tags' => 'apihelp-query+revisions+base-paramvalue-prop-tags', + 'roles' => 'apihelp-query+revisions+base-paramvalue-prop-roles', 'parsetree' => [ 'apihelp-query+revisions+base-paramvalue-prop-parsetree', CONTENT_MODEL_WIKITEXT ], ], + ApiBase::PARAM_DEPRECATED_VALUES => [ + 'parsetree' => true, + ], + ], + 'slots' => [ + ApiBase::PARAM_TYPE => $slotRoles, + ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-slots', + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_ALL => true, ], 'limit' => [ ApiBase::PARAM_TYPE => 'limit', @@ -515,6 +719,7 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase { 'contentformat' => [ ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(), ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-contentformat', + ApiBase::PARAM_DEPRECATED => true, ], ]; } diff --git a/includes/api/ApiQueryUserContribs.php b/includes/api/ApiQueryUserContribs.php index fdcaa76932..3aa61832fe 100644 --- a/includes/api/ApiQueryUserContribs.php +++ b/includes/api/ApiQueryUserContribs.php @@ -20,6 +20,9 @@ * @file */ +use MediaWiki\MediaWikiServices; +use MediaWiki\Storage\RevisionRecord; + /** * This query action adds a list of a specified user's contributions to the output. * @@ -399,7 +402,8 @@ class ApiQueryUserContribs extends ApiQueryBase { $revIds[] = $data[0]->rev_parent_id; } } - $this->parentLens = Revision::getParentLengths( $dbSecondary, $revIds ); + $this->parentLens = MediaWikiServices::getInstance()->getRevisionStore() + ->listRevisionSizes( $dbSecondary, $revIds ); } foreach ( $merged as $data ) { @@ -438,7 +442,7 @@ class ApiQueryUserContribs extends ApiQueryBase { $this->resetQueryParams(); $db = $this->getDB(); - $revQuery = Revision::getQueryInfo( [ 'page' ] ); + $revQuery = MediaWikiServices::getInstance()->getRevisionStore()->getQueryInfo( [ 'page' ] ); $this->addTables( $revQuery['tables'] ); $this->addJoinConds( $revQuery['joins'] ); $this->addFields( $revQuery['fields'] ); @@ -500,9 +504,9 @@ class ApiQueryUserContribs extends ApiQueryBase { // see the username. $user = $this->getUser(); if ( !$user->isAllowed( 'deletedhistory' ) ) { - $bitmask = Revision::DELETED_USER; + $bitmask = RevisionRecord::DELETED_USER; } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { - $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED; + $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED; } else { $bitmask = 0; } @@ -619,7 +623,7 @@ class ApiQueryUserContribs extends ApiQueryBase { $vals = []; $anyHidden = false; - if ( $row->rev_deleted & Revision::DELETED_TEXT ) { + if ( $row->rev_deleted & RevisionRecord::DELETED_TEXT ) { $vals['texthidden'] = true; $anyHidden = true; } @@ -627,7 +631,7 @@ class ApiQueryUserContribs extends ApiQueryBase { // Any rows where we can't view the user were filtered out in the query. $vals['userid'] = (int)$row->rev_user; $vals['user'] = $row->rev_user_text; - if ( $row->rev_deleted & Revision::DELETED_USER ) { + if ( $row->rev_deleted & RevisionRecord::DELETED_USER ) { $vals['userhidden'] = true; $anyHidden = true; } @@ -658,14 +662,14 @@ class ApiQueryUserContribs extends ApiQueryBase { } if ( $this->fld_comment || $this->fld_parsedcomment ) { - if ( $row->rev_deleted & Revision::DELETED_COMMENT ) { + if ( $row->rev_deleted & RevisionRecord::DELETED_COMMENT ) { $vals['commenthidden'] = true; $anyHidden = true; } - $userCanView = Revision::userCanBitfield( + $userCanView = RevisionRecord::userCanBitfield( $row->rev_deleted, - Revision::DELETED_COMMENT, $this->getUser() + RevisionRecord::DELETED_COMMENT, $this->getUser() ); if ( $userCanView ) { @@ -707,7 +711,7 @@ class ApiQueryUserContribs extends ApiQueryBase { } } - if ( $anyHidden && $row->rev_deleted & Revision::DELETED_RESTRICTED ) { + if ( $anyHidden && ( $row->rev_deleted & RevisionRecord::DELETED_RESTRICTED ) ) { $vals['suppressed'] = true; } diff --git a/includes/api/ApiQueryWatchlist.php b/includes/api/ApiQueryWatchlist.php index bb09838efc..5dd247a3ff 100644 --- a/includes/api/ApiQueryWatchlist.php +++ b/includes/api/ApiQueryWatchlist.php @@ -21,6 +21,7 @@ */ use MediaWiki\MediaWikiServices; +use MediaWiki\Storage\RevisionRecord; /** * This query action allows clients to retrieve a list of recently modified pages @@ -302,13 +303,13 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { /* Add user data and 'anon' flag, if user is anonymous. */ if ( $this->fld_user || $this->fld_userid ) { - if ( $recentChangeInfo['rc_deleted'] & Revision::DELETED_USER ) { + if ( $recentChangeInfo['rc_deleted'] & RevisionRecord::DELETED_USER ) { $vals['userhidden'] = true; $anyHidden = true; } - if ( Revision::userCanBitfield( + if ( RevisionRecord::userCanBitfield( $recentChangeInfo['rc_deleted'], - Revision::DELETED_USER, + RevisionRecord::DELETED_USER, $user ) ) { if ( $this->fld_userid ) { @@ -353,13 +354,13 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { /* Add edit summary / log summary. */ if ( $this->fld_comment || $this->fld_parsedcomment ) { - if ( $recentChangeInfo['rc_deleted'] & Revision::DELETED_COMMENT ) { + if ( $recentChangeInfo['rc_deleted'] & RevisionRecord::DELETED_COMMENT ) { $vals['commenthidden'] = true; $anyHidden = true; } - if ( Revision::userCanBitfield( + if ( RevisionRecord::userCanBitfield( $recentChangeInfo['rc_deleted'], - Revision::DELETED_COMMENT, + RevisionRecord::DELETED_COMMENT, $user ) ) { $comment = $this->commentStore->getComment( 'rc_comment', $recentChangeInfo )->text; @@ -407,7 +408,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { } } - if ( $anyHidden && ( $recentChangeInfo['rc_deleted'] & Revision::DELETED_RESTRICTED ) ) { + if ( $anyHidden && ( $recentChangeInfo['rc_deleted'] & RevisionRecord::DELETED_RESTRICTED ) ) { $vals['suppressed'] = true; } diff --git a/includes/api/ApiRevisionDelete.php b/includes/api/ApiRevisionDelete.php index 9a793e2f99..6121c3d338 100644 --- a/includes/api/ApiRevisionDelete.php +++ b/includes/api/ApiRevisionDelete.php @@ -21,6 +21,8 @@ * @since 1.23 */ +use MediaWiki\Storage\RevisionRecord; + /** * API interface to RevDel. The API equivalent of Special:RevisionDelete. * Requires API write mode to be enabled. @@ -61,8 +63,8 @@ class ApiRevisionDelete extends ApiBase { } $bits = [ 'content' => RevisionDeleter::getRevdelConstant( $params['type'] ), - 'comment' => Revision::DELETED_COMMENT, - 'user' => Revision::DELETED_USER, + 'comment' => RevisionRecord::DELETED_COMMENT, + 'user' => RevisionRecord::DELETED_USER, ]; $bitfield = []; foreach ( $bits as $key => $bit ) { @@ -77,11 +79,11 @@ class ApiRevisionDelete extends ApiBase { if ( $params['suppress'] === 'yes' ) { $this->checkUserRightsAny( 'suppressrevision' ); - $bitfield[Revision::DELETED_RESTRICTED] = 1; + $bitfield[RevisionRecord::DELETED_RESTRICTED] = 1; } elseif ( $params['suppress'] === 'no' ) { - $bitfield[Revision::DELETED_RESTRICTED] = 0; + $bitfield[RevisionRecord::DELETED_RESTRICTED] = 0; } else { - $bitfield[Revision::DELETED_RESTRICTED] = -1; + $bitfield[RevisionRecord::DELETED_RESTRICTED] = -1; } $targetObj = null; diff --git a/includes/api/ApiSetNotificationTimestamp.php b/includes/api/ApiSetNotificationTimestamp.php index f7dc4a78e9..b81c5bf6e0 100644 --- a/includes/api/ApiSetNotificationTimestamp.php +++ b/includes/api/ApiSetNotificationTimestamp.php @@ -22,6 +22,7 @@ * * @file */ + use MediaWiki\MediaWikiServices; /** @@ -73,10 +74,11 @@ class ApiSetNotificationTimestamp extends ApiBase { if ( $params['entirewatchlist'] || $pageSet->getGoodTitleCount() > 1 ) { $this->dieWithError( [ 'apierror-multpages', $this->encodeParamName( 'torevid' ) ] ); } - $title = reset( $pageSet->getGoodTitles() ); + $titles = $pageSet->getGoodTitles(); + $title = reset( $titles ); if ( $title ) { - $timestamp = Revision::getTimestampFromId( - $title, $params['torevid'], Revision::READ_LATEST ); + $timestamp = MediaWikiServices::getInstance()->getRevisionStore() + ->getTimestampFromId( $title, $params['torevid'], IDBAccessObject::READ_LATEST ); if ( $timestamp ) { $timestamp = $dbw->timestamp( $timestamp ); } else { @@ -87,12 +89,14 @@ class ApiSetNotificationTimestamp extends ApiBase { if ( $params['entirewatchlist'] || $pageSet->getGoodTitleCount() > 1 ) { $this->dieWithError( [ 'apierror-multpages', $this->encodeParamName( 'newerthanrevid' ) ] ); } - $title = reset( $pageSet->getGoodTitles() ); + $titles = $pageSet->getGoodTitles(); + $title = reset( $titles ); if ( $title ) { - $revid = $title->getNextRevisionID( - $params['newerthanrevid'], Title::GAID_FOR_UPDATE ); + $revid = $title->getNextRevisionID( $params['newerthanrevid'], Title::GAID_FOR_UPDATE ); if ( $revid ) { - $timestamp = $dbw->timestamp( Revision::getTimestampFromId( $title, $revid ) ); + $timestamp = $dbw->timestamp( + MediaWikiServices::getInstance()->getRevisionStore()->getTimestampFromId( $title, $revid ) + ); } else { $timestamp = null; } diff --git a/includes/api/ApiTag.php b/includes/api/ApiTag.php index c9f6db3994..e0c7a288e5 100644 --- a/includes/api/ApiTag.php +++ b/includes/api/ApiTag.php @@ -19,13 +19,21 @@ * @file */ +use MediaWiki\MediaWikiServices; +use MediaWiki\Storage\RevisionStore; + /** * @ingroup API * @since 1.25 */ class ApiTag extends ApiBase { + /** @var RevisionStore */ + private $revisionStore; + public function execute() { + $this->revisionStore = MediaWikiServices::getInstance()->getRevisionStore(); + $params = $this->extractRequestParams(); $user = $this->getUser(); @@ -84,7 +92,7 @@ class ApiTag extends ApiBase { $valid = RecentChange::newFromId( $id ); break; case 'revid': - $valid = Revision::newFromId( $id ); + $valid = $this->revisionStore->getRevisionById( $id ); break; case 'logid': $valid = self::validateLogId( $id ); diff --git a/includes/api/i18n/en.json b/includes/api/i18n/en.json index d7cdc6cfa7..44b4dfdcaf 100644 --- a/includes/api/i18n/en.json +++ b/includes/api/i18n/en.json @@ -1137,13 +1137,17 @@ "apihelp-query+revisions+base-paramvalue-prop-user": "User that made the revision.", "apihelp-query+revisions+base-paramvalue-prop-userid": "User ID of the revision creator.", "apihelp-query+revisions+base-paramvalue-prop-size": "Length (bytes) of the revision.", + "apihelp-query+revisions+base-paramvalue-prop-slotsize": "Length (bytes) of each revision slot.", "apihelp-query+revisions+base-paramvalue-prop-sha1": "SHA-1 (base 16) of the revision.", - "apihelp-query+revisions+base-paramvalue-prop-contentmodel": "Content model ID of the revision.", + "apihelp-query+revisions+base-paramvalue-prop-slotsha1": "SHA-1 (base 16) of each revision slot.", + "apihelp-query+revisions+base-paramvalue-prop-contentmodel": "Content model ID of each revision slot.", "apihelp-query+revisions+base-paramvalue-prop-comment": "Comment by the user for the revision.", "apihelp-query+revisions+base-paramvalue-prop-parsedcomment": "Parsed comment by the user for the revision.", - "apihelp-query+revisions+base-paramvalue-prop-content": "Text of the revision.", + "apihelp-query+revisions+base-paramvalue-prop-content": "Content of each revision slot.", "apihelp-query+revisions+base-paramvalue-prop-tags": "Tags for the revision.", - "apihelp-query+revisions+base-paramvalue-prop-parsetree": "Deprecated. Use [[Special:ApiHelp/expandtemplates|action=expandtemplates]] or [[Special:ApiHelp/parse|action=parse]] instead. The XML parse tree of revision content (requires content model $1).", + "apihelp-query+revisions+base-paramvalue-prop-roles": "List content slot roles that exist in the revision.", + "apihelp-query+revisions+base-paramvalue-prop-parsetree": "Use [[Special:ApiHelp/expandtemplates|action=expandtemplates]] or [[Special:ApiHelp/parse|action=parse]] instead. The XML parse tree of revision content (requires content model $1).", + "apihelp-query+revisions+base-param-slots": "Which revision slots to return data for, when slot-related properties are included in $1props. If omitted, data from the main slot will be returned in a backwards-compatible format.", "apihelp-query+revisions+base-param-limit": "Limit how many revisions will be returned.", "apihelp-query+revisions+base-param-expandtemplates": "Use [[Special:ApiHelp/expandtemplates|action=expandtemplates]] instead. Expand templates in revision content (requires $1prop=content).", "apihelp-query+revisions+base-param-generatexml": "Use [[Special:ApiHelp/expandtemplates|action=expandtemplates]] or [[Special:ApiHelp/parse|action=parse]] instead. Generate XML parse tree for revision content (requires $1prop=content).", @@ -1864,11 +1868,11 @@ "apiwarn-checktoken-percentencoding": "Check that symbols such as \"+\" in the token are properly percent-encoded in the URL.", "apiwarn-compare-nocontentmodel": "No content model could be determined, assuming $1.", "apiwarn-deprecation-deletedrevs": "list=deletedrevs has been deprecated. Please use prop=deletedrevisions or list=alldeletedrevisions instead.", - "apiwarn-deprecation-expandtemplates-prop": "Because no values have been specified for the prop parameter, a legacy format has been used for the output. This format is deprecated, and in the future, a default value will be set for the prop parameter, causing the new format to always be used.", "apiwarn-deprecation-httpsexpected": "HTTP used when HTTPS was expected.", "apiwarn-deprecation-login-botpw": "Main-account login via action=login is deprecated and may stop working without warning. To continue login with action=login, see [[Special:BotPasswords]]. To safely continue using main-account login, see action=clientlogin.", "apiwarn-deprecation-login-nobotpw": "Main-account login via action=login is deprecated and may stop working without warning. To safely log in, see action=clientlogin.", "apiwarn-deprecation-login-token": "Fetching a token via action=login is deprecated. Use action=query&meta=tokens&type=login instead.", + "apiwarn-deprecation-missingparam": "Because $1 was not specified, a legacy format has been used for the output. This format is deprecated, and in the future the new format will always be used.", "apiwarn-deprecation-parameter": "The parameter $1 has been deprecated.", "apiwarn-deprecation-parse-headitems": "prop=headitems is deprecated since MediaWiki 1.28. Use prop=headhtml when creating new HTML documents, or prop=modules|jsconfigvars when updating a document client-side.", "apiwarn-deprecation-purge-get": "Use of action=purge via GET is deprecated. Use POST instead.", diff --git a/includes/api/i18n/qqq.json b/includes/api/i18n/qqq.json index dd8e529db1..f158f27dd3 100644 --- a/includes/api/i18n/qqq.json +++ b/includes/api/i18n/qqq.json @@ -1061,13 +1061,17 @@ "apihelp-query+revisions+base-paramvalue-prop-user": "{{doc-apihelp-paramvalue|query+revisions+base|prop|user}}", "apihelp-query+revisions+base-paramvalue-prop-userid": "{{doc-apihelp-paramvalue|query+revisions+base|prop|userid}}", "apihelp-query+revisions+base-paramvalue-prop-size": "{{doc-apihelp-paramvalue|query+revisions+base|prop|size}}", + "apihelp-query+revisions+base-paramvalue-prop-slotsize": "{{doc-apihelp-paramvalue|query+revisions+base|prop|slotsize}}", "apihelp-query+revisions+base-paramvalue-prop-sha1": "{{doc-apihelp-paramvalue|query+revisions+base|prop|sha1}}", + "apihelp-query+revisions+base-paramvalue-prop-slotsha1": "{{doc-apihelp-paramvalue|query+revisions+base|prop|slotsha1}}", "apihelp-query+revisions+base-paramvalue-prop-contentmodel": "{{doc-apihelp-paramvalue|query+revisions+base|prop|contentmodel}}", "apihelp-query+revisions+base-paramvalue-prop-comment": "{{doc-apihelp-paramvalue|query+revisions+base|prop|comment}}", "apihelp-query+revisions+base-paramvalue-prop-parsedcomment": "{{doc-apihelp-paramvalue|query+revisions+base|prop|parsedcomment}}", "apihelp-query+revisions+base-paramvalue-prop-content": "{{doc-apihelp-paramvalue|query+revisions+base|prop|content}}", "apihelp-query+revisions+base-paramvalue-prop-tags": "{{doc-apihelp-paramvalue|query+revisions+base|prop|tags}}", + "apihelp-query+revisions+base-paramvalue-prop-roles": "{{doc-apihelp-paramvalue|query+revisions+base|prop|roles}}", "apihelp-query+revisions+base-paramvalue-prop-parsetree": "{{doc-apihelp-paramvalue|query+revisions+base|prop|parsetree|params=* $1 - Value of the constant CONTENT_MODEL_WIKITEXT|paramstart=2}}", + "apihelp-query+revisions+base-param-slots": "{{doc-apihelp-param|query+revisions+base|slots|description=the \"slots\" parameter to revision querying modules|noseealso=1}}", "apihelp-query+revisions+base-param-limit": "{{doc-apihelp-param|query+revisions+base|limit|description=the \"limit\" parameter to revision querying modules|noseealso=1}}", "apihelp-query+revisions+base-param-expandtemplates": "{{doc-apihelp-param|query+revisions+base|expandtemplates|description=the \"expandtemplates\" parameter to revision querying modules|noseealso=1}}", "apihelp-query+revisions+base-param-generatexml": "{{doc-apihelp-param|query+revisions+base|generatexml|description=the \"generatexml\" parameter to revision querying modules|noseealso=1}}", @@ -1751,11 +1755,11 @@ "apiwarn-checktoken-percentencoding": "{{doc-apierror}}", "apiwarn-compare-nocontentmodel": "{{doc-apierror}}\n\nParameters:\n* $1 - Content model being assumed.", "apiwarn-deprecation-deletedrevs": "{{doc-apierror}}", - "apiwarn-deprecation-expandtemplates-prop": "{{doc-apierror}}", "apiwarn-deprecation-httpsexpected": "{{doc-apierror}}", "apiwarn-deprecation-login-botpw": "{{doc-apierror}}", "apiwarn-deprecation-login-nobotpw": "{{doc-apierror}}", "apiwarn-deprecation-login-token": "{{doc-apierror}}", + "apiwarn-deprecation-missingparam": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.", "apiwarn-deprecation-parameter": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.", "apiwarn-deprecation-parse-headitems": "{{doc-apierror}}", "apiwarn-deprecation-purge-get": "{{doc-apierror}}", diff --git a/tests/phpunit/includes/api/query/ApiQueryRevisionsTest.php b/tests/phpunit/includes/api/query/ApiQueryRevisionsTest.php index 38a1d68591..1ed5e95e09 100644 --- a/tests/phpunit/includes/api/query/ApiQueryRevisionsTest.php +++ b/tests/phpunit/includes/api/query/ApiQueryRevisionsTest.php @@ -26,16 +26,19 @@ class ApiQueryRevisionsTest extends ApiTestCase { 'prop' => 'revisions', 'titles' => $pageName, 'rvprop' => 'content', + 'rvslots' => 'main', ] ); $this->assertArrayHasKey( 'query', $apiResult[0] ); $this->assertArrayHasKey( 'pages', $apiResult[0]['query'] ); foreach ( $apiResult[0]['query']['pages'] as $page ) { $this->assertArrayHasKey( 'revisions', $page ); foreach ( $page['revisions'] as $revision ) { - $this->assertArrayHasKey( 'contentformat', $revision, + $this->assertArrayHasKey( 'slots', $revision ); + $this->assertArrayHasKey( 'main', $revision['slots'] ); + $this->assertArrayHasKey( 'contentformat', $revision['slots']['main'], 'contentformat should be included when asking content so client knows how to interpret it' ); - $this->assertArrayHasKey( 'contentmodel', $revision, + $this->assertArrayHasKey( 'contentmodel', $revision['slots']['main'], 'contentmodel should be included when asking content so client knows how to interpret it' ); } -- 2.20.1