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