MediaWiki REL1_35
ApiQueryRecentChanges.php
Go to the documentation of this file.
1<?php
27
35
36 public function __construct( ApiQuery $query, $moduleName ) {
37 parent::__construct( $query, $moduleName, 'rc' );
38 }
39
42
43 private $fld_comment = false, $fld_parsedcomment = false, $fld_user = false, $fld_userid = false,
44 $fld_flags = false, $fld_timestamp = false, $fld_title = false, $fld_ids = false,
45 $fld_sizes = false, $fld_redirect = false, $fld_patrolled = false, $fld_loginfo = false,
46 $fld_tags = false, $fld_sha1 = false, $token = [];
47
49
57 protected function getTokenFunctions() {
58 // Don't call the hooks twice
59 if ( isset( $this->tokenFunctions ) ) {
61 }
62
63 // If we're in a mode that breaks the same-origin policy, no tokens can
64 // be obtained
65 if ( $this->lacksSameOriginSecurity() ) {
66 return [];
67 }
68
69 $this->tokenFunctions = [
70 'patrol' => [ self::class, 'getPatrolToken' ]
71 ];
72 $this->getHookRunner()->onAPIQueryRecentChangesTokens( $this->tokenFunctions );
73
75 }
76
84 public static function getPatrolToken( $pageid, $title, $rc = null ) {
85 global $wgUser;
86
87 $validTokenUser = false;
88
89 if ( $rc ) {
90 if ( ( $wgUser->useRCPatrol() && $rc->getAttribute( 'rc_type' ) == RC_EDIT ) ||
91 ( $wgUser->useNPPatrol() && $rc->getAttribute( 'rc_type' ) == RC_NEW )
92 ) {
93 $validTokenUser = true;
94 }
95 } elseif ( $wgUser->useRCPatrol() || $wgUser->useNPPatrol() ) {
96 $validTokenUser = true;
97 }
98
99 if ( $validTokenUser ) {
100 // The patrol token is always the same, let's exploit that
101 static $cachedPatrolToken = null;
102
103 if ( $cachedPatrolToken === null ) {
104 $cachedPatrolToken = $wgUser->getEditToken( 'patrol' );
105 }
106
107 return $cachedPatrolToken;
108 }
109
110 return false;
111 }
112
117 public function initProperties( $prop ) {
118 $this->fld_comment = isset( $prop['comment'] );
119 $this->fld_parsedcomment = isset( $prop['parsedcomment'] );
120 $this->fld_user = isset( $prop['user'] );
121 $this->fld_userid = isset( $prop['userid'] );
122 $this->fld_flags = isset( $prop['flags'] );
123 $this->fld_timestamp = isset( $prop['timestamp'] );
124 $this->fld_title = isset( $prop['title'] );
125 $this->fld_ids = isset( $prop['ids'] );
126 $this->fld_sizes = isset( $prop['sizes'] );
127 $this->fld_redirect = isset( $prop['redirect'] );
128 $this->fld_patrolled = isset( $prop['patrolled'] );
129 $this->fld_loginfo = isset( $prop['loginfo'] );
130 $this->fld_tags = isset( $prop['tags'] );
131 $this->fld_sha1 = isset( $prop['sha1'] );
132 }
133
134 public function execute() {
135 $this->run();
136 }
137
138 public function executeGenerator( $resultPageSet ) {
139 $this->run( $resultPageSet );
140 }
141
147 public function run( $resultPageSet = null ) {
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 = explode( '|', $params['continue'] );
161 $this->dieContinueUsageIf( count( $cont ) != 2 );
162 $db = $this->getDB();
163 $timestamp = $db->addQuotes( $db->timestamp( $cont[0] ) );
164 $id = (int)$cont[1];
165 $this->dieContinueUsageIf( $id != $cont[1] );
166 $op = $params['dir'] === 'older' ? '<' : '>';
167 $this->addWhere(
168 "rc_timestamp $op $timestamp OR " .
169 "(rc_timestamp = $timestamp AND " .
170 "rc_id $op= $id)"
171 );
172 }
173
174 $order = $params['dir'] === 'older' ? 'DESC' : 'ASC';
175 $this->addOption( 'ORDER BY', [
176 "rc_timestamp $order",
177 "rc_id $order",
178 ] );
179
180 $this->addWhereFld( 'rc_namespace', $params['namespace'] );
181
182 if ( $params['type'] !== null ) {
183 try {
184 $this->addWhereFld( 'rc_type', RecentChange::parseToRCType( $params['type'] ) );
185 } catch ( Exception $e ) {
186 ApiBase::dieDebug( __METHOD__, $e->getMessage() );
187 }
188 }
189
190 $title = $params['title'];
191 if ( $title !== null ) {
192 $titleObj = Title::newFromText( $title );
193 if ( $titleObj === null ) {
194 $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $title ) ] );
195 }
196 $this->addWhereFld( 'rc_namespace', $titleObj->getNamespace() );
197 $this->addWhereFld( 'rc_title', $titleObj->getDBkey() );
198 }
199
200 if ( $params['show'] !== null ) {
201 $show = array_flip( $params['show'] );
202
203 /* Check for conflicting parameters. */
204 if ( ( isset( $show['minor'] ) && isset( $show['!minor'] ) )
205 || ( isset( $show['bot'] ) && isset( $show['!bot'] ) )
206 || ( isset( $show['anon'] ) && isset( $show['!anon'] ) )
207 || ( isset( $show['redirect'] ) && isset( $show['!redirect'] ) )
208 || ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) )
209 || ( isset( $show['patrolled'] ) && isset( $show['unpatrolled'] ) )
210 || ( isset( $show['!patrolled'] ) && isset( $show['unpatrolled'] ) )
211 || ( isset( $show['autopatrolled'] ) && isset( $show['!autopatrolled'] ) )
212 || ( isset( $show['autopatrolled'] ) && isset( $show['unpatrolled'] ) )
213 || ( isset( $show['autopatrolled'] ) && isset( $show['!patrolled'] ) )
214 ) {
215 $this->dieWithError( 'apierror-show' );
216 }
217
218 // Check permissions
219 if ( $this->includesPatrollingFlags( $show ) ) {
220 if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
221 $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' );
222 }
223 }
224
225 /* Add additional conditions to query depending upon parameters. */
226 $this->addWhereIf( 'rc_minor = 0', isset( $show['!minor'] ) );
227 $this->addWhereIf( 'rc_minor != 0', isset( $show['minor'] ) );
228 $this->addWhereIf( 'rc_bot = 0', isset( $show['!bot'] ) );
229 $this->addWhereIf( 'rc_bot != 0', isset( $show['bot'] ) );
230 if ( isset( $show['anon'] ) || isset( $show['!anon'] ) ) {
231 $actorMigration = ActorMigration::newMigration();
232 $actorQuery = $actorMigration->getJoin( 'rc_user' );
233 $this->addTables( $actorQuery['tables'] );
234 $this->addJoinConds( $actorQuery['joins'] );
235 $this->addWhereIf(
236 $actorMigration->isAnon( $actorQuery['fields']['rc_user'] ), isset( $show['anon'] )
237 );
238 $this->addWhereIf(
239 $actorMigration->isNotAnon( $actorQuery['fields']['rc_user'] ), isset( $show['!anon'] )
240 );
241 }
242 $this->addWhereIf( 'rc_patrolled = 0', isset( $show['!patrolled'] ) );
243 $this->addWhereIf( 'rc_patrolled != 0', isset( $show['patrolled'] ) );
244 $this->addWhereIf( 'page_is_redirect = 1', isset( $show['redirect'] ) );
245
246 if ( isset( $show['unpatrolled'] ) ) {
247 // See ChangesList::isUnpatrolled
248 if ( $user->useRCPatrol() ) {
249 $this->addWhere( 'rc_patrolled = ' . RecentChange::PRC_UNPATROLLED );
250 } elseif ( $user->useNPPatrol() ) {
251 $this->addWhere( 'rc_patrolled = ' . RecentChange::PRC_UNPATROLLED );
252 $this->addWhereFld( 'rc_type', RC_NEW );
253 }
254 }
255
256 $this->addWhereIf(
257 'rc_patrolled != ' . RecentChange::PRC_AUTOPATROLLED,
258 isset( $show['!autopatrolled'] )
259 );
260 $this->addWhereIf(
261 'rc_patrolled = ' . RecentChange::PRC_AUTOPATROLLED,
262 isset( $show['autopatrolled'] )
263 );
264
265 // Don't throw log entries out the window here
266 $this->addWhereIf(
267 'page_is_redirect = 0 OR page_is_redirect IS NULL',
268 isset( $show['!redirect'] )
269 );
270 }
271
272 $this->requireMaxOneParameter( $params, 'user', 'excludeuser' );
273
274 if ( $params['user'] !== null ) {
275 // Don't query by user ID here, it might be able to use the rc_user_text index.
276 $actorQuery = ActorMigration::newMigration()
277 ->getWhere( $this->getDB(), 'rc_user', $params['user'], false );
278 $this->addTables( $actorQuery['tables'] );
279 $this->addJoinConds( $actorQuery['joins'] );
280 $this->addWhere( $actorQuery['conds'] );
281 }
282
283 if ( $params['excludeuser'] !== null ) {
284 // Here there's no chance to use the rc_user_text index, so allow ID to be used.
285 $actorQuery = ActorMigration::newMigration()
286 ->getWhere( $this->getDB(), 'rc_user', $params['excludeuser'] );
287 $this->addTables( $actorQuery['tables'] );
288 $this->addJoinConds( $actorQuery['joins'] );
289 $this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' );
290 }
291
292 /* Add the fields we're concerned with to our query. */
293 $this->addFields( [
294 'rc_id',
295 'rc_timestamp',
296 'rc_namespace',
297 'rc_title',
298 'rc_cur_id',
299 'rc_type',
300 'rc_deleted'
301 ] );
302
303 $showRedirects = false;
304 /* Determine what properties we need to display. */
305 if ( $params['prop'] !== null ) {
306 $prop = array_flip( $params['prop'] );
307
308 /* Set up internal members based upon params. */
309 $this->initProperties( $prop );
310
311 if ( $this->fld_patrolled && !$user->useRCPatrol() && !$user->useNPPatrol() ) {
312 $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' );
313 }
314
315 /* Add fields to our query if they are specified as a needed parameter. */
316 $this->addFieldsIf( [ 'rc_this_oldid', 'rc_last_oldid' ], $this->fld_ids );
317 $this->addFieldsIf( [ 'rc_minor', 'rc_type', 'rc_bot' ], $this->fld_flags );
318 $this->addFieldsIf( [ 'rc_old_len', 'rc_new_len' ], $this->fld_sizes );
319 $this->addFieldsIf( [ 'rc_patrolled', 'rc_log_type' ], $this->fld_patrolled );
320 $this->addFieldsIf(
321 [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ],
322 $this->fld_loginfo
323 );
324 $showRedirects = $this->fld_redirect || isset( $show['redirect'] )
325 || isset( $show['!redirect'] );
326 }
327 $this->addFieldsIf( [ 'rc_this_oldid' ],
328 $resultPageSet && $params['generaterevisions'] );
329
330 if ( $this->fld_tags ) {
331 $this->addFields( [ 'ts_tags' => ChangeTags::makeTagSummarySubquery( 'recentchanges' ) ] );
332 }
333
334 if ( $this->fld_sha1 ) {
335 $this->addTables( 'revision' );
336 $this->addJoinConds( [ 'revision' => [ 'LEFT JOIN',
337 [ 'rc_this_oldid=rev_id' ] ] ] );
338 $this->addFields( [ 'rev_sha1', 'rev_deleted' ] );
339 }
340
341 if ( $params['toponly'] || $showRedirects ) {
342 $this->addTables( 'page' );
343 $this->addJoinConds( [ 'page' => [ 'LEFT JOIN',
344 [ 'rc_namespace=page_namespace', 'rc_title=page_title' ] ] ] );
345 $this->addFields( 'page_is_redirect' );
346
347 if ( $params['toponly'] ) {
348 $this->addWhere( 'rc_this_oldid = page_latest' );
349 }
350 }
351
352 if ( $params['tag'] !== null ) {
353 $this->addTables( 'change_tag' );
354 $this->addJoinConds( [ 'change_tag' => [ 'JOIN', [ 'rc_id=ct_rc_id' ] ] ] );
355 $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
356 try {
357 $this->addWhereFld( 'ct_tag_id', $changeTagDefStore->getId( $params['tag'] ) );
358 } catch ( NameTableAccessException $exception ) {
359 // Return nothing.
360 $this->addWhere( '1=0' );
361 }
362 }
363
364 // Paranoia: avoid brute force searches (T19342)
365 if ( $params['user'] !== null || $params['excludeuser'] !== null ) {
366 if ( !$this->getPermissionManager()->userHasRight( $user, 'deletedhistory' ) ) {
367 $bitmask = RevisionRecord::DELETED_USER;
368 } elseif ( !$this->getPermissionManager()
369 ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
370 ) {
371 $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
372 } else {
373 $bitmask = 0;
374 }
375 if ( $bitmask ) {
376 $this->addWhere( $this->getDB()->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask" );
377 }
378 }
379 if ( $this->getRequest()->getCheck( 'namespace' ) ) {
380 // LogPage::DELETED_ACTION hides the affected page, too.
381 if ( !$this->getPermissionManager()->userHasRight( $user, 'deletedhistory' ) ) {
382 $bitmask = LogPage::DELETED_ACTION;
383 } elseif ( !$this->getPermissionManager()
384 ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
385 ) {
387 } else {
388 $bitmask = 0;
389 }
390 if ( $bitmask ) {
391 $this->addWhere( $this->getDB()->makeList( [
392 'rc_type != ' . RC_LOG,
393 $this->getDB()->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask",
394 ], LIST_OR ) );
395 }
396 }
397
398 $this->token = $params['token'];
399
400 if ( $this->fld_comment || $this->fld_parsedcomment || $this->token ) {
401 $this->commentStore = CommentStore::getStore();
402 $commentQuery = $this->commentStore->getJoin( 'rc_comment' );
403 $this->addTables( $commentQuery['tables'] );
404 $this->addFields( $commentQuery['fields'] );
405 $this->addJoinConds( $commentQuery['joins'] );
406 }
407
408 if ( $this->fld_user || $this->fld_userid || $this->token !== null ) {
409 // Token needs rc_user for RecentChange::newFromRow/User::newFromAnyId (T228425)
410 $actorQuery = ActorMigration::newMigration()->getJoin( 'rc_user' );
411 $this->addTables( $actorQuery['tables'] );
412 $this->addFields( $actorQuery['fields'] );
413 $this->addJoinConds( $actorQuery['joins'] );
414 }
415
416 if ( $params['slot'] !== null ) {
417 try {
418 $slotId = MediaWikiServices::getInstance()->getSlotRoleStore()->getId(
419 $params['slot']
420 );
421 } catch ( \Exception $e ) {
422 $slotId = null;
423 }
424
425 $this->addTables( [
426 'slot' => 'slots', 'parent_slot' => 'slots'
427 ] );
428 $this->addJoinConds( [
429 'slot' => [ 'LEFT JOIN', [
430 'rc_this_oldid = slot.slot_revision_id',
431 'slot.slot_role_id' => $slotId,
432 ] ],
433 'parent_slot' => [ 'LEFT JOIN', [
434 'rc_last_oldid = parent_slot.slot_revision_id',
435 'parent_slot.slot_role_id' => $slotId,
436 ] ]
437 ] );
438 // Detecting whether the slot has been touched as follows:
439 // 1. if slot_origin=slot_revision_id then the slot has been newly created or edited
440 // with this revision
441 // 2. otherwise if the content of a slot is different to the content of its parent slot,
442 // then the content of the slot has been changed in this revision
443 // (probably by a revert)
444 $this->addWhere(
445 'slot.slot_origin = slot.slot_revision_id OR ' .
446 'slot.slot_content_id != parent_slot.slot_content_id OR ' .
447 '(slot.slot_content_id IS NULL AND parent_slot.slot_content_id IS NOT NULL) OR ' .
448 '(slot.slot_content_id IS NOT NULL AND parent_slot.slot_content_id IS NULL)'
449 );
450 // Only include changes that touch page content (i.e. RC_NEW, RC_EDIT)
451 $changeTypes = RecentChange::parseToRCType(
452 array_intersect( $params['type'], [ 'new', 'edit' ] )
453 );
454 if ( count( $changeTypes ) ) {
455 $this->addWhereFld( 'rc_type', $changeTypes );
456 } else {
457 // Calling $this->addWhere() with an empty array does nothing, so explicitly
458 // add an unsatisfiable condition
459 $this->addWhere( 'rc_type IS NULL' );
460 }
461 }
462
463 $this->addOption( 'LIMIT', $params['limit'] + 1 );
464 $this->addOption(
465 'MAX_EXECUTION_TIME',
466 $this->getConfig()->get( 'MaxExecutionTimeForExpensiveQueries' )
467 );
468
469 $hookData = [];
470 $count = 0;
471 /* Perform the actual query. */
472 $res = $this->select( __METHOD__, [], $hookData );
473
474 if ( $this->fld_title && $resultPageSet === null ) {
475 $this->executeGenderCacheFromResultWrapper( $res, __METHOD__, 'rc' );
476 }
477
478 $revids = [];
479 $titles = [];
480
481 $result = $this->getResult();
482
483 /* Iterate through the rows, adding data extracted from them to our query result. */
484 foreach ( $res as $row ) {
485 if ( $count === 0 && $resultPageSet !== null ) {
486 // Set the non-continue since the list of recentchanges is
487 // prone to having entries added at the start frequently.
488 $this->getContinuationManager()->addGeneratorNonContinueParam(
489 $this, 'continue', "$row->rc_timestamp|$row->rc_id"
490 );
491 }
492 if ( ++$count > $params['limit'] ) {
493 // We've reached the one extra which shows that there are
494 // additional pages to be had. Stop here...
495 $this->setContinueEnumParameter( 'continue', "$row->rc_timestamp|$row->rc_id" );
496 break;
497 }
498
499 if ( $resultPageSet === null ) {
500 /* Extract the data from a single row. */
501 $vals = $this->extractRowInfo( $row );
502
503 /* Add that row's data to our final output. */
504 $fit = $this->processRow( $row, $vals, $hookData ) &&
505 $result->addValue( [ 'query', $this->getModuleName() ], null, $vals );
506 if ( !$fit ) {
507 $this->setContinueEnumParameter( 'continue', "$row->rc_timestamp|$row->rc_id" );
508 break;
509 }
510 } elseif ( $params['generaterevisions'] ) {
511 $revid = (int)$row->rc_this_oldid;
512 if ( $revid > 0 ) {
513 $revids[] = $revid;
514 }
515 } else {
516 $titles[] = Title::makeTitle( $row->rc_namespace, $row->rc_title );
517 }
518 }
519
520 if ( $resultPageSet === null ) {
521 /* Format the result */
522 $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'rc' );
523 } elseif ( $params['generaterevisions'] ) {
524 $resultPageSet->populateFromRevisionIDs( $revids );
525 } else {
526 $resultPageSet->populateFromTitles( $titles );
527 }
528 }
529
536 public function extractRowInfo( $row ) {
537 /* Determine the title of the page that has been changed. */
538 $title = Title::makeTitle( $row->rc_namespace, $row->rc_title );
539 $user = $this->getUser();
540
541 /* Our output data. */
542 $vals = [];
543
544 $type = (int)$row->rc_type;
545 $vals['type'] = RecentChange::parseFromRCType( $type );
546
547 $anyHidden = false;
548
549 /* Create a new entry in the result for the title. */
550 if ( $this->fld_title || $this->fld_ids ) {
551 if ( $type === RC_LOG && ( $row->rc_deleted & LogPage::DELETED_ACTION ) ) {
552 $vals['actionhidden'] = true;
553 $anyHidden = true;
554 }
555 if ( $type !== RC_LOG ||
556 LogEventsList::userCanBitfield( $row->rc_deleted, LogPage::DELETED_ACTION, $user )
557 ) {
558 if ( $this->fld_title ) {
560 }
561 if ( $this->fld_ids ) {
562 $vals['pageid'] = (int)$row->rc_cur_id;
563 $vals['revid'] = (int)$row->rc_this_oldid;
564 $vals['old_revid'] = (int)$row->rc_last_oldid;
565 }
566 }
567 }
568
569 if ( $this->fld_ids ) {
570 $vals['rcid'] = (int)$row->rc_id;
571 }
572
573 /* Add user data and 'anon' flag, if user is anonymous. */
574 if ( $this->fld_user || $this->fld_userid ) {
575 if ( $row->rc_deleted & RevisionRecord::DELETED_USER ) {
576 $vals['userhidden'] = true;
577 $anyHidden = true;
578 }
579 if ( RevisionRecord::userCanBitfield( $row->rc_deleted, RevisionRecord::DELETED_USER, $user ) ) {
580 if ( $this->fld_user ) {
581 $vals['user'] = $row->rc_user_text;
582 }
583
584 if ( $this->fld_userid ) {
585 $vals['userid'] = (int)$row->rc_user;
586 }
587
588 if ( !$row->rc_user ) {
589 $vals['anon'] = true;
590 }
591 }
592 }
593
594 /* Add flags, such as new, minor, bot. */
595 if ( $this->fld_flags ) {
596 $vals['bot'] = (bool)$row->rc_bot;
597 $vals['new'] = $row->rc_type == RC_NEW;
598 $vals['minor'] = (bool)$row->rc_minor;
599 }
600
601 /* Add sizes of each revision. (Only available on 1.10+) */
602 if ( $this->fld_sizes ) {
603 $vals['oldlen'] = (int)$row->rc_old_len;
604 $vals['newlen'] = (int)$row->rc_new_len;
605 }
606
607 /* Add the timestamp. */
608 if ( $this->fld_timestamp ) {
609 $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->rc_timestamp );
610 }
611
612 /* Add edit summary / log summary. */
613 if ( $this->fld_comment || $this->fld_parsedcomment ) {
614 if ( $row->rc_deleted & RevisionRecord::DELETED_COMMENT ) {
615 $vals['commenthidden'] = true;
616 $anyHidden = true;
617 }
618 if ( RevisionRecord::userCanBitfield(
619 $row->rc_deleted, RevisionRecord::DELETED_COMMENT, $user
620 ) ) {
621 $comment = $this->commentStore->getComment( 'rc_comment', $row )->text;
622 if ( $this->fld_comment ) {
623 $vals['comment'] = $comment;
624 }
625
626 if ( $this->fld_parsedcomment ) {
627 $vals['parsedcomment'] = Linker::formatComment( $comment, $title );
628 }
629 }
630 }
631
632 if ( $this->fld_redirect ) {
633 $vals['redirect'] = (bool)$row->page_is_redirect;
634 }
635
636 /* Add the patrolled flag */
637 if ( $this->fld_patrolled ) {
638 $vals['patrolled'] = $row->rc_patrolled != RecentChange::PRC_UNPATROLLED;
639 $vals['unpatrolled'] = ChangesList::isUnpatrolled( $row, $user );
640 $vals['autopatrolled'] = $row->rc_patrolled == RecentChange::PRC_AUTOPATROLLED;
641 }
642
643 if ( $this->fld_loginfo && $row->rc_type == RC_LOG ) {
644 if ( $row->rc_deleted & LogPage::DELETED_ACTION ) {
645 $vals['actionhidden'] = true;
646 $anyHidden = true;
647 }
648 if ( LogEventsList::userCanBitfield( $row->rc_deleted, LogPage::DELETED_ACTION, $user ) ) {
649 $vals['logid'] = (int)$row->rc_logid;
650 $vals['logtype'] = $row->rc_log_type;
651 $vals['logaction'] = $row->rc_log_action;
652 $vals['logparams'] = LogFormatter::newFromRow( $row )->formatParametersForApi();
653 }
654 }
655
656 if ( $this->fld_tags ) {
657 if ( $row->ts_tags ) {
658 $tags = explode( ',', $row->ts_tags );
659 ApiResult::setIndexedTagName( $tags, 'tag' );
660 $vals['tags'] = $tags;
661 } else {
662 $vals['tags'] = [];
663 }
664 }
665
666 if ( $this->fld_sha1 && $row->rev_sha1 !== null ) {
667 if ( $row->rev_deleted & RevisionRecord::DELETED_TEXT ) {
668 $vals['sha1hidden'] = true;
669 $anyHidden = true;
670 }
671 if ( RevisionRecord::userCanBitfield(
672 $row->rev_deleted, RevisionRecord::DELETED_TEXT, $user
673 ) ) {
674 if ( $row->rev_sha1 !== '' ) {
675 $vals['sha1'] = Wikimedia\base_convert( $row->rev_sha1, 36, 16, 40 );
676 } else {
677 $vals['sha1'] = '';
678 }
679 }
680 }
681
682 if ( $this->token !== null ) {
684 foreach ( $this->token as $t ) {
685 $val = call_user_func( $tokenFunctions[$t], $row->rc_cur_id,
686 $title, RecentChange::newFromRow( $row ) );
687 if ( $val === false ) {
688 $this->addWarning( [ 'apiwarn-tokennotallowed', $t ] );
689 } else {
690 $vals[$t . 'token'] = $val;
691 }
692 }
693 }
694
695 if ( $anyHidden && ( $row->rc_deleted & RevisionRecord::DELETED_RESTRICTED ) ) {
696 $vals['suppressed'] = true;
697 }
698
699 return $vals;
700 }
701
706 private function includesPatrollingFlags( array $flagsArray ) {
707 return isset( $flagsArray['patrolled'] ) ||
708 isset( $flagsArray['!patrolled'] ) ||
709 isset( $flagsArray['unpatrolled'] ) ||
710 isset( $flagsArray['autopatrolled'] ) ||
711 isset( $flagsArray['!autopatrolled'] );
712 }
713
714 public function getCacheMode( $params ) {
715 if ( isset( $params['show'] ) &&
716 $this->includesPatrollingFlags( array_flip( $params['show'] ) )
717 ) {
718 return 'private';
719 }
720 if ( isset( $params['token'] ) ) {
721 return 'private';
722 }
723 if ( $this->userCanSeeRevDel() ) {
724 return 'private';
725 }
726 if ( $params['prop'] !== null && in_array( 'parsedcomment', $params['prop'] ) ) {
727 // formatComment() calls wfMessage() among other things
728 return 'anon-public-user-private';
729 }
730
731 return 'public';
732 }
733
734 public function getAllowedParams() {
735 $slotRoles = MediaWikiServices::getInstance()->getSlotRoleRegistry()->getKnownRoles();
736 sort( $slotRoles, SORT_STRING );
737
738 return [
739 'start' => [
740 ApiBase::PARAM_TYPE => 'timestamp'
741 ],
742 'end' => [
743 ApiBase::PARAM_TYPE => 'timestamp'
744 ],
745 'dir' => [
746 ApiBase::PARAM_DFLT => 'older',
748 'newer',
749 'older'
750 ],
751 ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
752 ],
753 'namespace' => [
755 ApiBase::PARAM_TYPE => 'namespace',
757 ],
758 'user' => [
759 ApiBase::PARAM_TYPE => 'user',
760 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
761 UserDef::PARAM_RETURN_OBJECT => true,
762 ],
763 'excludeuser' => [
764 ApiBase::PARAM_TYPE => 'user',
765 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
766 UserDef::PARAM_RETURN_OBJECT => true,
767 ],
768 'tag' => null,
769 'prop' => [
771 ApiBase::PARAM_DFLT => 'title|timestamp|ids',
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 'token' => [
792 ApiBase::PARAM_TYPE => array_keys( $this->getTokenFunctions() ),
794 ],
795 'show' => [
798 'minor',
799 '!minor',
800 'bot',
801 '!bot',
802 'anon',
803 '!anon',
804 'redirect',
805 '!redirect',
806 'patrolled',
807 '!patrolled',
808 'unpatrolled',
809 'autopatrolled',
810 '!autopatrolled',
811 ]
812 ],
813 'limit' => [
815 ApiBase::PARAM_TYPE => 'limit',
819 ],
820 'type' => [
821 ApiBase::PARAM_DFLT => 'edit|new|log|categorize',
823 ApiBase::PARAM_TYPE => RecentChange::getChangeTypes()
824 ],
825 'toponly' => false,
826 'title' => null,
827 'continue' => [
828 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
829 ],
830 'generaterevisions' => false,
831 'slot' => [
832 ApiBase::PARAM_TYPE => $slotRoles
833 ],
834 ];
835 }
836
837 protected function getExamplesMessages() {
838 return [
839 'action=query&list=recentchanges'
840 => 'apihelp-query+recentchanges-example-simple',
841 'action=query&generator=recentchanges&grcshow=!patrolled&prop=info'
842 => 'apihelp-query+recentchanges-example-generator',
843 ];
844 }
845
846 public function getHelpUrls() {
847 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Recentchanges';
848 }
849}
getUser()
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1437
const PARAM_MAX2
Definition ApiBase.php:86
const PARAM_DEPRECATED
Definition ApiBase.php:98
const PARAM_MAX
Definition ApiBase.php:82
dieContinueUsageIf( $condition)
Die with the 'badcontinue' error.
Definition ApiBase.php:1617
static dieDebug( $method, $message)
Internal code errors should be reported with this method.
Definition ApiBase.php:1629
const PARAM_TYPE
Definition ApiBase.php:78
const PARAM_DFLT
Definition ApiBase.php:70
getPermissionManager()
Obtain a PermissionManager instance that subclasses may use in their authorization checks.
Definition ApiBase.php:692
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, this is an array mapping those values to $msg...
Definition ApiBase.php:195
const PARAM_MIN
Definition ApiBase.php:90
const LIMIT_BIG1
Fast query, standard limit.
Definition ApiBase.php:220
requireMaxOneParameter( $params,... $required)
Die if more than one of a certain set of parameters is set and not false.
Definition ApiBase.php:944
getResult()
Get the result object.
Definition ApiBase.php:620
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:772
const PARAM_EXTRA_NAMESPACES
Definition ApiBase.php:118
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition ApiBase.php:162
addWarning( $msg, $code=null, $data=null)
Add a warning for this module.
Definition ApiBase.php:1356
const LIMIT_BIG2
Fast query, apihighlimits limit.
Definition ApiBase.php:222
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:499
getHookRunner()
Get an ApiHookRunner for running core API hooks.
Definition ApiBase.php:717
getContinuationManager()
Get the continuation manager.
Definition ApiBase.php:662
const PARAM_ISMULTI
Definition ApiBase.php:74
lacksSameOriginSecurity()
Returns true if the current request breaks the same-origin policy.
Definition ApiBase.php:548
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) Stable to override.
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.
includesPatrollingFlags(array $flagsArray)
__construct(ApiQuery $query, $moduleName)
static getPatrolToken( $pageid, $title, $rc=null)
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.
getTokenFunctions()
Get an array mapping token names to their handler functions.
This is the main query class.
Definition ApiQuery.php:37
static makeTagSummarySubquery( $tables)
Make the tag summary subquery based on the given tables and return it.
CommentStore handles storage of comments (edit summaries, log reasons, etc) in the database.
static formatComment( $comment, $title=null, $local=false, $wikiId=null)
This function is called by all recent changes variants, by the page history, and by the user contribu...
Definition Linker.php:1209
static newFromRow( $row)
Handy shortcut for constructing a formatter directly from database row.
const DELETED_RESTRICTED
Definition LogPage.php:41
const DELETED_ACTION
Definition LogPage.php:38
MediaWikiServices is the service locator for the application scope of MediaWiki.
Type definition for user types.
Definition UserDef.php:23
Page revision base class.
Exception representing a failure to look up a row from a name table.
const RC_NEW
Definition Defines.php:133
const NS_SPECIAL
Definition Defines.php:59
const LIST_OR
Definition Defines.php:52
const RC_LOG
Definition Defines.php:134
const NS_MEDIA
Definition Defines.php:58
const RC_EDIT
Definition Defines.php:132
return true
Definition router.php:92