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