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