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