MediaWiki master
ApiQueryDeletedRevisions.php
Go to the documentation of this file.
1<?php
12namespace MediaWiki\Api;
13
31
38
39 public function __construct(
40 ApiQuery $query,
41 string $moduleName,
42 private readonly RevisionStore $revisionStore,
43 IContentHandlerFactory $contentHandlerFactory,
44 ParserFactory $parserFactory,
45 SlotRoleRegistry $slotRoleRegistry,
46 private readonly NameTableStore $changeTagDefStore,
47 private readonly ChangeTagsStore $changeTagsStore,
48 private readonly LinkBatchFactory $linkBatchFactory,
49 ContentRenderer $contentRenderer,
50 ContentTransformer $contentTransformer,
51 CommentFormatter $commentFormatter,
52 TempUserCreator $tempUserCreator,
53 UserFactory $userFactory,
54 ) {
55 parent::__construct(
56 $query,
57 $moduleName,
58 'drv',
59 $revisionStore,
60 $contentHandlerFactory,
61 $parserFactory,
62 $slotRoleRegistry,
63 $contentRenderer,
64 $contentTransformer,
65 $commentFormatter,
66 $tempUserCreator,
67 $userFactory
68 );
69 }
70
71 protected function run( ?ApiPageSet $resultPageSet = null ) {
72 $pageSet = $this->getPageSet();
73 $pageMap = $pageSet->getGoodAndMissingTitlesByNamespace();
74 $pageCount = count( $pageSet->getGoodAndMissingPages() );
75 $revCount = $pageSet->getRevisionCount();
76 if ( $revCount === 0 && $pageCount === 0 ) {
77 // Nothing to do
78 return;
79 }
80 if ( $revCount !== 0 && count( $pageSet->getDeletedRevisionIDs() ) === 0 ) {
81 // Nothing to do, revisions were supplied but none are deleted
82 return;
83 }
84
85 $params = $this->extractRequestParams( false );
86
87 $db = $this->getDB();
88
89 $this->requireMaxOneParameter( $params, 'user', 'excludeuser' );
90
91 if ( $resultPageSet === null ) {
92 $this->parseParameters( $params );
93 $arQuery = $this->revisionStore->getArchiveQueryInfo();
94 $this->addTables( $arQuery['tables'] );
95 $this->addFields( $arQuery['fields'] );
96 $this->addJoinConds( $arQuery['joins'] );
97 $this->addFields( [ 'ar_title', 'ar_namespace' ] );
98 } else {
99 $this->limit = $this->getParameter( 'limit' ) ?: 10;
100 $this->addTables( 'archive' );
101 $this->addFields( [ 'ar_title', 'ar_namespace', 'ar_timestamp', 'ar_rev_id', 'ar_id' ] );
102 }
103
104 if ( $this->fld_tags ) {
105 $this->addFields( [
106 'ts_tags' => $this->changeTagsStore->makeTagSummarySubquery( 'archive', $this->getAuthority() )
107 ] );
108 }
109
110 if ( $params['tag'] !== null ) {
111 if ( !$this->changeTagsStore->canViewTag( $params['tag'], $this->getAuthority() ) ) {
112 $this->addWhere( '1=0' );
113 }
114
115 $this->addTables( 'change_tag' );
116 $this->addJoinConds(
117 [ 'change_tag' => [ 'JOIN', [ 'ar_rev_id=ct_rev_id' ] ] ]
118 );
119 try {
120 $this->addWhereFld( 'ct_tag_id', $this->changeTagDefStore->getId( $params['tag'] ) );
121 } catch ( NameTableAccessException ) {
122 // Return nothing.
123 $this->addWhere( '1=0' );
124 }
125 }
126
127 // This means stricter restrictions
128 if ( ( $this->fld_comment || $this->fld_parsedcomment ) &&
129 !$this->getAuthority()->isAllowed( 'deletedhistory' )
130 ) {
131 $this->dieWithError( 'apierror-cantview-deleted-comment', 'permissiondenied' );
132 }
133 if ( $this->fetchContent && !$this->getAuthority()->isAllowedAny( 'deletedtext', 'undelete' ) ) {
134 $this->dieWithError( 'apierror-cantview-deleted-revision-content', 'permissiondenied' );
135 }
136
137 $dir = $params['dir'];
138
139 if ( $revCount !== 0 ) {
140 $this->addWhere( [
141 'ar_rev_id' => array_keys( $pageSet->getDeletedRevisionIDs() )
142 ] );
143 } else {
144 // We need a custom WHERE clause that matches all titles.
145 $lb = $this->linkBatchFactory->newLinkBatch( $pageSet->getGoodAndMissingPages() );
146 $where = $lb->constructSet( 'ar', $db );
147 $this->addWhere( $where );
148 }
149
150 if ( $params['user'] !== null || $params['excludeuser'] !== null ) {
151 // In the non-generator case, the actor join will already be present.
152 if ( $resultPageSet !== null ) {
153 $this->addTables( 'actor' );
154 $this->addJoinConds( [ 'actor' => [ 'JOIN', 'actor_id=ar_actor' ] ] );
155 }
156 if ( $params['user'] !== null ) {
157 $this->addWhereFld( 'actor_name', $params['user'] );
158 } elseif ( $params['excludeuser'] !== null ) {
159 $this->addWhere( $db->expr( 'actor_name', '!=', $params['excludeuser'] ) );
160 }
161 }
162
163 if ( $params['user'] !== null || $params['excludeuser'] !== null ) {
164 // Paranoia: avoid brute force searches (T19342)
165 if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
166 $bitmask = RevisionRecord::DELETED_USER;
167 } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
168 $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
169 } else {
170 $bitmask = 0;
171 }
172 if ( $bitmask ) {
173 $this->addWhere( $db->bitAnd( 'ar_deleted', $bitmask ) . " != $bitmask" );
174 }
175 }
176
177 if ( $params['continue'] !== null ) {
178 $op = ( $dir == 'newer' ? '>=' : '<=' );
179 if ( $revCount !== 0 ) {
180 $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'int', 'int' ] );
181 $this->addWhere( $db->buildComparison( $op, [
182 'ar_rev_id' => $cont[0],
183 'ar_id' => $cont[1],
184 ] ) );
185 } else {
186 $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'int', 'string', 'timestamp', 'int' ] );
187 $this->addWhere( $db->buildComparison( $op, [
188 'ar_namespace' => $cont[0],
189 'ar_title' => $cont[1],
190 'ar_timestamp' => $db->timestamp( $cont[2] ),
191 'ar_id' => $cont[3],
192 ] ) );
193 }
194 }
195
196 $this->addOption( 'LIMIT', $this->limit + 1 );
197
198 if ( $revCount !== 0 ) {
199 // Sort by ar_rev_id when querying by ar_rev_id
200 $this->addWhereRange( 'ar_rev_id', $dir, null, null );
201 } else {
202 // Sort by ns and title in the same order as timestamp for efficiency
203 // But only when not already unique in the query
204 if ( count( $pageMap ) > 1 ) {
205 $this->addWhereRange( 'ar_namespace', $dir, null, null );
206 }
207 $oneTitle = key( reset( $pageMap ) );
208 foreach ( $pageMap as $pages ) {
209 if ( count( $pages ) > 1 || key( $pages ) !== $oneTitle ) {
210 $this->addWhereRange( 'ar_title', $dir, null, null );
211 break;
212 }
213 }
214 $this->addTimestampWhereRange( 'ar_timestamp', $dir, $params['start'], $params['end'] );
215 }
216 // Include in ORDER BY for uniqueness
217 $this->addWhereRange( 'ar_id', $dir, null, null );
218
219 $res = $this->select( __METHOD__ );
220 $count = 0;
221 $generated = [];
222 foreach ( $res as $row ) {
223 if ( ++$count > $this->limit ) {
224 // We've had enough
225 $this->setContinueEnumParameter( 'continue',
226 $revCount
227 ? "$row->ar_rev_id|$row->ar_id"
228 : "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id"
229 );
230 break;
231 }
232
233 if ( $resultPageSet !== null ) {
234 $generated[] = $row->ar_rev_id;
235 } else {
236 if ( !isset( $pageMap[$row->ar_namespace][$row->ar_title] ) ) {
237 // Was it converted?
238 $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
239 $converted = $pageSet->getConvertedTitles();
240 if ( $title && isset( $converted[$title->getPrefixedText()] ) ) {
241 $title = Title::newFromText( $converted[$title->getPrefixedText()] );
242 if ( $title && isset( $pageMap[$title->getNamespace()][$title->getDBkey()] ) ) {
243 $pageMap[$row->ar_namespace][$row->ar_title] =
244 $pageMap[$title->getNamespace()][$title->getDBkey()];
245 }
246 }
247 }
248 if ( !isset( $pageMap[$row->ar_namespace][$row->ar_title] ) ) {
250 __METHOD__,
251 "Found row in archive (ar_id={$row->ar_id}) that didn't get processed by ApiPageSet"
252 );
253 }
254
255 $fit = $this->addPageSubItem(
256 $pageMap[$row->ar_namespace][$row->ar_title],
257 $this->extractRevisionInfo( $this->revisionStore->newRevisionFromArchiveRow( $row ), $row ),
258 'rev'
259 );
260 if ( !$fit ) {
261 $this->setContinueEnumParameter( 'continue',
262 $revCount
263 ? "$row->ar_rev_id|$row->ar_id"
264 : "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id"
265 );
266 break;
267 }
268 }
269 }
270
271 if ( $resultPageSet !== null ) {
272 $resultPageSet->populateFromRevisionIDs( $generated );
273 }
274 }
275
277 public function getAllowedParams() {
278 return parent::getAllowedParams() + [
279 'start' => [
280 ParamValidator::PARAM_TYPE => 'timestamp',
281 ],
282 'end' => [
283 ParamValidator::PARAM_TYPE => 'timestamp',
284 ],
285 'dir' => [
286 ParamValidator::PARAM_TYPE => [
287 'newer',
288 'older'
289 ],
290 ParamValidator::PARAM_DEFAULT => 'older',
291 ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
293 'newer' => 'api-help-paramvalue-direction-newer',
294 'older' => 'api-help-paramvalue-direction-older',
295 ],
296 ],
297 'tag' => null,
298 'user' => [
299 ParamValidator::PARAM_TYPE => 'user',
300 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'id', 'interwiki' ],
301 ],
302 'excludeuser' => [
303 ParamValidator::PARAM_TYPE => 'user',
304 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'id', 'interwiki' ],
305 ],
306 'continue' => [
307 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
308 ],
309 ];
310 }
311
313 protected function getExamplesMessages() {
314 $title = Title::newMainPage();
315 $talkTitle = $title->getTalkPageIfDefined();
316 $examples = [
317 'action=query&prop=deletedrevisions&revids=123456'
318 => 'apihelp-query+deletedrevisions-example-revids',
319 ];
320
321 if ( $talkTitle ) {
322 $title = rawurlencode( $title->getPrefixedText() );
323 $talkTitle = rawurlencode( $talkTitle->getPrefixedText() );
324 $examples["action=query&prop=deletedrevisions&titles={$title}|{$talkTitle}&" .
325 'drvslots=*&drvprop=user|comment|content'] = 'apihelp-query+deletedrevisions-example-titles';
326 }
327
328 return $examples;
329 }
330
332 public function getHelpUrls() {
333 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Deletedrevisions';
334 }
335}
336
338class_alias( ApiQueryDeletedRevisions::class, 'ApiQueryDeletedRevisions' );
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1522
parseContinueParamOrDie(string $continue, array $types)
Parse the 'continue' parameter in the usual format and validate the types of each part,...
Definition ApiBase.php:1707
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, or 'string' with PARAM_ISMULTI,...
Definition ApiBase.php:206
requireMaxOneParameter( $params,... $required)
Dies if more than one parameter from a certain set of parameters are set and not false.
Definition ApiBase.php:1012
static dieDebug( $method, $message)
Internal code errors should be reported with this method.
Definition ApiBase.php:1759
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition ApiBase.php:166
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:837
getParameter( $paramName, $parseLimit=true)
Get a value for the given parameter.
Definition ApiBase.php:958
This class contains a list of pages that the client has requested.
addOption( $name, $value=null)
Add an option such as LIMIT or USE INDEX.
addPageSubItem( $pageId, $item, $elemname=null)
Same as addPageSubItems(), but one element of $data at a time.
select( $method, $extraQuery=[], ?array &$hookData=null)
Execute a SELECT query based on the values in the internal arrays.
addTimestampWhereRange( $field, $dir, $start, $end, $sort=true)
Add a WHERE clause corresponding to a range, similar to addWhereRange, but converts $start and $end t...
addWhereFld( $field, $value)
Equivalent to addWhere( [ $field => $value ] )
addWhereRange( $field, $dir, $start, $end, $sort=true)
Add a WHERE clause corresponding to a range, and an ORDER BY clause to sort in the right direction.
Query module to enumerate deleted revisions for pages.
getExamplesMessages()
Returns usage examples for this module.Return value has query strings as keys, with values being eith...
__construct(ApiQuery $query, string $moduleName, private readonly RevisionStore $revisionStore, IContentHandlerFactory $contentHandlerFactory, ParserFactory $parserFactory, SlotRoleRegistry $slotRoleRegistry, private readonly NameTableStore $changeTagDefStore, private readonly ChangeTagsStore $changeTagsStore, private readonly LinkBatchFactory $linkBatchFactory, ContentRenderer $contentRenderer, ContentTransformer $contentTransformer, CommentFormatter $commentFormatter, TempUserCreator $tempUserCreator, UserFactory $userFactory,)
getHelpUrls()
Return links to more detailed help pages about the module.1.25, returning boolean false is deprecated...
setContinueEnumParameter( $paramName, $paramValue)
Overridden to set the generator param if in generator mode.
getPageSet()
Get the PageSet object to work on.
A base class for functions common to producing a list of revisions.
parseParameters( $params)
Parse the parameters into the various instance fields.
This is the main query class.
Definition ApiQuery.php:36
Read-write access to the change_tags table.
This is the main service interface for converting single-line comments from various DB comment fields...
makeTitle( $linkId)
Convert a link ID to a Title.to override Title
Factory for LinkBatch objects to batch query page metadata.
Type definition for user types.
Definition UserDef.php:27
Page revision base class.
Service for looking up page revisions.
A registry service for SlotRoleHandlers, used to define which slot roles are available on which page.
Exception representing a failure to look up a row from a name table.
Represents a title within MediaWiki.
Definition Title.php:69
Service for temporary user creation.
Create User objects.
Service for formatting and validating API parameters.
addTables( $tables, $alias=null)
addWhere( $conds)
addJoinConds( $conds)
addFields( $fields)