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