69 private $formattedComments = [];
83 parent::__construct( $query, $moduleName,
'rc' );
84 $this->commentStore = $commentStore;
85 $this->commentFormatter = $commentFormatter;
86 $this->changeTagDefStore = $changeTagDefStore;
87 $this->slotRoleStore = $slotRoleStore;
88 $this->slotRoleRegistry = $slotRoleRegistry;
89 $this->userNameUtils = $userNameUtils;
90 $this->tempUserConfig = $tempUserConfig;
91 $this->logFormatterFactory = $logFormatterFactory;
94 private bool $fld_comment =
false;
95 private bool $fld_parsedcomment =
false;
96 private bool $fld_user =
false;
97 private bool $fld_userid =
false;
98 private bool $fld_flags =
false;
99 private bool $fld_timestamp =
false;
100 private bool $fld_title =
false;
101 private bool $fld_ids =
false;
102 private bool $fld_sizes =
false;
103 private bool $fld_redirect =
false;
104 private bool $fld_patrolled =
false;
105 private bool $fld_loginfo =
false;
106 private bool $fld_tags =
false;
107 private bool $fld_sha1 =
false;
114 $this->fld_comment = isset( $prop[
'comment'] );
115 $this->fld_parsedcomment = isset( $prop[
'parsedcomment'] );
116 $this->fld_user = isset( $prop[
'user'] );
117 $this->fld_userid = isset( $prop[
'userid'] );
118 $this->fld_flags = isset( $prop[
'flags'] );
119 $this->fld_timestamp = isset( $prop[
'timestamp'] );
120 $this->fld_title = isset( $prop[
'title'] );
121 $this->fld_ids = isset( $prop[
'ids'] );
122 $this->fld_sizes = isset( $prop[
'sizes'] );
123 $this->fld_redirect = isset( $prop[
'redirect'] );
124 $this->fld_patrolled = isset( $prop[
'patrolled'] );
125 $this->fld_loginfo = isset( $prop[
'loginfo'] );
126 $this->fld_tags = isset( $prop[
'tags'] );
127 $this->fld_sha1 = isset( $prop[
'sha1'] );
135 $this->
run( $resultPageSet );
143 public function run( $resultPageSet =
null ) {
144 $db = $this->
getDB();
156 if (
$params[
'continue'] !==
null ) {
158 $op =
$params[
'dir'] ===
'older' ?
'<=' :
'>=';
159 $this->
addWhere( $db->buildComparison( $op, [
160 'rc_timestamp' => $db->timestamp( $cont[0] ),
165 $order =
$params[
'dir'] ===
'older' ?
'DESC' :
'ASC';
167 "rc_timestamp $order",
171 if (
$params[
'type'] !==
null ) {
176 if ( $title !==
null ) {
177 $titleObj = Title::newFromText( $title );
178 if ( $titleObj ===
null || $titleObj->isExternal() ) {
180 } elseif (
$params[
'namespace'] && !in_array( $titleObj->getNamespace(),
$params[
'namespace'] ) ) {
183 $this->
addWhereFld(
'rc_namespace', $titleObj->getNamespace() );
184 $this->
addWhereFld(
'rc_title', $titleObj->getDBkey() );
189 if (
$params[
'show'] !==
null ) {
190 $show = array_fill_keys(
$params[
'show'],
true );
193 if ( ( isset( $show[
'minor'] ) && isset( $show[
'!minor'] ) )
194 || ( isset( $show[
'bot'] ) && isset( $show[
'!bot'] ) )
195 || ( isset( $show[
'anon'] ) && isset( $show[
'!anon'] ) )
196 || ( isset( $show[
'redirect'] ) && isset( $show[
'!redirect'] ) )
197 || ( isset( $show[
'patrolled'] ) && isset( $show[
'!patrolled'] ) )
198 || ( isset( $show[
'patrolled'] ) && isset( $show[
'unpatrolled'] ) )
199 || ( isset( $show[
'!patrolled'] ) && isset( $show[
'unpatrolled'] ) )
200 || ( isset( $show[
'autopatrolled'] ) && isset( $show[
'!autopatrolled'] ) )
201 || ( isset( $show[
'autopatrolled'] ) && isset( $show[
'unpatrolled'] ) )
202 || ( isset( $show[
'autopatrolled'] ) && isset( $show[
'!patrolled'] ) )
208 if ( $this->includesPatrollingFlags( $show ) && !$user->useRCPatrol() && !$user->useNPPatrol() ) {
209 $this->
dieWithError(
'apierror-permissiondenied-patrolflag',
'permissiondenied' );
213 $this->
addWhereIf( [
'rc_minor' => 0 ], isset( $show[
'!minor'] ) );
214 $this->
addWhereIf( $db->expr(
'rc_minor',
'!=', 0 ), isset( $show[
'minor'] ) );
215 $this->
addWhereIf( [
'rc_bot' => 0 ], isset( $show[
'!bot'] ) );
216 $this->
addWhereIf( $db->expr(
'rc_bot',
'!=', 0 ), isset( $show[
'bot'] ) );
217 if ( isset( $show[
'anon'] ) || isset( $show[
'!anon'] ) ) {
219 $this->
addJoinConds( [
'actor' => [
'JOIN',
'actor_id=rc_actor' ] ] );
221 if ( $this->tempUserConfig->isKnown() ) {
222 $isAnon = isset( $show[
'anon'] );
223 $anonExpr = $db->expr(
'actor_user', $isAnon ?
'=' :
'!=', null );
225 $anonExpr = $anonExpr->orExpr( $this->tempUserConfig->getMatchCondition(
231 $anonExpr = $anonExpr->andExpr( $this->tempUserConfig->getMatchCondition(
234 IExpression::NOT_LIKE
240 [
'actor_user' =>
null ], isset( $show[
'anon'] )
243 $db->expr(
'actor_user',
'!=',
null ), isset( $show[
'!anon'] )
247 $this->
addWhereIf( [
'rc_patrolled' => 0 ], isset( $show[
'!patrolled'] ) );
248 $this->
addWhereIf( $db->expr(
'rc_patrolled',
'!=', 0 ), isset( $show[
'patrolled'] ) );
249 $this->
addWhereIf( [
'page_is_redirect' => 1 ], isset( $show[
'redirect'] ) );
251 if ( isset( $show[
'unpatrolled'] ) ) {
253 if ( $user->useRCPatrol() ) {
254 $this->
addWhereFld(
'rc_patrolled', RecentChange::PRC_UNPATROLLED );
255 } elseif ( $user->useNPPatrol() ) {
256 $this->
addWhereFld(
'rc_patrolled', RecentChange::PRC_UNPATROLLED );
262 $db->expr(
'rc_patrolled',
'!=', RecentChange::PRC_AUTOPATROLLED ),
263 isset( $show[
'!autopatrolled'] )
266 [
'rc_patrolled' => RecentChange::PRC_AUTOPATROLLED ],
267 isset( $show[
'autopatrolled'] )
272 [
'page_is_redirect' => [ 0,
null ] ],
273 isset( $show[
'!redirect'] )
279 if (
$params[
'prop'] !==
null ) {
280 $prop = array_fill_keys(
$params[
'prop'],
true );
289 ||
$params[
'excludeuser'] !==
null
292 $this->
addFields( [
'actor_name',
'actor_user',
'rc_actor' ] );
293 $this->
addJoinConds( [
'actor' => [
'JOIN',
'actor_id=rc_actor' ] ] );
296 if (
$params[
'user'] !==
null ) {
300 if (
$params[
'excludeuser'] !==
null ) {
301 $this->
addWhere( $db->expr(
'actor_name',
'!=',
$params[
'excludeuser'] ) );
315 $showRedirects =
false;
317 if (
$params[
'prop'] !==
null ) {
318 if ( $this->fld_patrolled && !$user->useRCPatrol() && !$user->useNPPatrol() ) {
319 $this->
dieWithError(
'apierror-permissiondenied-patrolflag',
'permissiondenied' );
323 $this->
addFieldsIf( [
'rc_this_oldid',
'rc_last_oldid' ], $this->fld_ids );
324 $this->
addFieldsIf( [
'rc_minor',
'rc_type',
'rc_bot' ], $this->fld_flags );
325 $this->
addFieldsIf( [
'rc_old_len',
'rc_new_len' ], $this->fld_sizes );
326 $this->
addFieldsIf( [
'rc_patrolled',
'rc_log_type' ], $this->fld_patrolled );
328 [
'rc_logid',
'rc_log_type',
'rc_log_action',
'rc_params' ],
331 $showRedirects = $this->fld_redirect || isset( $show[
'redirect'] )
332 || isset( $show[
'!redirect'] );
335 $resultPageSet &&
$params[
'generaterevisions'] );
337 if ( $this->fld_tags ) {
340 ->makeTagSummarySubquery(
'recentchanges' )
344 if ( $this->fld_sha1 ) {
347 [
'rc_this_oldid=rev_id' ] ] ] );
348 $this->
addFields( [
'rev_sha1',
'rev_deleted' ] );
351 if (
$params[
'toponly'] || $showRedirects ) {
354 [
'rc_namespace=page_namespace',
'rc_title=page_title' ] ] ] );
358 $this->
addWhere(
'rc_this_oldid = page_latest' );
362 if (
$params[
'tag'] !==
null ) {
364 $this->
addJoinConds( [
'change_tag' => [
'JOIN', [
'rc_id=ct_rc_id' ] ] ] );
374 if (
$params[
'user'] !==
null ||
$params[
'excludeuser'] !==
null ) {
375 if ( !$this->
getAuthority()->isAllowed(
'deletedhistory' ) ) {
376 $bitmask = RevisionRecord::DELETED_USER;
377 } elseif ( !$this->
getAuthority()->isAllowedAny(
'suppressrevision',
'viewsuppressed' ) ) {
378 $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
383 $this->
addWhere( $db->bitAnd(
'rc_deleted', $bitmask ) .
" != $bitmask" );
386 if ( $this->
getRequest()->getCheck(
'namespace' ) ) {
388 if ( !$this->
getAuthority()->isAllowed(
'deletedhistory' ) ) {
389 $bitmask = LogPage::DELETED_ACTION;
390 } elseif ( !$this->
getAuthority()->isAllowedAny(
'suppressrevision',
'viewsuppressed' ) ) {
391 $bitmask = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED;
397 $db->expr(
'rc_type',
'!=',
RC_LOG )
398 ->orExpr(
new RawSQLExpression( $db->bitAnd(
'rc_deleted', $bitmask ) .
" != $bitmask" ) )
403 if ( $this->fld_comment || $this->fld_parsedcomment ) {
404 $commentQuery = $this->commentStore->getJoin(
'rc_comment' );
405 $this->
addTables( $commentQuery[
'tables'] );
406 $this->
addFields( $commentQuery[
'fields'] );
410 if (
$params[
'slot'] !==
null ) {
412 $slotId = $this->slotRoleStore->getId(
$params[
'slot'] );
413 }
catch ( Exception $e ) {
418 'slot' =>
'slots',
'parent_slot' =>
'slots'
421 'slot' => [
'LEFT JOIN', [
422 'rc_this_oldid = slot.slot_revision_id',
423 'slot.slot_role_id' => $slotId,
425 'parent_slot' => [
'LEFT JOIN', [
426 'rc_last_oldid = parent_slot.slot_revision_id',
427 'parent_slot.slot_role_id' => $slotId,
438 new RawSQLExpression(
'slot.slot_content_id != parent_slot.slot_content_id' ),
439 $db->expr(
'slot.slot_content_id',
'=',
null )->
and(
'parent_slot.slot_content_id',
'!=',
null ),
440 $db->expr(
'slot.slot_content_id',
'!=',
null )->and(
'parent_slot.slot_content_id',
'=',
null ),
443 $changeTypes = RecentChange::parseToRCType(
444 array_intersect(
$params[
'type'], [
'new',
'edit' ] )
446 if ( count( $changeTypes ) ) {
451 $this->
addWhere( [
'rc_type' =>
null ] );
457 'MAX_EXECUTION_TIME',
464 $res = $this->
select( __METHOD__, [], $hookData );
467 if ( $this->fld_title && $resultPageSet ===
null ) {
470 if ( $this->fld_parsedcomment ) {
471 $this->formattedComments = $this->commentFormatter->formatItems(
472 $this->commentFormatter->rows( $res )
473 ->indexField(
'rc_id' )
474 ->commentKey(
'rc_comment' )
475 ->namespaceField(
'rc_namespace' )
476 ->titleField(
'rc_title' )
486 foreach ( $res as $row ) {
487 if ( $count === 0 && $resultPageSet !==
null ) {
491 $this,
'continue',
"$row->rc_timestamp|$row->rc_id"
494 if ( ++$count >
$params[
'limit'] ) {
501 if ( $resultPageSet ===
null ) {
506 $fit = $this->
processRow( $row, $vals, $hookData ) &&
507 $result->addValue( [
'query', $this->
getModuleName() ],
null, $vals );
512 } elseif (
$params[
'generaterevisions'] ) {
513 $revid = (int)$row->rc_this_oldid;
518 $titles[] = Title::makeTitle( $row->rc_namespace, $row->rc_title );
522 if ( $resultPageSet ===
null ) {
524 $result->addIndexedTagName( [
'query', $this->
getModuleName() ],
'rc' );
525 } elseif (
$params[
'generaterevisions'] ) {
526 $resultPageSet->populateFromRevisionIDs( $revids );
528 $resultPageSet->populateFromTitles( $titles );
540 $title = Title::makeTitle( $row->rc_namespace, $row->rc_title );
546 $type = (int)$row->rc_type;
547 $vals[
'type'] = RecentChange::parseFromRCType( $type );
552 if ( $this->fld_title || $this->fld_ids ) {
553 if ( $type ===
RC_LOG && ( $row->rc_deleted & LogPage::DELETED_ACTION ) ) {
554 $vals[
'actionhidden'] =
true;
558 LogEventsList::userCanBitfield( $row->rc_deleted, LogPage::DELETED_ACTION, $user )
560 if ( $this->fld_title ) {
563 if ( $this->fld_ids ) {
564 $vals[
'pageid'] = (int)$row->rc_cur_id;
565 $vals[
'revid'] = (int)$row->rc_this_oldid;
566 $vals[
'old_revid'] = (int)$row->rc_last_oldid;
571 if ( $this->fld_ids ) {
572 $vals[
'rcid'] = (int)$row->rc_id;
576 if ( $this->fld_user || $this->fld_userid ) {
577 if ( $row->rc_deleted & RevisionRecord::DELETED_USER ) {
578 $vals[
'userhidden'] =
true;
581 if ( RevisionRecord::userCanBitfield( $row->rc_deleted, RevisionRecord::DELETED_USER, $user ) ) {
582 if ( $this->fld_user ) {
583 $vals[
'user'] = $row->actor_name;
586 if ( $this->fld_userid ) {
587 $vals[
'userid'] = (int)$row->actor_user;
590 if ( isset( $row->actor_name ) && $this->userNameUtils->isTemp( $row->actor_name ) ) {
591 $vals[
'temp'] =
true;
594 if ( !$row->actor_user ) {
595 $vals[
'anon'] =
true;
601 if ( $this->fld_flags ) {
602 $vals[
'bot'] = (bool)$row->rc_bot;
603 $vals[
'new'] = $row->rc_type ==
RC_NEW;
604 $vals[
'minor'] = (bool)$row->rc_minor;
608 if ( $this->fld_sizes ) {
609 $vals[
'oldlen'] = (int)$row->rc_old_len;
610 $vals[
'newlen'] = (int)$row->rc_new_len;
614 if ( $this->fld_timestamp ) {
615 $vals[
'timestamp'] =
wfTimestamp( TS_ISO_8601, $row->rc_timestamp );
619 if ( $this->fld_comment || $this->fld_parsedcomment ) {
620 if ( $row->rc_deleted & RevisionRecord::DELETED_COMMENT ) {
621 $vals[
'commenthidden'] =
true;
624 if ( RevisionRecord::userCanBitfield(
625 $row->rc_deleted, RevisionRecord::DELETED_COMMENT, $user
627 if ( $this->fld_comment ) {
628 $vals[
'comment'] = $this->commentStore->getComment(
'rc_comment', $row )->text;
631 if ( $this->fld_parsedcomment ) {
632 $vals[
'parsedcomment'] = $this->formattedComments[$row->rc_id];
637 if ( $this->fld_redirect ) {
638 $vals[
'redirect'] = (bool)$row->page_is_redirect;
642 if ( $this->fld_patrolled ) {
643 $vals[
'patrolled'] = $row->rc_patrolled != RecentChange::PRC_UNPATROLLED;
644 $vals[
'unpatrolled'] = ChangesList::isUnpatrolled( $row, $user );
645 $vals[
'autopatrolled'] = $row->rc_patrolled == RecentChange::PRC_AUTOPATROLLED;
648 if ( $this->fld_loginfo && $row->rc_type ==
RC_LOG ) {
649 if ( $row->rc_deleted & LogPage::DELETED_ACTION ) {
650 $vals[
'actionhidden'] =
true;
653 if ( LogEventsList::userCanBitfield( $row->rc_deleted, LogPage::DELETED_ACTION, $user ) ) {
654 $vals[
'logid'] = (int)$row->rc_logid;
655 $vals[
'logtype'] = $row->rc_log_type;
656 $vals[
'logaction'] = $row->rc_log_action;
657 $vals[
'logparams'] = $this->logFormatterFactory->newFromRow( $row )->formatParametersForApi();
661 if ( $this->fld_tags ) {
662 if ( $row->ts_tags ) {
663 $tags = explode(
',', $row->ts_tags );
665 $vals[
'tags'] = $tags;
671 if ( $this->fld_sha1 && $row->rev_sha1 !==
null ) {
672 if ( $row->rev_deleted & RevisionRecord::DELETED_TEXT ) {
673 $vals[
'sha1hidden'] =
true;
676 if ( RevisionRecord::userCanBitfield(
677 $row->rev_deleted, RevisionRecord::DELETED_TEXT, $user
679 if ( $row->rev_sha1 !==
'' ) {
680 $vals[
'sha1'] = \Wikimedia\base_convert( $row->rev_sha1, 36, 16, 40 );
687 if ( $anyHidden && ( $row->rc_deleted & RevisionRecord::DELETED_RESTRICTED ) ) {
688 $vals[
'suppressed'] =
true;
698 private function includesPatrollingFlags( array $flagsArray ) {
699 return isset( $flagsArray[
'patrolled'] ) ||
700 isset( $flagsArray[
'!patrolled'] ) ||
701 isset( $flagsArray[
'unpatrolled'] ) ||
702 isset( $flagsArray[
'autopatrolled'] ) ||
703 isset( $flagsArray[
'!autopatrolled'] );
707 if ( isset(
$params[
'show'] ) &&
708 $this->includesPatrollingFlags( array_fill_keys(
$params[
'show'],
true ) )
715 if (
$params[
'prop'] !==
null && in_array(
'parsedcomment',
$params[
'prop'] ) ) {
717 return 'anon-public-user-private';
724 $slotRoles = $this->slotRoleRegistry->getKnownRoles();
725 sort( $slotRoles, SORT_STRING );
729 ParamValidator::PARAM_TYPE =>
'timestamp'
732 ParamValidator::PARAM_TYPE =>
'timestamp'
735 ParamValidator::PARAM_DEFAULT =>
'older',
736 ParamValidator::PARAM_TYPE => [
742 'newer' =>
'api-help-paramvalue-direction-newer',
743 'older' =>
'api-help-paramvalue-direction-older',
747 ParamValidator::PARAM_ISMULTI =>
true,
748 ParamValidator::PARAM_TYPE =>
'namespace',
752 ParamValidator::PARAM_TYPE =>
'user',
753 UserDef::PARAM_ALLOWED_USER_TYPES => [
'name',
'ip',
'temp',
'id',
'interwiki' ],
756 ParamValidator::PARAM_TYPE =>
'user',
757 UserDef::PARAM_ALLOWED_USER_TYPES => [
'name',
'ip',
'temp',
'id',
'interwiki' ],
761 ParamValidator::PARAM_ISMULTI =>
true,
762 ParamValidator::PARAM_DEFAULT =>
'title|timestamp|ids',
763 ParamValidator::PARAM_TYPE => [
782 ParamValidator::PARAM_ISMULTI =>
true,
783 ParamValidator::PARAM_TYPE => [
800 ParamValidator::PARAM_DEFAULT => 10,
801 ParamValidator::PARAM_TYPE =>
'limit',
802 IntegerDef::PARAM_MIN => 1,
807 ParamValidator::PARAM_DEFAULT =>
'edit|new|log|categorize',
808 ParamValidator::PARAM_ISMULTI =>
true,
809 ParamValidator::PARAM_TYPE => RecentChange::getChangeTypes()
816 'generaterevisions' =>
false,
818 ParamValidator::PARAM_TYPE => $slotRoles
825 'action=query&list=recentchanges'
826 =>
'apihelp-query+recentchanges-example-simple',
827 'action=query&generator=recentchanges&grcshow=!patrolled&prop=info'
828 =>
'apihelp-query+recentchanges-example-generator',
833 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Recentchanges';
838class_alias( ApiQueryRecentChanges::class,
'ApiQueryRecentChanges' );
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.
Base class for lists of recent changes shown on special pages.
Class to simplify the use of log pages.
A class containing constants representing the names of configuration variables.
const MaxExecutionTimeForExpensiveQueries
Name constant for the MaxExecutionTimeForExpensiveQueries setting, for use with Config::get()
Utility class for creating and reading rows in the recentchanges table.