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