70 private $formattedComments = [];
85 parent::__construct( $query, $moduleName,
'rc' );
86 $this->commentStore = $commentStore;
87 $this->commentFormatter = $commentFormatter;
88 $this->changeTagDefStore = $changeTagDefStore;
89 $this->changeTagsStore = $changeTagsStore;
90 $this->slotRoleStore = $slotRoleStore;
91 $this->slotRoleRegistry = $slotRoleRegistry;
92 $this->userNameUtils = $userNameUtils;
93 $this->tempUserConfig = $tempUserConfig;
94 $this->logFormatterFactory = $logFormatterFactory;
97 private bool $fld_comment =
false;
98 private bool $fld_parsedcomment =
false;
99 private bool $fld_user =
false;
100 private bool $fld_userid =
false;
101 private bool $fld_flags =
false;
102 private bool $fld_timestamp =
false;
103 private bool $fld_title =
false;
104 private bool $fld_ids =
false;
105 private bool $fld_sizes =
false;
106 private bool $fld_redirect =
false;
107 private bool $fld_patrolled =
false;
108 private bool $fld_loginfo =
false;
109 private bool $fld_tags =
false;
110 private bool $fld_sha1 =
false;
117 $this->fld_comment = isset( $prop[
'comment'] );
118 $this->fld_parsedcomment = isset( $prop[
'parsedcomment'] );
119 $this->fld_user = isset( $prop[
'user'] );
120 $this->fld_userid = isset( $prop[
'userid'] );
121 $this->fld_flags = isset( $prop[
'flags'] );
122 $this->fld_timestamp = isset( $prop[
'timestamp'] );
123 $this->fld_title = isset( $prop[
'title'] );
124 $this->fld_ids = isset( $prop[
'ids'] );
125 $this->fld_sizes = isset( $prop[
'sizes'] );
126 $this->fld_redirect = isset( $prop[
'redirect'] );
127 $this->fld_patrolled = isset( $prop[
'patrolled'] );
128 $this->fld_loginfo = isset( $prop[
'loginfo'] );
129 $this->fld_tags = isset( $prop[
'tags'] );
130 $this->fld_sha1 = isset( $prop[
'sha1'] );
138 $this->
run( $resultPageSet );
146 public function run( $resultPageSet =
null ) {
147 $db = $this->
getDB();
159 if (
$params[
'continue'] !==
null ) {
161 $op =
$params[
'dir'] ===
'older' ?
'<=' :
'>=';
162 $this->
addWhere( $db->buildComparison( $op, [
163 'rc_timestamp' => $db->timestamp( $cont[0] ),
168 $order =
$params[
'dir'] ===
'older' ?
'DESC' :
'ASC';
170 "rc_timestamp $order",
174 if (
$params[
'type'] !==
null ) {
179 if ( $title !==
null ) {
180 $titleObj = Title::newFromText( $title );
181 if ( $titleObj ===
null || $titleObj->isExternal() ) {
183 } elseif (
$params[
'namespace'] && !in_array( $titleObj->getNamespace(),
$params[
'namespace'] ) ) {
186 $this->
addWhereFld(
'rc_namespace', $titleObj->getNamespace() );
187 $this->
addWhereFld(
'rc_title', $titleObj->getDBkey() );
192 if (
$params[
'show'] !==
null ) {
193 $show = array_fill_keys(
$params[
'show'],
true );
196 if ( ( isset( $show[
'minor'] ) && isset( $show[
'!minor'] ) )
197 || ( isset( $show[
'bot'] ) && isset( $show[
'!bot'] ) )
198 || ( isset( $show[
'anon'] ) && isset( $show[
'!anon'] ) )
199 || ( isset( $show[
'redirect'] ) && isset( $show[
'!redirect'] ) )
200 || ( isset( $show[
'patrolled'] ) && isset( $show[
'!patrolled'] ) )
201 || ( isset( $show[
'patrolled'] ) && isset( $show[
'unpatrolled'] ) )
202 || ( isset( $show[
'!patrolled'] ) && isset( $show[
'unpatrolled'] ) )
203 || ( isset( $show[
'autopatrolled'] ) && isset( $show[
'!autopatrolled'] ) )
204 || ( isset( $show[
'autopatrolled'] ) && isset( $show[
'unpatrolled'] ) )
205 || ( isset( $show[
'autopatrolled'] ) && isset( $show[
'!patrolled'] ) )
211 if ( $this->includesPatrollingFlags( $show ) && !$user->useRCPatrol() && !$user->useNPPatrol() ) {
212 $this->
dieWithError(
'apierror-permissiondenied-patrolflag',
'permissiondenied' );
216 $this->
addWhereIf( [
'rc_minor' => 0 ], isset( $show[
'!minor'] ) );
217 $this->
addWhereIf( $db->expr(
'rc_minor',
'!=', 0 ), isset( $show[
'minor'] ) );
218 $this->
addWhereIf( [
'rc_bot' => 0 ], isset( $show[
'!bot'] ) );
219 $this->
addWhereIf( $db->expr(
'rc_bot',
'!=', 0 ), isset( $show[
'bot'] ) );
220 if ( isset( $show[
'anon'] ) || isset( $show[
'!anon'] ) ) {
222 $this->
addJoinConds( [
'actor' => [
'JOIN',
'actor_id=rc_actor' ] ] );
224 if ( $this->tempUserConfig->isKnown() ) {
225 $isAnon = isset( $show[
'anon'] );
226 $anonExpr = $db->expr(
'actor_user', $isAnon ?
'=' :
'!=', null );
228 $anonExpr = $anonExpr->orExpr( $this->tempUserConfig->getMatchCondition(
234 $anonExpr = $anonExpr->andExpr( $this->tempUserConfig->getMatchCondition(
237 IExpression::NOT_LIKE
243 [
'actor_user' =>
null ], isset( $show[
'anon'] )
246 $db->expr(
'actor_user',
'!=',
null ), isset( $show[
'!anon'] )
250 $this->
addWhereIf( [
'rc_patrolled' => 0 ], isset( $show[
'!patrolled'] ) );
251 $this->
addWhereIf( $db->expr(
'rc_patrolled',
'!=', 0 ), isset( $show[
'patrolled'] ) );
252 $this->
addWhereIf( [
'page_is_redirect' => 1 ], isset( $show[
'redirect'] ) );
254 if ( isset( $show[
'unpatrolled'] ) ) {
256 if ( $user->useRCPatrol() ) {
257 $this->
addWhereFld(
'rc_patrolled', RecentChange::PRC_UNPATROLLED );
258 } elseif ( $user->useNPPatrol() ) {
259 $this->
addWhereFld(
'rc_patrolled', RecentChange::PRC_UNPATROLLED );
265 $db->expr(
'rc_patrolled',
'!=', RecentChange::PRC_AUTOPATROLLED ),
266 isset( $show[
'!autopatrolled'] )
269 [
'rc_patrolled' => RecentChange::PRC_AUTOPATROLLED ],
270 isset( $show[
'autopatrolled'] )
275 [
'page_is_redirect' => [ 0,
null ] ],
276 isset( $show[
'!redirect'] )
282 if (
$params[
'prop'] !==
null ) {
283 $prop = array_fill_keys(
$params[
'prop'],
true );
292 ||
$params[
'excludeuser'] !==
null
295 $this->
addFields( [
'actor_name',
'actor_user',
'rc_actor' ] );
296 $this->
addJoinConds( [
'actor' => [
'JOIN',
'actor_id=rc_actor' ] ] );
299 if (
$params[
'user'] !==
null ) {
303 if (
$params[
'excludeuser'] !==
null ) {
304 $this->
addWhere( $db->expr(
'actor_name',
'!=',
$params[
'excludeuser'] ) );
318 $showRedirects =
false;
320 if (
$params[
'prop'] !==
null ) {
321 if ( $this->fld_patrolled && !$user->useRCPatrol() && !$user->useNPPatrol() ) {
322 $this->
dieWithError(
'apierror-permissiondenied-patrolflag',
'permissiondenied' );
326 $this->
addFieldsIf( [
'rc_this_oldid',
'rc_last_oldid' ], $this->fld_ids );
327 $this->
addFieldsIf( [
'rc_minor',
'rc_type',
'rc_bot' ], $this->fld_flags );
328 $this->
addFieldsIf( [
'rc_old_len',
'rc_new_len' ], $this->fld_sizes );
329 $this->
addFieldsIf( [
'rc_patrolled',
'rc_log_type' ], $this->fld_patrolled );
331 [
'rc_logid',
'rc_log_type',
'rc_log_action',
'rc_params' ],
334 $showRedirects = $this->fld_redirect || isset( $show[
'redirect'] )
335 || isset( $show[
'!redirect'] );
338 $resultPageSet &&
$params[
'generaterevisions'] );
340 if ( $this->fld_tags ) {
342 'ts_tags' => $this->changeTagsStore->makeTagSummarySubquery(
'recentchanges' )
346 if ( $this->fld_sha1 ) {
349 [
'rc_this_oldid=rev_id' ] ] ] );
350 $this->
addFields( [
'rev_sha1',
'rev_deleted' ] );
353 if (
$params[
'toponly'] || $showRedirects ) {
356 [
'rc_namespace=page_namespace',
'rc_title=page_title' ] ] ] );
360 $this->
addWhere(
'rc_this_oldid = page_latest' );
364 if (
$params[
'tag'] !==
null ) {
366 $this->
addJoinConds( [
'change_tag' => [
'JOIN', [
'rc_id=ct_rc_id' ] ] ] );
376 if (
$params[
'user'] !==
null ||
$params[
'excludeuser'] !==
null ) {
377 if ( !$this->
getAuthority()->isAllowed(
'deletedhistory' ) ) {
378 $bitmask = RevisionRecord::DELETED_USER;
379 } elseif ( !$this->
getAuthority()->isAllowedAny(
'suppressrevision',
'viewsuppressed' ) ) {
380 $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
385 $this->
addWhere( $db->bitAnd(
'rc_deleted', $bitmask ) .
" != $bitmask" );
388 if ( $this->
getRequest()->getCheck(
'namespace' ) ) {
390 if ( !$this->
getAuthority()->isAllowed(
'deletedhistory' ) ) {
391 $bitmask = LogPage::DELETED_ACTION;
392 } elseif ( !$this->
getAuthority()->isAllowedAny(
'suppressrevision',
'viewsuppressed' ) ) {
393 $bitmask = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED;
399 $db->expr(
'rc_type',
'!=',
RC_LOG )
400 ->orExpr(
new RawSQLExpression( $db->bitAnd(
'rc_deleted', $bitmask ) .
" != $bitmask" ) )
405 if ( $this->fld_comment || $this->fld_parsedcomment ) {
406 $commentQuery = $this->commentStore->getJoin(
'rc_comment' );
407 $this->
addTables( $commentQuery[
'tables'] );
408 $this->
addFields( $commentQuery[
'fields'] );
412 if (
$params[
'slot'] !==
null ) {
414 $slotId = $this->slotRoleStore->getId(
$params[
'slot'] );
415 }
catch ( Exception $e ) {
420 'slot' =>
'slots',
'parent_slot' =>
'slots'
423 'slot' => [
'LEFT JOIN', [
424 'rc_this_oldid = slot.slot_revision_id',
425 'slot.slot_role_id' => $slotId,
427 'parent_slot' => [
'LEFT JOIN', [
428 'rc_last_oldid = parent_slot.slot_revision_id',
429 'parent_slot.slot_role_id' => $slotId,
440 new RawSQLExpression(
'slot.slot_content_id != parent_slot.slot_content_id' ),
441 $db->expr(
'slot.slot_content_id',
'=',
null )->
and(
'parent_slot.slot_content_id',
'!=',
null ),
442 $db->expr(
'slot.slot_content_id',
'!=',
null )->and(
'parent_slot.slot_content_id',
'=',
null ),
445 $changeTypes = RecentChange::parseToRCType(
446 array_intersect(
$params[
'type'], [
'new',
'edit' ] )
448 if ( count( $changeTypes ) ) {
453 $this->
addWhere( [
'rc_type' =>
null ] );
459 'MAX_EXECUTION_TIME',
466 $res = $this->
select( __METHOD__, [], $hookData );
469 if ( $this->fld_title && $resultPageSet ===
null ) {
472 if ( $this->fld_parsedcomment ) {
473 $this->formattedComments = $this->commentFormatter->formatItems(
474 $this->commentFormatter->rows( $res )
475 ->indexField(
'rc_id' )
476 ->commentKey(
'rc_comment' )
477 ->namespaceField(
'rc_namespace' )
478 ->titleField(
'rc_title' )
488 foreach ( $res as $row ) {
489 if ( $count === 0 && $resultPageSet !==
null ) {
493 $this,
'continue',
"$row->rc_timestamp|$row->rc_id"
496 if ( ++$count >
$params[
'limit'] ) {
503 if ( $resultPageSet ===
null ) {
508 $fit = $this->
processRow( $row, $vals, $hookData ) &&
509 $result->addValue( [
'query', $this->
getModuleName() ],
null, $vals );
514 } elseif (
$params[
'generaterevisions'] ) {
515 $revid = (int)$row->rc_this_oldid;
520 $titles[] = Title::makeTitle( $row->rc_namespace, $row->rc_title );
524 if ( $resultPageSet ===
null ) {
526 $result->addIndexedTagName( [
'query', $this->
getModuleName() ],
'rc' );
527 } elseif (
$params[
'generaterevisions'] ) {
528 $resultPageSet->populateFromRevisionIDs( $revids );
530 $resultPageSet->populateFromTitles( $titles );
542 $title = Title::makeTitle( $row->rc_namespace, $row->rc_title );
548 $type = (int)$row->rc_type;
549 $vals[
'type'] = RecentChange::parseFromRCType( $type );
554 if ( $this->fld_title || $this->fld_ids ) {
555 if ( $type ===
RC_LOG && ( $row->rc_deleted & LogPage::DELETED_ACTION ) ) {
556 $vals[
'actionhidden'] =
true;
560 LogEventsList::userCanBitfield( $row->rc_deleted, LogPage::DELETED_ACTION, $user )
562 if ( $this->fld_title ) {
565 if ( $this->fld_ids ) {
566 $vals[
'pageid'] = (int)$row->rc_cur_id;
567 $vals[
'revid'] = (int)$row->rc_this_oldid;
568 $vals[
'old_revid'] = (int)$row->rc_last_oldid;
573 if ( $this->fld_ids ) {
574 $vals[
'rcid'] = (int)$row->rc_id;
578 if ( $this->fld_user || $this->fld_userid ) {
579 if ( $row->rc_deleted & RevisionRecord::DELETED_USER ) {
580 $vals[
'userhidden'] =
true;
583 if ( RevisionRecord::userCanBitfield( $row->rc_deleted, RevisionRecord::DELETED_USER, $user ) ) {
584 if ( $this->fld_user ) {
585 $vals[
'user'] = $row->actor_name;
588 if ( $this->fld_userid ) {
589 $vals[
'userid'] = (int)$row->actor_user;
592 if ( isset( $row->actor_name ) && $this->userNameUtils->isTemp( $row->actor_name ) ) {
593 $vals[
'temp'] =
true;
596 if ( !$row->actor_user ) {
597 $vals[
'anon'] =
true;
603 if ( $this->fld_flags ) {
604 $vals[
'bot'] = (bool)$row->rc_bot;
605 $vals[
'new'] = $row->rc_type ==
RC_NEW;
606 $vals[
'minor'] = (bool)$row->rc_minor;
610 if ( $this->fld_sizes ) {
611 $vals[
'oldlen'] = (int)$row->rc_old_len;
612 $vals[
'newlen'] = (int)$row->rc_new_len;
616 if ( $this->fld_timestamp ) {
617 $vals[
'timestamp'] =
wfTimestamp( TS_ISO_8601, $row->rc_timestamp );
621 if ( $this->fld_comment || $this->fld_parsedcomment ) {
622 if ( $row->rc_deleted & RevisionRecord::DELETED_COMMENT ) {
623 $vals[
'commenthidden'] =
true;
626 if ( RevisionRecord::userCanBitfield(
627 $row->rc_deleted, RevisionRecord::DELETED_COMMENT, $user
629 if ( $this->fld_comment ) {
630 $vals[
'comment'] = $this->commentStore->getComment(
'rc_comment', $row )->text;
633 if ( $this->fld_parsedcomment ) {
634 $vals[
'parsedcomment'] = $this->formattedComments[$row->rc_id];
639 if ( $this->fld_redirect ) {
640 $vals[
'redirect'] = (bool)$row->page_is_redirect;
644 if ( $this->fld_patrolled ) {
645 $vals[
'patrolled'] = $row->rc_patrolled != RecentChange::PRC_UNPATROLLED;
646 $vals[
'unpatrolled'] = ChangesList::isUnpatrolled( $row, $user );
647 $vals[
'autopatrolled'] = $row->rc_patrolled == RecentChange::PRC_AUTOPATROLLED;
650 if ( $this->fld_loginfo && $row->rc_type ==
RC_LOG ) {
651 if ( $row->rc_deleted & LogPage::DELETED_ACTION ) {
652 $vals[
'actionhidden'] =
true;
655 if ( LogEventsList::userCanBitfield( $row->rc_deleted, LogPage::DELETED_ACTION, $user ) ) {
656 $vals[
'logid'] = (int)$row->rc_logid;
657 $vals[
'logtype'] = $row->rc_log_type;
658 $vals[
'logaction'] = $row->rc_log_action;
659 $vals[
'logparams'] = $this->logFormatterFactory->newFromRow( $row )->formatParametersForApi();
663 if ( $this->fld_tags ) {
664 if ( $row->ts_tags ) {
665 $tags = explode(
',', $row->ts_tags );
667 $vals[
'tags'] = $tags;
673 if ( $this->fld_sha1 && $row->rev_sha1 !==
null ) {
674 if ( $row->rev_deleted & RevisionRecord::DELETED_TEXT ) {
675 $vals[
'sha1hidden'] =
true;
678 if ( RevisionRecord::userCanBitfield(
679 $row->rev_deleted, RevisionRecord::DELETED_TEXT, $user
681 if ( $row->rev_sha1 !==
'' ) {
682 $vals[
'sha1'] = \Wikimedia\base_convert( $row->rev_sha1, 36, 16, 40 );
689 if ( $anyHidden && ( $row->rc_deleted & RevisionRecord::DELETED_RESTRICTED ) ) {
690 $vals[
'suppressed'] =
true;
700 private function includesPatrollingFlags( array $flagsArray ) {
701 return isset( $flagsArray[
'patrolled'] ) ||
702 isset( $flagsArray[
'!patrolled'] ) ||
703 isset( $flagsArray[
'unpatrolled'] ) ||
704 isset( $flagsArray[
'autopatrolled'] ) ||
705 isset( $flagsArray[
'!autopatrolled'] );
709 if ( isset(
$params[
'show'] ) &&
710 $this->includesPatrollingFlags( array_fill_keys(
$params[
'show'],
true ) )
717 if (
$params[
'prop'] !==
null && in_array(
'parsedcomment',
$params[
'prop'] ) ) {
719 return 'anon-public-user-private';
726 $slotRoles = $this->slotRoleRegistry->getKnownRoles();
727 sort( $slotRoles, SORT_STRING );
731 ParamValidator::PARAM_TYPE =>
'timestamp'
734 ParamValidator::PARAM_TYPE =>
'timestamp'
737 ParamValidator::PARAM_DEFAULT =>
'older',
738 ParamValidator::PARAM_TYPE => [
744 'newer' =>
'api-help-paramvalue-direction-newer',
745 'older' =>
'api-help-paramvalue-direction-older',
749 ParamValidator::PARAM_ISMULTI =>
true,
750 ParamValidator::PARAM_TYPE =>
'namespace',
754 ParamValidator::PARAM_TYPE =>
'user',
755 UserDef::PARAM_ALLOWED_USER_TYPES => [
'name',
'ip',
'temp',
'id',
'interwiki' ],
758 ParamValidator::PARAM_TYPE =>
'user',
759 UserDef::PARAM_ALLOWED_USER_TYPES => [
'name',
'ip',
'temp',
'id',
'interwiki' ],
763 ParamValidator::PARAM_ISMULTI =>
true,
764 ParamValidator::PARAM_DEFAULT =>
'title|timestamp|ids',
765 ParamValidator::PARAM_TYPE => [
784 ParamValidator::PARAM_ISMULTI =>
true,
785 ParamValidator::PARAM_TYPE => [
802 ParamValidator::PARAM_DEFAULT => 10,
803 ParamValidator::PARAM_TYPE =>
'limit',
804 IntegerDef::PARAM_MIN => 1,
809 ParamValidator::PARAM_DEFAULT =>
'edit|new|log|categorize',
810 ParamValidator::PARAM_ISMULTI =>
true,
811 ParamValidator::PARAM_TYPE => RecentChange::getChangeTypes()
818 'generaterevisions' =>
false,
820 ParamValidator::PARAM_TYPE => $slotRoles
827 'action=query&list=recentchanges'
828 =>
'apihelp-query+recentchanges-example-simple',
829 'action=query&generator=recentchanges&grcshow=!patrolled&prop=info'
830 =>
'apihelp-query+recentchanges-example-generator',
835 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Recentchanges';
840class_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.