Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
66.31% |
124 / 187 |
|
20.00% |
1 / 5 |
CRAP | |
0.00% |
0 / 1 |
ApiQueryAllRevisions | |
66.67% |
124 / 186 |
|
20.00% |
1 / 5 |
107.33 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
1 | |||
run | |
54.78% |
63 / 115 |
|
0.00% |
0 / 1 |
163.57 | |||
getAllowedParams | |
93.48% |
43 / 46 |
|
0.00% |
0 / 1 |
2.00 | |||
getExamplesMessages | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
getHelpUrls | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Copyright © 2015 Wikimedia Foundation and contributors |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18 | * http://www.gnu.org/copyleft/gpl.html |
19 | * |
20 | * @file |
21 | */ |
22 | |
23 | namespace MediaWiki\Api; |
24 | |
25 | use MediaWiki\ChangeTags\ChangeTagsStore; |
26 | use MediaWiki\CommentFormatter\CommentFormatter; |
27 | use MediaWiki\Content\IContentHandlerFactory; |
28 | use MediaWiki\Content\Renderer\ContentRenderer; |
29 | use MediaWiki\Content\Transform\ContentTransformer; |
30 | use MediaWiki\MainConfigNames; |
31 | use MediaWiki\ParamValidator\TypeDef\UserDef; |
32 | use MediaWiki\Parser\ParserFactory; |
33 | use MediaWiki\Revision\RevisionRecord; |
34 | use MediaWiki\Revision\RevisionStore; |
35 | use MediaWiki\Revision\SlotRoleRegistry; |
36 | use MediaWiki\Title\NamespaceInfo; |
37 | use MediaWiki\Title\Title; |
38 | use MediaWiki\User\ActorMigration; |
39 | use MediaWiki\User\TempUser\TempUserCreator; |
40 | use MediaWiki\User\UserFactory; |
41 | use Wikimedia\ParamValidator\ParamValidator; |
42 | |
43 | /** |
44 | * Query module to enumerate all revisions. |
45 | * |
46 | * @ingroup API |
47 | * @since 1.27 |
48 | */ |
49 | class ApiQueryAllRevisions extends ApiQueryRevisionsBase { |
50 | |
51 | private RevisionStore $revisionStore; |
52 | private ActorMigration $actorMigration; |
53 | private NamespaceInfo $namespaceInfo; |
54 | private ChangeTagsStore $changeTagsStore; |
55 | |
56 | public function __construct( |
57 | ApiQuery $query, |
58 | string $moduleName, |
59 | RevisionStore $revisionStore, |
60 | IContentHandlerFactory $contentHandlerFactory, |
61 | ParserFactory $parserFactory, |
62 | SlotRoleRegistry $slotRoleRegistry, |
63 | ActorMigration $actorMigration, |
64 | NamespaceInfo $namespaceInfo, |
65 | ChangeTagsStore $changeTagsStore, |
66 | ContentRenderer $contentRenderer, |
67 | ContentTransformer $contentTransformer, |
68 | CommentFormatter $commentFormatter, |
69 | TempUserCreator $tempUserCreator, |
70 | UserFactory $userFactory |
71 | ) { |
72 | parent::__construct( |
73 | $query, |
74 | $moduleName, |
75 | 'arv', |
76 | $revisionStore, |
77 | $contentHandlerFactory, |
78 | $parserFactory, |
79 | $slotRoleRegistry, |
80 | $contentRenderer, |
81 | $contentTransformer, |
82 | $commentFormatter, |
83 | $tempUserCreator, |
84 | $userFactory |
85 | ); |
86 | $this->revisionStore = $revisionStore; |
87 | $this->actorMigration = $actorMigration; |
88 | $this->namespaceInfo = $namespaceInfo; |
89 | $this->changeTagsStore = $changeTagsStore; |
90 | } |
91 | |
92 | /** |
93 | * @param ApiPageSet|null $resultPageSet |
94 | * @return void |
95 | */ |
96 | protected function run( ?ApiPageSet $resultPageSet = null ) { |
97 | $db = $this->getDB(); |
98 | $params = $this->extractRequestParams( false ); |
99 | |
100 | $result = $this->getResult(); |
101 | |
102 | $this->requireMaxOneParameter( $params, 'user', 'excludeuser' ); |
103 | |
104 | $tsField = 'rev_timestamp'; |
105 | $idField = 'rev_id'; |
106 | $pageField = 'rev_page'; |
107 | |
108 | // Namespace check is likely to be desired, but can't be done |
109 | // efficiently in SQL. |
110 | $miser_ns = null; |
111 | $needPageTable = false; |
112 | if ( $params['namespace'] !== null ) { |
113 | $params['namespace'] = array_unique( $params['namespace'] ); |
114 | sort( $params['namespace'] ); |
115 | if ( $params['namespace'] != $this->namespaceInfo->getValidNamespaces() ) { |
116 | $needPageTable = true; |
117 | if ( $this->getConfig()->get( MainConfigNames::MiserMode ) ) { |
118 | $miser_ns = $params['namespace']; |
119 | } else { |
120 | $this->addWhere( [ 'page_namespace' => $params['namespace'] ] ); |
121 | } |
122 | } |
123 | } |
124 | |
125 | if ( $resultPageSet === null ) { |
126 | $this->parseParameters( $params ); |
127 | $queryBuilder = $this->revisionStore->newSelectQueryBuilder( $db ) |
128 | ->joinComment() |
129 | ->joinPage(); |
130 | $this->getQueryBuilder()->merge( $queryBuilder ); |
131 | } else { |
132 | $this->limit = $this->getParameter( 'limit' ) ?: 10; |
133 | $this->addTables( [ 'revision' ] ); |
134 | $this->addFields( [ 'rev_timestamp', 'rev_id' ] ); |
135 | |
136 | if ( $params['generatetitles'] ) { |
137 | $this->addFields( [ 'rev_page' ] ); |
138 | } |
139 | |
140 | if ( $params['user'] !== null || $params['excludeuser'] !== null ) { |
141 | $this->getQueryBuilder()->join( 'actor', 'actor_rev_user', 'actor_rev_user.actor_id = rev_actor' ); |
142 | } |
143 | |
144 | if ( $needPageTable ) { |
145 | $this->getQueryBuilder()->join( 'page', null, [ "$pageField = page_id" ] ); |
146 | if ( (bool)$miser_ns ) { |
147 | $this->addFields( [ 'page_namespace' ] ); |
148 | } |
149 | } |
150 | } |
151 | |
152 | // Seems to be needed to avoid a planner bug (T113901) |
153 | $this->addOption( 'STRAIGHT_JOIN' ); |
154 | |
155 | $dir = $params['dir']; |
156 | $this->addTimestampWhereRange( $tsField, $dir, $params['start'], $params['end'] ); |
157 | |
158 | if ( $this->fld_tags ) { |
159 | $this->addFields( [ |
160 | 'ts_tags' => $this->changeTagsStore->makeTagSummarySubquery( 'revision' ) |
161 | ] ); |
162 | } |
163 | |
164 | if ( $params['user'] !== null ) { |
165 | $actorQuery = $this->actorMigration->getWhere( $db, 'rev_user', $params['user'] ); |
166 | $this->addWhere( $actorQuery['conds'] ); |
167 | } elseif ( $params['excludeuser'] !== null ) { |
168 | $actorQuery = $this->actorMigration->getWhere( $db, 'rev_user', $params['excludeuser'] ); |
169 | $this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' ); |
170 | } |
171 | |
172 | if ( $params['user'] !== null || $params['excludeuser'] !== null ) { |
173 | // Paranoia: avoid brute force searches (T19342) |
174 | if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) { |
175 | $bitmask = RevisionRecord::DELETED_USER; |
176 | } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { |
177 | $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED; |
178 | } else { |
179 | $bitmask = 0; |
180 | } |
181 | if ( $bitmask ) { |
182 | $this->addWhere( $db->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask" ); |
183 | } |
184 | } |
185 | |
186 | if ( $params['continue'] !== null ) { |
187 | $op = ( $dir == 'newer' ? '>=' : '<=' ); |
188 | $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'timestamp', 'int' ] ); |
189 | $this->addWhere( $db->buildComparison( $op, [ |
190 | $tsField => $db->timestamp( $cont[0] ), |
191 | $idField => $cont[1], |
192 | ] ) ); |
193 | } |
194 | |
195 | $this->addOption( 'LIMIT', $this->limit + 1 ); |
196 | |
197 | $sort = ( $dir == 'newer' ? '' : ' DESC' ); |
198 | $orderby = []; |
199 | // Targeting index rev_timestamp, user_timestamp, usertext_timestamp, or actor_timestamp. |
200 | // But 'user' is always constant for the latter three, so it doesn't matter here. |
201 | $orderby[] = "rev_timestamp $sort"; |
202 | $orderby[] = "rev_id $sort"; |
203 | $this->addOption( 'ORDER BY', $orderby ); |
204 | |
205 | $hookData = []; |
206 | $res = $this->select( __METHOD__, [], $hookData ); |
207 | |
208 | if ( $resultPageSet === null ) { |
209 | $this->executeGenderCacheFromResultWrapper( $res, __METHOD__ ); |
210 | } |
211 | |
212 | $pageMap = []; // Maps rev_page to array index |
213 | $count = 0; |
214 | $nextIndex = 0; |
215 | $generated = []; |
216 | foreach ( $res as $row ) { |
217 | if ( $count === 0 && $resultPageSet !== null ) { |
218 | // Set the non-continue since the list of all revisions is |
219 | // prone to having entries added at the start frequently. |
220 | $this->getContinuationManager()->addGeneratorNonContinueParam( |
221 | $this, 'continue', "$row->rev_timestamp|$row->rev_id" |
222 | ); |
223 | } |
224 | if ( ++$count > $this->limit ) { |
225 | // We've had enough |
226 | $this->setContinueEnumParameter( 'continue', "$row->rev_timestamp|$row->rev_id" ); |
227 | break; |
228 | } |
229 | |
230 | // Miser mode namespace check |
231 | if ( $miser_ns !== null && !in_array( $row->page_namespace, $miser_ns ) ) { |
232 | continue; |
233 | } |
234 | |
235 | if ( $resultPageSet !== null ) { |
236 | if ( $params['generatetitles'] ) { |
237 | $generated[$row->rev_page] = $row->rev_page; |
238 | } else { |
239 | $generated[] = $row->rev_id; |
240 | } |
241 | } else { |
242 | $revision = $this->revisionStore->newRevisionFromRow( $row, 0, Title::newFromRow( $row ) ); |
243 | $rev = $this->extractRevisionInfo( $revision, $row ); |
244 | |
245 | if ( !isset( $pageMap[$row->rev_page] ) ) { |
246 | $index = $nextIndex++; |
247 | $pageMap[$row->rev_page] = $index; |
248 | $title = Title::newFromPageIdentity( $revision->getPage() ); |
249 | $a = [ |
250 | 'pageid' => $title->getArticleID(), |
251 | 'revisions' => [ $rev ], |
252 | ]; |
253 | ApiResult::setIndexedTagName( $a['revisions'], 'rev' ); |
254 | ApiQueryBase::addTitleInfo( $a, $title ); |
255 | $fit = $this->processRow( $row, $a['revisions'][0], $hookData ) && |
256 | $result->addValue( [ 'query', $this->getModuleName() ], $index, $a ); |
257 | } else { |
258 | $index = $pageMap[$row->rev_page]; |
259 | $fit = $this->processRow( $row, $rev, $hookData ) && |
260 | $result->addValue( [ 'query', $this->getModuleName(), $index, 'revisions' ], null, $rev ); |
261 | } |
262 | if ( !$fit ) { |
263 | $this->setContinueEnumParameter( 'continue', "$row->rev_timestamp|$row->rev_id" ); |
264 | break; |
265 | } |
266 | } |
267 | } |
268 | |
269 | if ( $resultPageSet !== null ) { |
270 | if ( $params['generatetitles'] ) { |
271 | $resultPageSet->populateFromPageIDs( $generated ); |
272 | } else { |
273 | $resultPageSet->populateFromRevisionIDs( $generated ); |
274 | } |
275 | } else { |
276 | $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'page' ); |
277 | } |
278 | } |
279 | |
280 | public function getAllowedParams() { |
281 | $ret = parent::getAllowedParams() + [ |
282 | 'user' => [ |
283 | ParamValidator::PARAM_TYPE => 'user', |
284 | UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'id', 'interwiki' ], |
285 | UserDef::PARAM_RETURN_OBJECT => true, |
286 | ], |
287 | 'namespace' => [ |
288 | ParamValidator::PARAM_ISMULTI => true, |
289 | ParamValidator::PARAM_TYPE => 'namespace', |
290 | ParamValidator::PARAM_DEFAULT => null, |
291 | ], |
292 | 'start' => [ |
293 | ParamValidator::PARAM_TYPE => 'timestamp', |
294 | ], |
295 | 'end' => [ |
296 | ParamValidator::PARAM_TYPE => 'timestamp', |
297 | ], |
298 | 'dir' => [ |
299 | ParamValidator::PARAM_TYPE => [ |
300 | 'newer', |
301 | 'older' |
302 | ], |
303 | ParamValidator::PARAM_DEFAULT => 'older', |
304 | ApiBase::PARAM_HELP_MSG => 'api-help-param-direction', |
305 | ApiBase::PARAM_HELP_MSG_PER_VALUE => [ |
306 | 'newer' => 'api-help-paramvalue-direction-newer', |
307 | 'older' => 'api-help-paramvalue-direction-older', |
308 | ], |
309 | ], |
310 | 'excludeuser' => [ |
311 | ParamValidator::PARAM_TYPE => 'user', |
312 | UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'id', 'interwiki' ], |
313 | UserDef::PARAM_RETURN_OBJECT => true, |
314 | ], |
315 | 'continue' => [ |
316 | ApiBase::PARAM_HELP_MSG => 'api-help-param-continue', |
317 | ], |
318 | 'generatetitles' => [ |
319 | ParamValidator::PARAM_DEFAULT => false, |
320 | ], |
321 | ]; |
322 | |
323 | if ( $this->getConfig()->get( MainConfigNames::MiserMode ) ) { |
324 | $ret['namespace'][ApiBase::PARAM_HELP_MSG_APPEND] = [ |
325 | 'api-help-param-limited-in-miser-mode', |
326 | ]; |
327 | } |
328 | |
329 | return $ret; |
330 | } |
331 | |
332 | protected function getExamplesMessages() { |
333 | return [ |
334 | 'action=query&list=allrevisions&arvuser=Example&arvlimit=50' |
335 | => 'apihelp-query+allrevisions-example-user', |
336 | 'action=query&list=allrevisions&arvdir=newer&arvlimit=50' |
337 | => 'apihelp-query+allrevisions-example-ns-any', |
338 | ]; |
339 | } |
340 | |
341 | public function getHelpUrls() { |
342 | return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Allrevisions'; |
343 | } |
344 | } |
345 | |
346 | /** @deprecated class alias since 1.43 */ |
347 | class_alias( ApiQueryAllRevisions::class, 'ApiQueryAllRevisions' ); |