Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
75.38% |
349 / 463 |
|
54.55% |
6 / 11 |
CRAP | |
0.00% |
0 / 1 |
ApiQueryRecentChanges | |
75.38% |
349 / 463 |
|
54.55% |
6 / 11 |
480.39 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
initProperties | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
executeGenerator | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
run | |
67.09% |
159 / 237 |
|
0.00% |
0 / 1 |
364.06 | |||
extractRowInfo | |
68.97% |
60 / 87 |
|
0.00% |
0 / 1 |
98.27 | |||
includesPatrollingFlags | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
5 | |||
getCacheMode | |
75.00% |
6 / 8 |
|
0.00% |
0 / 1 |
6.56 | |||
getAllowedParams | |
100.00% |
96 / 96 |
|
100.00% |
1 / 1 |
1 | |||
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 © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" |
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\RowCommentFormatter; |
24 | use MediaWiki\CommentStore\CommentStore; |
25 | use MediaWiki\MainConfigNames; |
26 | use MediaWiki\ParamValidator\TypeDef\NamespaceDef; |
27 | use MediaWiki\ParamValidator\TypeDef\UserDef; |
28 | use MediaWiki\Revision\RevisionRecord; |
29 | use MediaWiki\Revision\SlotRoleRegistry; |
30 | use MediaWiki\Storage\NameTableAccessException; |
31 | use MediaWiki\Storage\NameTableStore; |
32 | use MediaWiki\Title\Title; |
33 | use MediaWiki\User\UserNameUtils; |
34 | use Wikimedia\ParamValidator\ParamValidator; |
35 | use Wikimedia\ParamValidator\TypeDef\IntegerDef; |
36 | |
37 | /** |
38 | * A query action to enumerate the recent changes that were done to the wiki. |
39 | * Various filters are supported. |
40 | * |
41 | * @ingroup API |
42 | */ |
43 | class ApiQueryRecentChanges extends ApiQueryGeneratorBase { |
44 | |
45 | private CommentStore $commentStore; |
46 | private RowCommentFormatter $commentFormatter; |
47 | private NameTableStore $changeTagDefStore; |
48 | private NameTableStore $slotRoleStore; |
49 | private SlotRoleRegistry $slotRoleRegistry; |
50 | private UserNameUtils $userNameUtils; |
51 | |
52 | private $formattedComments = []; |
53 | |
54 | /** |
55 | * @param ApiQuery $query |
56 | * @param string $moduleName |
57 | * @param CommentStore $commentStore |
58 | * @param RowCommentFormatter $commentFormatter |
59 | * @param NameTableStore $changeTagDefStore |
60 | * @param NameTableStore $slotRoleStore |
61 | * @param SlotRoleRegistry $slotRoleRegistry |
62 | * @param UserNameUtils $userNameUtils |
63 | */ |
64 | public function __construct( |
65 | ApiQuery $query, |
66 | $moduleName, |
67 | CommentStore $commentStore, |
68 | RowCommentFormatter $commentFormatter, |
69 | NameTableStore $changeTagDefStore, |
70 | NameTableStore $slotRoleStore, |
71 | SlotRoleRegistry $slotRoleRegistry, |
72 | UserNameUtils $userNameUtils |
73 | ) { |
74 | parent::__construct( $query, $moduleName, 'rc' ); |
75 | $this->commentStore = $commentStore; |
76 | $this->commentFormatter = $commentFormatter; |
77 | $this->changeTagDefStore = $changeTagDefStore; |
78 | $this->slotRoleStore = $slotRoleStore; |
79 | $this->slotRoleRegistry = $slotRoleRegistry; |
80 | $this->userNameUtils = $userNameUtils; |
81 | } |
82 | |
83 | private $fld_comment = false, $fld_parsedcomment = false, $fld_user = false, $fld_userid = false, |
84 | $fld_flags = false, $fld_timestamp = false, $fld_title = false, $fld_ids = false, |
85 | $fld_sizes = false, $fld_redirect = false, $fld_patrolled = false, $fld_loginfo = false, |
86 | $fld_tags = false, $fld_sha1 = false; |
87 | |
88 | /** |
89 | * Sets internal state to include the desired properties in the output. |
90 | * @param array $prop Associative array of properties, only keys are used here |
91 | */ |
92 | public function initProperties( $prop ) { |
93 | $this->fld_comment = isset( $prop['comment'] ); |
94 | $this->fld_parsedcomment = isset( $prop['parsedcomment'] ); |
95 | $this->fld_user = isset( $prop['user'] ); |
96 | $this->fld_userid = isset( $prop['userid'] ); |
97 | $this->fld_flags = isset( $prop['flags'] ); |
98 | $this->fld_timestamp = isset( $prop['timestamp'] ); |
99 | $this->fld_title = isset( $prop['title'] ); |
100 | $this->fld_ids = isset( $prop['ids'] ); |
101 | $this->fld_sizes = isset( $prop['sizes'] ); |
102 | $this->fld_redirect = isset( $prop['redirect'] ); |
103 | $this->fld_patrolled = isset( $prop['patrolled'] ); |
104 | $this->fld_loginfo = isset( $prop['loginfo'] ); |
105 | $this->fld_tags = isset( $prop['tags'] ); |
106 | $this->fld_sha1 = isset( $prop['sha1'] ); |
107 | } |
108 | |
109 | public function execute() { |
110 | $this->run(); |
111 | } |
112 | |
113 | public function executeGenerator( $resultPageSet ) { |
114 | $this->run( $resultPageSet ); |
115 | } |
116 | |
117 | /** |
118 | * Generates and outputs the result of this query based upon the provided parameters. |
119 | * |
120 | * @param ApiPageSet|null $resultPageSet |
121 | */ |
122 | public function run( $resultPageSet = null ) { |
123 | $user = $this->getUser(); |
124 | /* Get the parameters of the request. */ |
125 | $params = $this->extractRequestParams(); |
126 | |
127 | /* Build our basic query. Namely, something along the lines of: |
128 | * SELECT * FROM recentchanges WHERE rc_timestamp > $start |
129 | * AND rc_timestamp < $end AND rc_namespace = $namespace |
130 | */ |
131 | $this->addTables( 'recentchanges' ); |
132 | $this->addTimestampWhereRange( 'rc_timestamp', $params['dir'], $params['start'], $params['end'] ); |
133 | |
134 | if ( $params['continue'] !== null ) { |
135 | $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'timestamp', 'int' ] ); |
136 | $db = $this->getDB(); |
137 | $op = $params['dir'] === 'older' ? '<=' : '>='; |
138 | $this->addWhere( $db->buildComparison( $op, [ |
139 | 'rc_timestamp' => $db->timestamp( $cont[0] ), |
140 | 'rc_id' => $cont[1], |
141 | ] ) ); |
142 | } |
143 | |
144 | $order = $params['dir'] === 'older' ? 'DESC' : 'ASC'; |
145 | $this->addOption( 'ORDER BY', [ |
146 | "rc_timestamp $order", |
147 | "rc_id $order", |
148 | ] ); |
149 | |
150 | if ( $params['type'] !== null ) { |
151 | $this->addWhereFld( 'rc_type', RecentChange::parseToRCType( $params['type'] ) ); |
152 | } |
153 | |
154 | $title = $params['title']; |
155 | if ( $title !== null ) { |
156 | $titleObj = Title::newFromText( $title ); |
157 | if ( $titleObj === null || $titleObj->isExternal() ) { |
158 | $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $title ) ] ); |
159 | } elseif ( $params['namespace'] && !in_array( $titleObj->getNamespace(), $params['namespace'] ) ) { |
160 | $this->requireMaxOneParameter( $params, 'title', 'namespace' ); |
161 | } |
162 | $this->addWhereFld( 'rc_namespace', $titleObj->getNamespace() ); |
163 | $this->addWhereFld( 'rc_title', $titleObj->getDBkey() ); |
164 | } else { |
165 | $this->addWhereFld( 'rc_namespace', $params['namespace'] ); |
166 | } |
167 | |
168 | if ( $params['show'] !== null ) { |
169 | $show = array_fill_keys( $params['show'], true ); |
170 | |
171 | /* Check for conflicting parameters. */ |
172 | if ( ( isset( $show['minor'] ) && isset( $show['!minor'] ) ) |
173 | || ( isset( $show['bot'] ) && isset( $show['!bot'] ) ) |
174 | || ( isset( $show['anon'] ) && isset( $show['!anon'] ) ) |
175 | || ( isset( $show['redirect'] ) && isset( $show['!redirect'] ) ) |
176 | || ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) ) |
177 | || ( isset( $show['patrolled'] ) && isset( $show['unpatrolled'] ) ) |
178 | || ( isset( $show['!patrolled'] ) && isset( $show['unpatrolled'] ) ) |
179 | || ( isset( $show['autopatrolled'] ) && isset( $show['!autopatrolled'] ) ) |
180 | || ( isset( $show['autopatrolled'] ) && isset( $show['unpatrolled'] ) ) |
181 | || ( isset( $show['autopatrolled'] ) && isset( $show['!patrolled'] ) ) |
182 | ) { |
183 | $this->dieWithError( 'apierror-show' ); |
184 | } |
185 | |
186 | // Check permissions |
187 | if ( $this->includesPatrollingFlags( $show ) && !$user->useRCPatrol() && !$user->useNPPatrol() ) { |
188 | $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' ); |
189 | } |
190 | |
191 | /* Add additional conditions to query depending upon parameters. */ |
192 | $this->addWhereIf( [ 'rc_minor' => 0 ], isset( $show['!minor'] ) ); |
193 | $this->addWhereIf( 'rc_minor != 0', isset( $show['minor'] ) ); |
194 | $this->addWhereIf( [ 'rc_bot' => 0 ], isset( $show['!bot'] ) ); |
195 | $this->addWhereIf( 'rc_bot != 0', isset( $show['bot'] ) ); |
196 | if ( isset( $show['anon'] ) || isset( $show['!anon'] ) ) { |
197 | $this->addTables( 'actor', 'actor' ); |
198 | $this->addJoinConds( [ 'actor' => [ 'JOIN', 'actor_id=rc_actor' ] ] ); |
199 | $this->addWhereIf( |
200 | [ 'actor_user' => null ], isset( $show['anon'] ) |
201 | ); |
202 | $this->addWhereIf( |
203 | 'actor_user IS NOT NULL', isset( $show['!anon'] ) |
204 | ); |
205 | } |
206 | $this->addWhereIf( [ 'rc_patrolled' => 0 ], isset( $show['!patrolled'] ) ); |
207 | $this->addWhereIf( 'rc_patrolled != 0', isset( $show['patrolled'] ) ); |
208 | $this->addWhereIf( [ 'page_is_redirect' => 1 ], isset( $show['redirect'] ) ); |
209 | |
210 | if ( isset( $show['unpatrolled'] ) ) { |
211 | // See ChangesList::isUnpatrolled |
212 | if ( $user->useRCPatrol() ) { |
213 | $this->addWhereFld( 'rc_patrolled', RecentChange::PRC_UNPATROLLED ); |
214 | } elseif ( $user->useNPPatrol() ) { |
215 | $this->addWhereFld( 'rc_patrolled', RecentChange::PRC_UNPATROLLED ); |
216 | $this->addWhereFld( 'rc_type', RC_NEW ); |
217 | } |
218 | } |
219 | |
220 | $this->addWhereIf( |
221 | 'rc_patrolled != ' . RecentChange::PRC_AUTOPATROLLED, |
222 | isset( $show['!autopatrolled'] ) |
223 | ); |
224 | $this->addWhereIf( |
225 | [ 'rc_patrolled' => RecentChange::PRC_AUTOPATROLLED ], |
226 | isset( $show['autopatrolled'] ) |
227 | ); |
228 | |
229 | // Don't throw log entries out the window here |
230 | $this->addWhereIf( |
231 | [ 'page_is_redirect' => [ 0, null ] ], |
232 | isset( $show['!redirect'] ) |
233 | ); |
234 | } |
235 | |
236 | $this->requireMaxOneParameter( $params, 'user', 'excludeuser' ); |
237 | |
238 | if ( $params['prop'] !== null ) { |
239 | $prop = array_fill_keys( $params['prop'], true ); |
240 | |
241 | /* Set up internal members based upon params. */ |
242 | $this->initProperties( $prop ); |
243 | } |
244 | |
245 | if ( $this->fld_user |
246 | || $this->fld_userid |
247 | || $params['user'] !== null |
248 | || $params['excludeuser'] !== null |
249 | ) { |
250 | $this->addTables( 'actor', 'actor' ); |
251 | $this->addFields( [ 'actor_name', 'actor_user', 'rc_actor' ] ); |
252 | $this->addJoinConds( [ 'actor' => [ 'JOIN', 'actor_id=rc_actor' ] ] ); |
253 | } |
254 | |
255 | if ( $params['user'] !== null ) { |
256 | $this->addWhereFld( 'actor_name', $params['user'] ); |
257 | } |
258 | |
259 | if ( $params['excludeuser'] !== null ) { |
260 | $this->addWhere( 'actor_name<>' . $this->getDB()->addQuotes( $params['excludeuser'] ) ); |
261 | } |
262 | |
263 | /* Add the fields we're concerned with to our query. */ |
264 | $this->addFields( [ |
265 | 'rc_id', |
266 | 'rc_timestamp', |
267 | 'rc_namespace', |
268 | 'rc_title', |
269 | 'rc_cur_id', |
270 | 'rc_type', |
271 | 'rc_deleted' |
272 | ] ); |
273 | |
274 | $showRedirects = false; |
275 | /* Determine what properties we need to display. */ |
276 | if ( $params['prop'] !== null ) { |
277 | if ( $this->fld_patrolled && !$user->useRCPatrol() && !$user->useNPPatrol() ) { |
278 | $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' ); |
279 | } |
280 | |
281 | /* Add fields to our query if they are specified as a needed parameter. */ |
282 | $this->addFieldsIf( [ 'rc_this_oldid', 'rc_last_oldid' ], $this->fld_ids ); |
283 | $this->addFieldsIf( [ 'rc_minor', 'rc_type', 'rc_bot' ], $this->fld_flags ); |
284 | $this->addFieldsIf( [ 'rc_old_len', 'rc_new_len' ], $this->fld_sizes ); |
285 | $this->addFieldsIf( [ 'rc_patrolled', 'rc_log_type' ], $this->fld_patrolled ); |
286 | $this->addFieldsIf( |
287 | [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ], |
288 | $this->fld_loginfo |
289 | ); |
290 | $showRedirects = $this->fld_redirect || isset( $show['redirect'] ) |
291 | || isset( $show['!redirect'] ); |
292 | } |
293 | $this->addFieldsIf( [ 'rc_this_oldid' ], |
294 | $resultPageSet && $params['generaterevisions'] ); |
295 | |
296 | if ( $this->fld_tags ) { |
297 | $this->addFields( [ 'ts_tags' => ChangeTags::makeTagSummarySubquery( 'recentchanges' ) ] ); |
298 | } |
299 | |
300 | if ( $this->fld_sha1 ) { |
301 | $this->addTables( 'revision' ); |
302 | $this->addJoinConds( [ 'revision' => [ 'LEFT JOIN', |
303 | [ 'rc_this_oldid=rev_id' ] ] ] ); |
304 | $this->addFields( [ 'rev_sha1', 'rev_deleted' ] ); |
305 | } |
306 | |
307 | if ( $params['toponly'] || $showRedirects ) { |
308 | $this->addTables( 'page' ); |
309 | $this->addJoinConds( [ 'page' => [ 'LEFT JOIN', |
310 | [ 'rc_namespace=page_namespace', 'rc_title=page_title' ] ] ] ); |
311 | $this->addFields( 'page_is_redirect' ); |
312 | |
313 | if ( $params['toponly'] ) { |
314 | $this->addWhere( 'rc_this_oldid = page_latest' ); |
315 | } |
316 | } |
317 | |
318 | if ( $params['tag'] !== null ) { |
319 | $this->addTables( 'change_tag' ); |
320 | $this->addJoinConds( [ 'change_tag' => [ 'JOIN', [ 'rc_id=ct_rc_id' ] ] ] ); |
321 | try { |
322 | $this->addWhereFld( 'ct_tag_id', $this->changeTagDefStore->getId( $params['tag'] ) ); |
323 | } catch ( NameTableAccessException $exception ) { |
324 | // Return nothing. |
325 | $this->addWhere( '1=0' ); |
326 | } |
327 | } |
328 | |
329 | // Paranoia: avoid brute force searches (T19342) |
330 | if ( $params['user'] !== null || $params['excludeuser'] !== null ) { |
331 | if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) { |
332 | $bitmask = RevisionRecord::DELETED_USER; |
333 | } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { |
334 | $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED; |
335 | } else { |
336 | $bitmask = 0; |
337 | } |
338 | if ( $bitmask ) { |
339 | $this->addWhere( $this->getDB()->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask" ); |
340 | } |
341 | } |
342 | if ( $this->getRequest()->getCheck( 'namespace' ) ) { |
343 | // LogPage::DELETED_ACTION hides the affected page, too. |
344 | if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) { |
345 | $bitmask = LogPage::DELETED_ACTION; |
346 | } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { |
347 | $bitmask = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED; |
348 | } else { |
349 | $bitmask = 0; |
350 | } |
351 | if ( $bitmask ) { |
352 | $this->addWhere( $this->getDB()->makeList( [ |
353 | 'rc_type != ' . RC_LOG, |
354 | $this->getDB()->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask", |
355 | ], LIST_OR ) ); |
356 | } |
357 | } |
358 | |
359 | if ( $this->fld_comment || $this->fld_parsedcomment ) { |
360 | $commentQuery = $this->commentStore->getJoin( 'rc_comment' ); |
361 | $this->addTables( $commentQuery['tables'] ); |
362 | $this->addFields( $commentQuery['fields'] ); |
363 | $this->addJoinConds( $commentQuery['joins'] ); |
364 | } |
365 | |
366 | if ( $params['slot'] !== null ) { |
367 | try { |
368 | $slotId = $this->slotRoleStore->getId( $params['slot'] ); |
369 | } catch ( Exception $e ) { |
370 | $slotId = null; |
371 | } |
372 | |
373 | $this->addTables( [ |
374 | 'slot' => 'slots', 'parent_slot' => 'slots' |
375 | ] ); |
376 | $this->addJoinConds( [ |
377 | 'slot' => [ 'LEFT JOIN', [ |
378 | 'rc_this_oldid = slot.slot_revision_id', |
379 | 'slot.slot_role_id' => $slotId, |
380 | ] ], |
381 | 'parent_slot' => [ 'LEFT JOIN', [ |
382 | 'rc_last_oldid = parent_slot.slot_revision_id', |
383 | 'parent_slot.slot_role_id' => $slotId, |
384 | ] ] |
385 | ] ); |
386 | // Detecting whether the slot has been touched as follows: |
387 | // 1. if slot_origin=slot_revision_id then the slot has been newly created or edited |
388 | // with this revision |
389 | // 2. otherwise if the content of a slot is different to the content of its parent slot, |
390 | // then the content of the slot has been changed in this revision |
391 | // (probably by a revert) |
392 | $this->addWhere( |
393 | 'slot.slot_origin = slot.slot_revision_id OR ' . |
394 | 'slot.slot_content_id != parent_slot.slot_content_id OR ' . |
395 | '(slot.slot_content_id IS NULL AND parent_slot.slot_content_id IS NOT NULL) OR ' . |
396 | '(slot.slot_content_id IS NOT NULL AND parent_slot.slot_content_id IS NULL)' |
397 | ); |
398 | // Only include changes that touch page content (i.e. RC_NEW, RC_EDIT) |
399 | $changeTypes = RecentChange::parseToRCType( |
400 | array_intersect( $params['type'], [ 'new', 'edit' ] ) |
401 | ); |
402 | if ( count( $changeTypes ) ) { |
403 | $this->addWhereFld( 'rc_type', $changeTypes ); |
404 | } else { |
405 | // Calling $this->addWhere() with an empty array does nothing, so explicitly |
406 | // add an unsatisfiable condition |
407 | $this->addWhere( [ 'rc_type' => null ] ); |
408 | } |
409 | } |
410 | |
411 | $this->addOption( 'LIMIT', $params['limit'] + 1 ); |
412 | $this->addOption( |
413 | 'MAX_EXECUTION_TIME', |
414 | $this->getConfig()->get( MainConfigNames::MaxExecutionTimeForExpensiveQueries ) |
415 | ); |
416 | |
417 | $hookData = []; |
418 | $count = 0; |
419 | /* Perform the actual query. */ |
420 | $res = $this->select( __METHOD__, [], $hookData ); |
421 | |
422 | // Do batch queries |
423 | if ( $this->fld_title && $resultPageSet === null ) { |
424 | $this->executeGenderCacheFromResultWrapper( $res, __METHOD__, 'rc' ); |
425 | } |
426 | if ( $this->fld_parsedcomment ) { |
427 | $this->formattedComments = $this->commentFormatter->formatItems( |
428 | $this->commentFormatter->rows( $res ) |
429 | ->indexField( 'rc_id' ) |
430 | ->commentKey( 'rc_comment' ) |
431 | ->namespaceField( 'rc_namespace' ) |
432 | ->titleField( 'rc_title' ) |
433 | ); |
434 | } |
435 | |
436 | $revids = []; |
437 | $titles = []; |
438 | |
439 | $result = $this->getResult(); |
440 | |
441 | /* Iterate through the rows, adding data extracted from them to our query result. */ |
442 | foreach ( $res as $row ) { |
443 | if ( $count === 0 && $resultPageSet !== null ) { |
444 | // Set the non-continue since the list of recentchanges is |
445 | // prone to having entries added at the start frequently. |
446 | $this->getContinuationManager()->addGeneratorNonContinueParam( |
447 | $this, 'continue', "$row->rc_timestamp|$row->rc_id" |
448 | ); |
449 | } |
450 | if ( ++$count > $params['limit'] ) { |
451 | // We've reached the one extra which shows that there are |
452 | // additional pages to be had. Stop here... |
453 | $this->setContinueEnumParameter( 'continue', "$row->rc_timestamp|$row->rc_id" ); |
454 | break; |
455 | } |
456 | |
457 | if ( $resultPageSet === null ) { |
458 | /* Extract the data from a single row. */ |
459 | $vals = $this->extractRowInfo( $row ); |
460 | |
461 | /* Add that row's data to our final output. */ |
462 | $fit = $this->processRow( $row, $vals, $hookData ) && |
463 | $result->addValue( [ 'query', $this->getModuleName() ], null, $vals ); |
464 | if ( !$fit ) { |
465 | $this->setContinueEnumParameter( 'continue', "$row->rc_timestamp|$row->rc_id" ); |
466 | break; |
467 | } |
468 | } elseif ( $params['generaterevisions'] ) { |
469 | $revid = (int)$row->rc_this_oldid; |
470 | if ( $revid > 0 ) { |
471 | $revids[] = $revid; |
472 | } |
473 | } else { |
474 | $titles[] = Title::makeTitle( $row->rc_namespace, $row->rc_title ); |
475 | } |
476 | } |
477 | |
478 | if ( $resultPageSet === null ) { |
479 | /* Format the result */ |
480 | $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'rc' ); |
481 | } elseif ( $params['generaterevisions'] ) { |
482 | $resultPageSet->populateFromRevisionIDs( $revids ); |
483 | } else { |
484 | $resultPageSet->populateFromTitles( $titles ); |
485 | } |
486 | } |
487 | |
488 | /** |
489 | * Extracts from a single sql row the data needed to describe one recent change. |
490 | * |
491 | * @param stdClass $row The row from which to extract the data. |
492 | * @return array An array mapping strings (descriptors) to their respective string values. |
493 | */ |
494 | public function extractRowInfo( $row ) { |
495 | /* Determine the title of the page that has been changed. */ |
496 | $title = Title::makeTitle( $row->rc_namespace, $row->rc_title ); |
497 | $user = $this->getUser(); |
498 | |
499 | /* Our output data. */ |
500 | $vals = []; |
501 | |
502 | $type = (int)$row->rc_type; |
503 | $vals['type'] = RecentChange::parseFromRCType( $type ); |
504 | |
505 | $anyHidden = false; |
506 | |
507 | /* Create a new entry in the result for the title. */ |
508 | if ( $this->fld_title || $this->fld_ids ) { |
509 | if ( $type === RC_LOG && ( $row->rc_deleted & LogPage::DELETED_ACTION ) ) { |
510 | $vals['actionhidden'] = true; |
511 | $anyHidden = true; |
512 | } |
513 | if ( $type !== RC_LOG || |
514 | LogEventsList::userCanBitfield( $row->rc_deleted, LogPage::DELETED_ACTION, $user ) |
515 | ) { |
516 | if ( $this->fld_title ) { |
517 | ApiQueryBase::addTitleInfo( $vals, $title ); |
518 | } |
519 | if ( $this->fld_ids ) { |
520 | $vals['pageid'] = (int)$row->rc_cur_id; |
521 | $vals['revid'] = (int)$row->rc_this_oldid; |
522 | $vals['old_revid'] = (int)$row->rc_last_oldid; |
523 | } |
524 | } |
525 | } |
526 | |
527 | if ( $this->fld_ids ) { |
528 | $vals['rcid'] = (int)$row->rc_id; |
529 | } |
530 | |
531 | /* Add user data and 'anon' flag, if user is anonymous. */ |
532 | if ( $this->fld_user || $this->fld_userid ) { |
533 | if ( $row->rc_deleted & RevisionRecord::DELETED_USER ) { |
534 | $vals['userhidden'] = true; |
535 | $anyHidden = true; |
536 | } |
537 | if ( RevisionRecord::userCanBitfield( $row->rc_deleted, RevisionRecord::DELETED_USER, $user ) ) { |
538 | if ( $this->fld_user ) { |
539 | $vals['user'] = $row->actor_name; |
540 | } |
541 | |
542 | if ( $this->fld_userid ) { |
543 | $vals['userid'] = (int)$row->actor_user; |
544 | } |
545 | |
546 | if ( isset( $row->actor_name ) && $this->userNameUtils->isTemp( $row->actor_name ) ) { |
547 | $vals['temp'] = true; |
548 | } |
549 | |
550 | if ( !$row->actor_user ) { |
551 | $vals['anon'] = true; |
552 | } |
553 | } |
554 | } |
555 | |
556 | /* Add flags, such as new, minor, bot. */ |
557 | if ( $this->fld_flags ) { |
558 | $vals['bot'] = (bool)$row->rc_bot; |
559 | $vals['new'] = $row->rc_type == RC_NEW; |
560 | $vals['minor'] = (bool)$row->rc_minor; |
561 | } |
562 | |
563 | /* Add sizes of each revision. (Only available on 1.10+) */ |
564 | if ( $this->fld_sizes ) { |
565 | $vals['oldlen'] = (int)$row->rc_old_len; |
566 | $vals['newlen'] = (int)$row->rc_new_len; |
567 | } |
568 | |
569 | /* Add the timestamp. */ |
570 | if ( $this->fld_timestamp ) { |
571 | $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->rc_timestamp ); |
572 | } |
573 | |
574 | /* Add edit summary / log summary. */ |
575 | if ( $this->fld_comment || $this->fld_parsedcomment ) { |
576 | if ( $row->rc_deleted & RevisionRecord::DELETED_COMMENT ) { |
577 | $vals['commenthidden'] = true; |
578 | $anyHidden = true; |
579 | } |
580 | if ( RevisionRecord::userCanBitfield( |
581 | $row->rc_deleted, RevisionRecord::DELETED_COMMENT, $user |
582 | ) ) { |
583 | if ( $this->fld_comment ) { |
584 | $vals['comment'] = $this->commentStore->getComment( 'rc_comment', $row )->text; |
585 | } |
586 | |
587 | if ( $this->fld_parsedcomment ) { |
588 | $vals['parsedcomment'] = $this->formattedComments[$row->rc_id]; |
589 | } |
590 | } |
591 | } |
592 | |
593 | if ( $this->fld_redirect ) { |
594 | $vals['redirect'] = (bool)$row->page_is_redirect; |
595 | } |
596 | |
597 | /* Add the patrolled flag */ |
598 | if ( $this->fld_patrolled ) { |
599 | $vals['patrolled'] = $row->rc_patrolled != RecentChange::PRC_UNPATROLLED; |
600 | $vals['unpatrolled'] = ChangesList::isUnpatrolled( $row, $user ); |
601 | $vals['autopatrolled'] = $row->rc_patrolled == RecentChange::PRC_AUTOPATROLLED; |
602 | } |
603 | |
604 | if ( $this->fld_loginfo && $row->rc_type == RC_LOG ) { |
605 | if ( $row->rc_deleted & LogPage::DELETED_ACTION ) { |
606 | $vals['actionhidden'] = true; |
607 | $anyHidden = true; |
608 | } |
609 | if ( LogEventsList::userCanBitfield( $row->rc_deleted, LogPage::DELETED_ACTION, $user ) ) { |
610 | $vals['logid'] = (int)$row->rc_logid; |
611 | $vals['logtype'] = $row->rc_log_type; |
612 | $vals['logaction'] = $row->rc_log_action; |
613 | $vals['logparams'] = LogFormatter::newFromRow( $row )->formatParametersForApi(); |
614 | } |
615 | } |
616 | |
617 | if ( $this->fld_tags ) { |
618 | if ( $row->ts_tags ) { |
619 | $tags = explode( ',', $row->ts_tags ); |
620 | ApiResult::setIndexedTagName( $tags, 'tag' ); |
621 | $vals['tags'] = $tags; |
622 | } else { |
623 | $vals['tags'] = []; |
624 | } |
625 | } |
626 | |
627 | if ( $this->fld_sha1 && $row->rev_sha1 !== null ) { |
628 | if ( $row->rev_deleted & RevisionRecord::DELETED_TEXT ) { |
629 | $vals['sha1hidden'] = true; |
630 | $anyHidden = true; |
631 | } |
632 | if ( RevisionRecord::userCanBitfield( |
633 | $row->rev_deleted, RevisionRecord::DELETED_TEXT, $user |
634 | ) ) { |
635 | if ( $row->rev_sha1 !== '' ) { |
636 | $vals['sha1'] = Wikimedia\base_convert( $row->rev_sha1, 36, 16, 40 ); |
637 | } else { |
638 | $vals['sha1'] = ''; |
639 | } |
640 | } |
641 | } |
642 | |
643 | if ( $anyHidden && ( $row->rc_deleted & RevisionRecord::DELETED_RESTRICTED ) ) { |
644 | $vals['suppressed'] = true; |
645 | } |
646 | |
647 | return $vals; |
648 | } |
649 | |
650 | /** |
651 | * @param array $flagsArray flipped array (string flags are keys) |
652 | * @return bool |
653 | */ |
654 | private function includesPatrollingFlags( array $flagsArray ) { |
655 | return isset( $flagsArray['patrolled'] ) || |
656 | isset( $flagsArray['!patrolled'] ) || |
657 | isset( $flagsArray['unpatrolled'] ) || |
658 | isset( $flagsArray['autopatrolled'] ) || |
659 | isset( $flagsArray['!autopatrolled'] ); |
660 | } |
661 | |
662 | public function getCacheMode( $params ) { |
663 | if ( isset( $params['show'] ) && |
664 | $this->includesPatrollingFlags( array_fill_keys( $params['show'], true ) ) |
665 | ) { |
666 | return 'private'; |
667 | } |
668 | if ( $this->userCanSeeRevDel() ) { |
669 | return 'private'; |
670 | } |
671 | if ( $params['prop'] !== null && in_array( 'parsedcomment', $params['prop'] ) ) { |
672 | // MediaWiki\CommentFormatter\CommentFormatter::formatItems() calls wfMessage() among other things |
673 | return 'anon-public-user-private'; |
674 | } |
675 | |
676 | return 'public'; |
677 | } |
678 | |
679 | public function getAllowedParams() { |
680 | $slotRoles = $this->slotRoleRegistry->getKnownRoles(); |
681 | sort( $slotRoles, SORT_STRING ); |
682 | |
683 | return [ |
684 | 'start' => [ |
685 | ParamValidator::PARAM_TYPE => 'timestamp' |
686 | ], |
687 | 'end' => [ |
688 | ParamValidator::PARAM_TYPE => 'timestamp' |
689 | ], |
690 | 'dir' => [ |
691 | ParamValidator::PARAM_DEFAULT => 'older', |
692 | ParamValidator::PARAM_TYPE => [ |
693 | 'newer', |
694 | 'older' |
695 | ], |
696 | ApiBase::PARAM_HELP_MSG => 'api-help-param-direction', |
697 | ApiBase::PARAM_HELP_MSG_PER_VALUE => [ |
698 | 'newer' => 'api-help-paramvalue-direction-newer', |
699 | 'older' => 'api-help-paramvalue-direction-older', |
700 | ], |
701 | ], |
702 | 'namespace' => [ |
703 | ParamValidator::PARAM_ISMULTI => true, |
704 | ParamValidator::PARAM_TYPE => 'namespace', |
705 | NamespaceDef::PARAM_EXTRA_NAMESPACES => [ NS_MEDIA, NS_SPECIAL ], |
706 | ], |
707 | 'user' => [ |
708 | ParamValidator::PARAM_TYPE => 'user', |
709 | UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'id', 'interwiki' ], |
710 | ], |
711 | 'excludeuser' => [ |
712 | ParamValidator::PARAM_TYPE => 'user', |
713 | UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'id', 'interwiki' ], |
714 | ], |
715 | 'tag' => null, |
716 | 'prop' => [ |
717 | ParamValidator::PARAM_ISMULTI => true, |
718 | ParamValidator::PARAM_DEFAULT => 'title|timestamp|ids', |
719 | ParamValidator::PARAM_TYPE => [ |
720 | 'user', |
721 | 'userid', |
722 | 'comment', |
723 | 'parsedcomment', |
724 | 'flags', |
725 | 'timestamp', |
726 | 'title', |
727 | 'ids', |
728 | 'sizes', |
729 | 'redirect', |
730 | 'patrolled', |
731 | 'loginfo', |
732 | 'tags', |
733 | 'sha1', |
734 | ], |
735 | ApiBase::PARAM_HELP_MSG_PER_VALUE => [], |
736 | ], |
737 | 'show' => [ |
738 | ParamValidator::PARAM_ISMULTI => true, |
739 | ParamValidator::PARAM_TYPE => [ |
740 | 'minor', |
741 | '!minor', |
742 | 'bot', |
743 | '!bot', |
744 | 'anon', |
745 | '!anon', |
746 | 'redirect', |
747 | '!redirect', |
748 | 'patrolled', |
749 | '!patrolled', |
750 | 'unpatrolled', |
751 | 'autopatrolled', |
752 | '!autopatrolled', |
753 | ] |
754 | ], |
755 | 'limit' => [ |
756 | ParamValidator::PARAM_DEFAULT => 10, |
757 | ParamValidator::PARAM_TYPE => 'limit', |
758 | IntegerDef::PARAM_MIN => 1, |
759 | IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1, |
760 | IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2 |
761 | ], |
762 | 'type' => [ |
763 | ParamValidator::PARAM_DEFAULT => 'edit|new|log|categorize', |
764 | ParamValidator::PARAM_ISMULTI => true, |
765 | ParamValidator::PARAM_TYPE => RecentChange::getChangeTypes() |
766 | ], |
767 | 'toponly' => false, |
768 | 'title' => null, |
769 | 'continue' => [ |
770 | ApiBase::PARAM_HELP_MSG => 'api-help-param-continue', |
771 | ], |
772 | 'generaterevisions' => false, |
773 | 'slot' => [ |
774 | ParamValidator::PARAM_TYPE => $slotRoles |
775 | ], |
776 | ]; |
777 | } |
778 | |
779 | protected function getExamplesMessages() { |
780 | return [ |
781 | 'action=query&list=recentchanges' |
782 | => 'apihelp-query+recentchanges-example-simple', |
783 | 'action=query&generator=recentchanges&grcshow=!patrolled&prop=info' |
784 | => 'apihelp-query+recentchanges-example-generator', |
785 | ]; |
786 | } |
787 | |
788 | public function getHelpUrls() { |
789 | return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Recentchanges'; |
790 | } |
791 | } |