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