Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 189 |
|
0.00% |
0 / 5 |
CRAP | |
0.00% |
0 / 1 |
ApiQueryDeletedRevisions | |
0.00% |
0 / 189 |
|
0.00% |
0 / 5 |
2756 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
2 | |||
run | |
0.00% |
0 / 127 |
|
0.00% |
0 / 1 |
2256 | |||
getAllowedParams | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
2 | |||
getExamplesMessages | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 | |||
getHelpUrls | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Copyright © 2014 Wikimedia Foundation and contributors |
4 | * |
5 | * Heavily based on ApiQueryDeletedrevs, |
6 | * Copyright © 2007 Roan Kattouw <roan.kattouw@gmail.com> |
7 | * |
8 | * This program is free software; you can redistribute it and/or modify |
9 | * it under the terms of the GNU General Public License as published by |
10 | * the Free Software Foundation; either version 2 of the License, or |
11 | * (at your option) any later version. |
12 | * |
13 | * This program is distributed in the hope that it will be useful, |
14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
16 | * GNU General Public License for more details. |
17 | * |
18 | * You should have received a copy of the GNU General Public License along |
19 | * with this program; if not, write to the Free Software Foundation, Inc., |
20 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
21 | * http://www.gnu.org/copyleft/gpl.html |
22 | * |
23 | * @file |
24 | */ |
25 | |
26 | use MediaWiki\Cache\LinkBatchFactory; |
27 | use MediaWiki\CommentFormatter\CommentFormatter; |
28 | use MediaWiki\Content\IContentHandlerFactory; |
29 | use MediaWiki\Content\Renderer\ContentRenderer; |
30 | use MediaWiki\Content\Transform\ContentTransformer; |
31 | use MediaWiki\ParamValidator\TypeDef\UserDef; |
32 | use MediaWiki\Revision\RevisionRecord; |
33 | use MediaWiki\Revision\RevisionStore; |
34 | use MediaWiki\Revision\SlotRoleRegistry; |
35 | use MediaWiki\Storage\NameTableAccessException; |
36 | use MediaWiki\Storage\NameTableStore; |
37 | use MediaWiki\Title\Title; |
38 | use MediaWiki\User\TempUser\TempUserCreator; |
39 | use MediaWiki\User\UserFactory; |
40 | use Wikimedia\ParamValidator\ParamValidator; |
41 | |
42 | /** |
43 | * Query module to enumerate deleted revisions for pages. |
44 | * |
45 | * @ingroup API |
46 | */ |
47 | class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase { |
48 | |
49 | private RevisionStore $revisionStore; |
50 | private NameTableStore $changeTagDefStore; |
51 | private LinkBatchFactory $linkBatchFactory; |
52 | |
53 | /** |
54 | * @param ApiQuery $query |
55 | * @param string $moduleName |
56 | * @param RevisionStore $revisionStore |
57 | * @param IContentHandlerFactory $contentHandlerFactory |
58 | * @param ParserFactory $parserFactory |
59 | * @param SlotRoleRegistry $slotRoleRegistry |
60 | * @param NameTableStore $changeTagDefStore |
61 | * @param LinkBatchFactory $linkBatchFactory |
62 | * @param ContentRenderer $contentRenderer |
63 | * @param ContentTransformer $contentTransformer |
64 | * @param CommentFormatter $commentFormatter |
65 | * @param TempUserCreator $tempUserCreator |
66 | * @param UserFactory $userFactory |
67 | */ |
68 | public function __construct( |
69 | ApiQuery $query, |
70 | $moduleName, |
71 | RevisionStore $revisionStore, |
72 | IContentHandlerFactory $contentHandlerFactory, |
73 | ParserFactory $parserFactory, |
74 | SlotRoleRegistry $slotRoleRegistry, |
75 | NameTableStore $changeTagDefStore, |
76 | LinkBatchFactory $linkBatchFactory, |
77 | ContentRenderer $contentRenderer, |
78 | ContentTransformer $contentTransformer, |
79 | CommentFormatter $commentFormatter, |
80 | TempUserCreator $tempUserCreator, |
81 | UserFactory $userFactory |
82 | ) { |
83 | parent::__construct( |
84 | $query, |
85 | $moduleName, |
86 | 'drv', |
87 | $revisionStore, |
88 | $contentHandlerFactory, |
89 | $parserFactory, |
90 | $slotRoleRegistry, |
91 | $contentRenderer, |
92 | $contentTransformer, |
93 | $commentFormatter, |
94 | $tempUserCreator, |
95 | $userFactory |
96 | ); |
97 | $this->revisionStore = $revisionStore; |
98 | $this->changeTagDefStore = $changeTagDefStore; |
99 | $this->linkBatchFactory = $linkBatchFactory; |
100 | } |
101 | |
102 | protected function run( ApiPageSet $resultPageSet = null ) { |
103 | $pageSet = $this->getPageSet(); |
104 | $pageMap = $pageSet->getGoodAndMissingTitlesByNamespace(); |
105 | $pageCount = count( $pageSet->getGoodAndMissingPages() ); |
106 | $revCount = $pageSet->getRevisionCount(); |
107 | if ( $revCount === 0 && $pageCount === 0 ) { |
108 | // Nothing to do |
109 | return; |
110 | } |
111 | if ( $revCount !== 0 && count( $pageSet->getDeletedRevisionIDs() ) === 0 ) { |
112 | // Nothing to do, revisions were supplied but none are deleted |
113 | return; |
114 | } |
115 | |
116 | $params = $this->extractRequestParams( false ); |
117 | |
118 | $db = $this->getDB(); |
119 | |
120 | $this->requireMaxOneParameter( $params, 'user', 'excludeuser' ); |
121 | |
122 | if ( $resultPageSet === null ) { |
123 | $this->parseParameters( $params ); |
124 | $arQuery = $this->revisionStore->getArchiveQueryInfo(); |
125 | $this->addTables( $arQuery['tables'] ); |
126 | $this->addFields( $arQuery['fields'] ); |
127 | $this->addJoinConds( $arQuery['joins'] ); |
128 | $this->addFields( [ 'ar_title', 'ar_namespace' ] ); |
129 | } else { |
130 | $this->limit = $this->getParameter( 'limit' ) ?: 10; |
131 | $this->addTables( 'archive' ); |
132 | $this->addFields( [ 'ar_title', 'ar_namespace', 'ar_timestamp', 'ar_rev_id', 'ar_id' ] ); |
133 | } |
134 | |
135 | if ( $this->fld_tags ) { |
136 | $this->addFields( [ 'ts_tags' => ChangeTags::makeTagSummarySubquery( 'archive' ) ] ); |
137 | } |
138 | |
139 | if ( $params['tag'] !== null ) { |
140 | $this->addTables( 'change_tag' ); |
141 | $this->addJoinConds( |
142 | [ 'change_tag' => [ 'JOIN', [ 'ar_rev_id=ct_rev_id' ] ] ] |
143 | ); |
144 | try { |
145 | $this->addWhereFld( 'ct_tag_id', $this->changeTagDefStore->getId( $params['tag'] ) ); |
146 | } catch ( NameTableAccessException $exception ) { |
147 | // Return nothing. |
148 | $this->addWhere( '1=0' ); |
149 | } |
150 | } |
151 | |
152 | // This means stricter restrictions |
153 | if ( ( $this->fld_comment || $this->fld_parsedcomment ) && |
154 | !$this->getAuthority()->isAllowed( 'deletedhistory' ) |
155 | ) { |
156 | $this->dieWithError( 'apierror-cantview-deleted-comment', 'permissiondenied' ); |
157 | } |
158 | if ( $this->fetchContent && !$this->getAuthority()->isAllowedAny( 'deletedtext', 'undelete' ) ) { |
159 | $this->dieWithError( 'apierror-cantview-deleted-revision-content', 'permissiondenied' ); |
160 | } |
161 | |
162 | $dir = $params['dir']; |
163 | |
164 | if ( $revCount !== 0 ) { |
165 | $this->addWhere( [ |
166 | 'ar_rev_id' => array_keys( $pageSet->getDeletedRevisionIDs() ) |
167 | ] ); |
168 | } else { |
169 | // We need a custom WHERE clause that matches all titles. |
170 | $lb = $this->linkBatchFactory->newLinkBatch( $pageSet->getGoodAndMissingPages() ); |
171 | $where = $lb->constructSet( 'ar', $db ); |
172 | $this->addWhere( $where ); |
173 | } |
174 | |
175 | if ( $params['user'] !== null || $params['excludeuser'] !== null ) { |
176 | // In the non-generator case, the actor join will already be present. |
177 | if ( $resultPageSet !== null ) { |
178 | $this->addTables( 'actor' ); |
179 | $this->addJoinConds( [ 'actor' => [ 'JOIN', 'actor_id=ar_actor' ] ] ); |
180 | } |
181 | if ( $params['user'] !== null ) { |
182 | $this->addWhereFld( 'actor_name', $params['user'] ); |
183 | } elseif ( $params['excludeuser'] !== null ) { |
184 | $this->addWhere( $db->expr( 'actor_name', '!=', $params['excludeuser'] ) ); |
185 | } |
186 | } |
187 | |
188 | if ( $params['user'] !== null || $params['excludeuser'] !== null ) { |
189 | // Paranoia: avoid brute force searches (T19342) |
190 | if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) { |
191 | $bitmask = RevisionRecord::DELETED_USER; |
192 | } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { |
193 | $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED; |
194 | } else { |
195 | $bitmask = 0; |
196 | } |
197 | if ( $bitmask ) { |
198 | $this->addWhere( $db->bitAnd( 'ar_deleted', $bitmask ) . " != $bitmask" ); |
199 | } |
200 | } |
201 | |
202 | if ( $params['continue'] !== null ) { |
203 | $op = ( $dir == 'newer' ? '>=' : '<=' ); |
204 | if ( $revCount !== 0 ) { |
205 | $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'int', 'int' ] ); |
206 | $this->addWhere( $db->buildComparison( $op, [ |
207 | 'ar_rev_id' => $cont[0], |
208 | 'ar_id' => $cont[1], |
209 | ] ) ); |
210 | } else { |
211 | $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'int', 'string', 'timestamp', 'int' ] ); |
212 | $this->addWhere( $db->buildComparison( $op, [ |
213 | 'ar_namespace' => $cont[0], |
214 | 'ar_title' => $cont[1], |
215 | 'ar_timestamp' => $db->timestamp( $cont[2] ), |
216 | 'ar_id' => $cont[3], |
217 | ] ) ); |
218 | } |
219 | } |
220 | |
221 | $this->addOption( 'LIMIT', $this->limit + 1 ); |
222 | |
223 | if ( $revCount !== 0 ) { |
224 | // Sort by ar_rev_id when querying by ar_rev_id |
225 | $this->addWhereRange( 'ar_rev_id', $dir, null, null ); |
226 | } else { |
227 | // Sort by ns and title in the same order as timestamp for efficiency |
228 | // But only when not already unique in the query |
229 | if ( count( $pageMap ) > 1 ) { |
230 | $this->addWhereRange( 'ar_namespace', $dir, null, null ); |
231 | } |
232 | $oneTitle = key( reset( $pageMap ) ); |
233 | foreach ( $pageMap as $pages ) { |
234 | if ( count( $pages ) > 1 || key( $pages ) !== $oneTitle ) { |
235 | $this->addWhereRange( 'ar_title', $dir, null, null ); |
236 | break; |
237 | } |
238 | } |
239 | $this->addTimestampWhereRange( 'ar_timestamp', $dir, $params['start'], $params['end'] ); |
240 | } |
241 | // Include in ORDER BY for uniqueness |
242 | $this->addWhereRange( 'ar_id', $dir, null, null ); |
243 | |
244 | $res = $this->select( __METHOD__ ); |
245 | $count = 0; |
246 | $generated = []; |
247 | foreach ( $res as $row ) { |
248 | if ( ++$count > $this->limit ) { |
249 | // We've had enough |
250 | $this->setContinueEnumParameter( 'continue', |
251 | $revCount |
252 | ? "$row->ar_rev_id|$row->ar_id" |
253 | : "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id" |
254 | ); |
255 | break; |
256 | } |
257 | |
258 | if ( $resultPageSet !== null ) { |
259 | $generated[] = $row->ar_rev_id; |
260 | } else { |
261 | if ( !isset( $pageMap[$row->ar_namespace][$row->ar_title] ) ) { |
262 | // Was it converted? |
263 | $title = Title::makeTitle( $row->ar_namespace, $row->ar_title ); |
264 | $converted = $pageSet->getConvertedTitles(); |
265 | if ( $title && isset( $converted[$title->getPrefixedText()] ) ) { |
266 | $title = Title::newFromText( $converted[$title->getPrefixedText()] ); |
267 | if ( $title && isset( $pageMap[$title->getNamespace()][$title->getDBkey()] ) ) { |
268 | $pageMap[$row->ar_namespace][$row->ar_title] = |
269 | $pageMap[$title->getNamespace()][$title->getDBkey()]; |
270 | } |
271 | } |
272 | } |
273 | if ( !isset( $pageMap[$row->ar_namespace][$row->ar_title] ) ) { |
274 | ApiBase::dieDebug( |
275 | __METHOD__, |
276 | "Found row in archive (ar_id={$row->ar_id}) that didn't get processed by ApiPageSet" |
277 | ); |
278 | } |
279 | |
280 | $fit = $this->addPageSubItem( |
281 | $pageMap[$row->ar_namespace][$row->ar_title], |
282 | $this->extractRevisionInfo( $this->revisionStore->newRevisionFromArchiveRow( $row ), $row ), |
283 | 'rev' |
284 | ); |
285 | if ( !$fit ) { |
286 | $this->setContinueEnumParameter( 'continue', |
287 | $revCount |
288 | ? "$row->ar_rev_id|$row->ar_id" |
289 | : "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id" |
290 | ); |
291 | break; |
292 | } |
293 | } |
294 | } |
295 | |
296 | if ( $resultPageSet !== null ) { |
297 | $resultPageSet->populateFromRevisionIDs( $generated ); |
298 | } |
299 | } |
300 | |
301 | public function getAllowedParams() { |
302 | return parent::getAllowedParams() + [ |
303 | 'start' => [ |
304 | ParamValidator::PARAM_TYPE => 'timestamp', |
305 | ], |
306 | 'end' => [ |
307 | ParamValidator::PARAM_TYPE => 'timestamp', |
308 | ], |
309 | 'dir' => [ |
310 | ParamValidator::PARAM_TYPE => [ |
311 | 'newer', |
312 | 'older' |
313 | ], |
314 | ParamValidator::PARAM_DEFAULT => 'older', |
315 | ApiBase::PARAM_HELP_MSG => 'api-help-param-direction', |
316 | ApiBase::PARAM_HELP_MSG_PER_VALUE => [ |
317 | 'newer' => 'api-help-paramvalue-direction-newer', |
318 | 'older' => 'api-help-paramvalue-direction-older', |
319 | ], |
320 | ], |
321 | 'tag' => null, |
322 | 'user' => [ |
323 | ParamValidator::PARAM_TYPE => 'user', |
324 | UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'id', 'interwiki' ], |
325 | ], |
326 | 'excludeuser' => [ |
327 | ParamValidator::PARAM_TYPE => 'user', |
328 | UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'id', 'interwiki' ], |
329 | ], |
330 | 'continue' => [ |
331 | ApiBase::PARAM_HELP_MSG => 'api-help-param-continue', |
332 | ], |
333 | ]; |
334 | } |
335 | |
336 | protected function getExamplesMessages() { |
337 | $title = Title::newMainPage(); |
338 | $talkTitle = $title->getTalkPageIfDefined(); |
339 | $examples = [ |
340 | 'action=query&prop=deletedrevisions&revids=123456' |
341 | => 'apihelp-query+deletedrevisions-example-revids', |
342 | ]; |
343 | |
344 | if ( $talkTitle ) { |
345 | $title = rawurlencode( $title->getPrefixedText() ); |
346 | $talkTitle = rawurlencode( $talkTitle->getPrefixedText() ); |
347 | $examples["action=query&prop=deletedrevisions&titles={$title}|{$talkTitle}&" . |
348 | 'drvslots=*&drvprop=user|comment|content'] = 'apihelp-query+deletedrevisions-example-titles'; |
349 | } |
350 | |
351 | return $examples; |
352 | } |
353 | |
354 | public function getHelpUrls() { |
355 | return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Deletedrevisions'; |
356 | } |
357 | } |