MediaWiki master
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 ChangeTagsStore $changeTagsStore;
63 private NameTableStore $slotRoleStore;
64 private SlotRoleRegistry $slotRoleRegistry;
65 private UserNameUtils $userNameUtils;
66 private TempUserConfig $tempUserConfig;
67 private LogFormatterFactory $logFormatterFactory;
68
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
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
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',
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
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
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',
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 ],
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
840class_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:1565
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:1756
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.
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...
__construct(ApiQuery $query, string $moduleName, CommentStore $commentStore, RowCommentFormatter $commentFormatter, NameTableStore $changeTagDefStore, ChangeTagsStore $changeTagsStore, NameTableStore $slotRoleStore, SlotRoleRegistry $slotRoleRegistry, UserNameUtils $userNameUtils, TempUserConfig $tempUserConfig, LogFormatterFactory $logFormatterFactory)
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.
Read-write access to the change_tags table.
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.