69 private $formattedComments = [];
95 parent::__construct( $query, $moduleName,
'rc' );
96 $this->commentStore = $commentStore;
97 $this->commentFormatter = $commentFormatter;
98 $this->changeTagDefStore = $changeTagDefStore;
99 $this->slotRoleStore = $slotRoleStore;
100 $this->slotRoleRegistry = $slotRoleRegistry;
101 $this->userNameUtils = $userNameUtils;
102 $this->tempUserConfig = $tempUserConfig;
103 $this->logFormatterFactory = $logFormatterFactory;
106 private bool $fld_comment =
false;
107 private bool $fld_parsedcomment =
false;
108 private bool $fld_user =
false;
109 private bool $fld_userid =
false;
110 private bool $fld_flags =
false;
111 private bool $fld_timestamp =
false;
112 private bool $fld_title =
false;
113 private bool $fld_ids =
false;
114 private bool $fld_sizes =
false;
115 private bool $fld_redirect =
false;
116 private bool $fld_patrolled =
false;
117 private bool $fld_loginfo =
false;
118 private bool $fld_tags =
false;
119 private bool $fld_sha1 =
false;
126 $this->fld_comment = isset( $prop[
'comment'] );
127 $this->fld_parsedcomment = isset( $prop[
'parsedcomment'] );
128 $this->fld_user = isset( $prop[
'user'] );
129 $this->fld_userid = isset( $prop[
'userid'] );
130 $this->fld_flags = isset( $prop[
'flags'] );
131 $this->fld_timestamp = isset( $prop[
'timestamp'] );
132 $this->fld_title = isset( $prop[
'title'] );
133 $this->fld_ids = isset( $prop[
'ids'] );
134 $this->fld_sizes = isset( $prop[
'sizes'] );
135 $this->fld_redirect = isset( $prop[
'redirect'] );
136 $this->fld_patrolled = isset( $prop[
'patrolled'] );
137 $this->fld_loginfo = isset( $prop[
'loginfo'] );
138 $this->fld_tags = isset( $prop[
'tags'] );
139 $this->fld_sha1 = isset( $prop[
'sha1'] );
147 $this->
run( $resultPageSet );
155 public function run( $resultPageSet =
null ) {
156 $db = $this->
getDB();
168 if (
$params[
'continue'] !==
null ) {
170 $op =
$params[
'dir'] ===
'older' ?
'<=' :
'>=';
171 $this->
addWhere( $db->buildComparison( $op, [
172 'rc_timestamp' => $db->timestamp( $cont[0] ),
177 $order =
$params[
'dir'] ===
'older' ?
'DESC' :
'ASC';
179 "rc_timestamp $order",
183 if (
$params[
'type'] !==
null ) {
188 if ( $title !==
null ) {
189 $titleObj = Title::newFromText( $title );
190 if ( $titleObj ===
null || $titleObj->isExternal() ) {
192 } elseif (
$params[
'namespace'] && !in_array( $titleObj->getNamespace(),
$params[
'namespace'] ) ) {
195 $this->
addWhereFld(
'rc_namespace', $titleObj->getNamespace() );
196 $this->
addWhereFld(
'rc_title', $titleObj->getDBkey() );
201 if (
$params[
'show'] !==
null ) {
202 $show = array_fill_keys(
$params[
'show'],
true );
205 if ( ( isset( $show[
'minor'] ) && isset( $show[
'!minor'] ) )
206 || ( isset( $show[
'bot'] ) && isset( $show[
'!bot'] ) )
207 || ( isset( $show[
'anon'] ) && isset( $show[
'!anon'] ) )
208 || ( isset( $show[
'redirect'] ) && isset( $show[
'!redirect'] ) )
209 || ( isset( $show[
'patrolled'] ) && isset( $show[
'!patrolled'] ) )
210 || ( isset( $show[
'patrolled'] ) && isset( $show[
'unpatrolled'] ) )
211 || ( isset( $show[
'!patrolled'] ) && isset( $show[
'unpatrolled'] ) )
212 || ( isset( $show[
'autopatrolled'] ) && isset( $show[
'!autopatrolled'] ) )
213 || ( isset( $show[
'autopatrolled'] ) && isset( $show[
'unpatrolled'] ) )
214 || ( isset( $show[
'autopatrolled'] ) && isset( $show[
'!patrolled'] ) )
220 if ( $this->includesPatrollingFlags( $show ) && !$user->useRCPatrol() && !$user->useNPPatrol() ) {
221 $this->
dieWithError(
'apierror-permissiondenied-patrolflag',
'permissiondenied' );
225 $this->
addWhereIf( [
'rc_minor' => 0 ], isset( $show[
'!minor'] ) );
226 $this->
addWhereIf( $db->expr(
'rc_minor',
'!=', 0 ), isset( $show[
'minor'] ) );
227 $this->
addWhereIf( [
'rc_bot' => 0 ], isset( $show[
'!bot'] ) );
228 $this->
addWhereIf( $db->expr(
'rc_bot',
'!=', 0 ), isset( $show[
'bot'] ) );
229 if ( isset( $show[
'anon'] ) || isset( $show[
'!anon'] ) ) {
231 $this->
addJoinConds( [
'actor' => [
'JOIN',
'actor_id=rc_actor' ] ] );
233 if ( $this->tempUserConfig->isKnown() ) {
234 $isAnon = isset( $show[
'anon'] );
235 $anonExpr = $db->expr(
'actor_user', $isAnon ?
'=' :
'!=', null );
237 $anonExpr = $anonExpr->orExpr( $this->tempUserConfig->getMatchCondition(
243 $anonExpr = $anonExpr->andExpr( $this->tempUserConfig->getMatchCondition(
246 IExpression::NOT_LIKE
252 [
'actor_user' =>
null ], isset( $show[
'anon'] )
255 $db->expr(
'actor_user',
'!=',
null ), isset( $show[
'!anon'] )
259 $this->
addWhereIf( [
'rc_patrolled' => 0 ], isset( $show[
'!patrolled'] ) );
260 $this->
addWhereIf( $db->expr(
'rc_patrolled',
'!=', 0 ), isset( $show[
'patrolled'] ) );
261 $this->
addWhereIf( [
'page_is_redirect' => 1 ], isset( $show[
'redirect'] ) );
263 if ( isset( $show[
'unpatrolled'] ) ) {
265 if ( $user->useRCPatrol() ) {
266 $this->
addWhereFld(
'rc_patrolled', RecentChange::PRC_UNPATROLLED );
267 } elseif ( $user->useNPPatrol() ) {
268 $this->
addWhereFld(
'rc_patrolled', RecentChange::PRC_UNPATROLLED );
274 $db->expr(
'rc_patrolled',
'!=', RecentChange::PRC_AUTOPATROLLED ),
275 isset( $show[
'!autopatrolled'] )
278 [
'rc_patrolled' => RecentChange::PRC_AUTOPATROLLED ],
279 isset( $show[
'autopatrolled'] )
284 [
'page_is_redirect' => [ 0,
null ] ],
285 isset( $show[
'!redirect'] )
291 if (
$params[
'prop'] !==
null ) {
292 $prop = array_fill_keys(
$params[
'prop'],
true );
301 ||
$params[
'excludeuser'] !==
null
304 $this->
addFields( [
'actor_name',
'actor_user',
'rc_actor' ] );
305 $this->
addJoinConds( [
'actor' => [
'JOIN',
'actor_id=rc_actor' ] ] );
308 if (
$params[
'user'] !==
null ) {
312 if (
$params[
'excludeuser'] !==
null ) {
313 $this->
addWhere( $db->expr(
'actor_name',
'!=',
$params[
'excludeuser'] ) );
327 $showRedirects =
false;
329 if (
$params[
'prop'] !==
null ) {
330 if ( $this->fld_patrolled && !$user->useRCPatrol() && !$user->useNPPatrol() ) {
331 $this->
dieWithError(
'apierror-permissiondenied-patrolflag',
'permissiondenied' );
335 $this->
addFieldsIf( [
'rc_this_oldid',
'rc_last_oldid' ], $this->fld_ids );
336 $this->
addFieldsIf( [
'rc_minor',
'rc_type',
'rc_bot' ], $this->fld_flags );
337 $this->
addFieldsIf( [
'rc_old_len',
'rc_new_len' ], $this->fld_sizes );
338 $this->
addFieldsIf( [
'rc_patrolled',
'rc_log_type' ], $this->fld_patrolled );
340 [
'rc_logid',
'rc_log_type',
'rc_log_action',
'rc_params' ],
343 $showRedirects = $this->fld_redirect || isset( $show[
'redirect'] )
344 || isset( $show[
'!redirect'] );
347 $resultPageSet &&
$params[
'generaterevisions'] );
349 if ( $this->fld_tags ) {
353 if ( $this->fld_sha1 ) {
356 [
'rc_this_oldid=rev_id' ] ] ] );
357 $this->
addFields( [
'rev_sha1',
'rev_deleted' ] );
360 if (
$params[
'toponly'] || $showRedirects ) {
363 [
'rc_namespace=page_namespace',
'rc_title=page_title' ] ] ] );
367 $this->
addWhere(
'rc_this_oldid = page_latest' );
371 if (
$params[
'tag'] !==
null ) {
373 $this->
addJoinConds( [
'change_tag' => [
'JOIN', [
'rc_id=ct_rc_id' ] ] ] );
383 if (
$params[
'user'] !==
null ||
$params[
'excludeuser'] !==
null ) {
384 if ( !$this->
getAuthority()->isAllowed(
'deletedhistory' ) ) {
385 $bitmask = RevisionRecord::DELETED_USER;
386 } elseif ( !$this->
getAuthority()->isAllowedAny(
'suppressrevision',
'viewsuppressed' ) ) {
387 $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
392 $this->
addWhere( $db->bitAnd(
'rc_deleted', $bitmask ) .
" != $bitmask" );
395 if ( $this->
getRequest()->getCheck(
'namespace' ) ) {
397 if ( !$this->
getAuthority()->isAllowed(
'deletedhistory' ) ) {
398 $bitmask = LogPage::DELETED_ACTION;
399 } elseif ( !$this->
getAuthority()->isAllowedAny(
'suppressrevision',
'viewsuppressed' ) ) {
400 $bitmask = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED;
406 $db->expr(
'rc_type',
'!=',
RC_LOG )
407 ->orExpr(
new RawSQLExpression( $db->bitAnd(
'rc_deleted', $bitmask ) .
" != $bitmask" ) )
412 if ( $this->fld_comment || $this->fld_parsedcomment ) {
413 $commentQuery = $this->commentStore->getJoin(
'rc_comment' );
414 $this->
addTables( $commentQuery[
'tables'] );
415 $this->
addFields( $commentQuery[
'fields'] );
419 if (
$params[
'slot'] !==
null ) {
421 $slotId = $this->slotRoleStore->getId(
$params[
'slot'] );
422 }
catch ( Exception $e ) {
427 'slot' =>
'slots',
'parent_slot' =>
'slots'
430 'slot' => [
'LEFT JOIN', [
431 'rc_this_oldid = slot.slot_revision_id',
432 'slot.slot_role_id' => $slotId,
434 'parent_slot' => [
'LEFT JOIN', [
435 'rc_last_oldid = parent_slot.slot_revision_id',
436 'parent_slot.slot_role_id' => $slotId,
447 new RawSQLExpression(
'slot.slot_content_id != parent_slot.slot_content_id' ),
448 $db->expr(
'slot.slot_content_id',
'=',
null )->
and(
'parent_slot.slot_content_id',
'!=',
null ),
449 $db->expr(
'slot.slot_content_id',
'!=',
null )->and(
'parent_slot.slot_content_id',
'=',
null ),
452 $changeTypes = RecentChange::parseToRCType(
453 array_intersect(
$params[
'type'], [
'new',
'edit' ] )
455 if ( count( $changeTypes ) ) {
460 $this->
addWhere( [
'rc_type' =>
null ] );
466 'MAX_EXECUTION_TIME',
473 $res = $this->
select( __METHOD__, [], $hookData );
476 if ( $this->fld_title && $resultPageSet ===
null ) {
479 if ( $this->fld_parsedcomment ) {
480 $this->formattedComments = $this->commentFormatter->formatItems(
481 $this->commentFormatter->rows( $res )
482 ->indexField(
'rc_id' )
483 ->commentKey(
'rc_comment' )
484 ->namespaceField(
'rc_namespace' )
485 ->titleField(
'rc_title' )
495 foreach ( $res as $row ) {
496 if ( $count === 0 && $resultPageSet !==
null ) {
500 $this,
'continue',
"$row->rc_timestamp|$row->rc_id"
503 if ( ++$count >
$params[
'limit'] ) {
510 if ( $resultPageSet ===
null ) {
515 $fit = $this->
processRow( $row, $vals, $hookData ) &&
516 $result->addValue( [
'query', $this->
getModuleName() ],
null, $vals );
521 } elseif (
$params[
'generaterevisions'] ) {
522 $revid = (int)$row->rc_this_oldid;
527 $titles[] = Title::makeTitle( $row->rc_namespace, $row->rc_title );
531 if ( $resultPageSet ===
null ) {
533 $result->addIndexedTagName( [
'query', $this->
getModuleName() ],
'rc' );
534 } elseif (
$params[
'generaterevisions'] ) {
535 $resultPageSet->populateFromRevisionIDs( $revids );
537 $resultPageSet->populateFromTitles( $titles );
549 $title = Title::makeTitle( $row->rc_namespace, $row->rc_title );
555 $type = (int)$row->rc_type;
556 $vals[
'type'] = RecentChange::parseFromRCType( $type );
561 if ( $this->fld_title || $this->fld_ids ) {
562 if ( $type ===
RC_LOG && ( $row->rc_deleted & LogPage::DELETED_ACTION ) ) {
563 $vals[
'actionhidden'] =
true;
567 LogEventsList::userCanBitfield( $row->rc_deleted, LogPage::DELETED_ACTION, $user )
569 if ( $this->fld_title ) {
572 if ( $this->fld_ids ) {
573 $vals[
'pageid'] = (int)$row->rc_cur_id;
574 $vals[
'revid'] = (int)$row->rc_this_oldid;
575 $vals[
'old_revid'] = (int)$row->rc_last_oldid;
580 if ( $this->fld_ids ) {
581 $vals[
'rcid'] = (int)$row->rc_id;
585 if ( $this->fld_user || $this->fld_userid ) {
586 if ( $row->rc_deleted & RevisionRecord::DELETED_USER ) {
587 $vals[
'userhidden'] =
true;
590 if ( RevisionRecord::userCanBitfield( $row->rc_deleted, RevisionRecord::DELETED_USER, $user ) ) {
591 if ( $this->fld_user ) {
592 $vals[
'user'] = $row->actor_name;
595 if ( $this->fld_userid ) {
596 $vals[
'userid'] = (int)$row->actor_user;
599 if ( isset( $row->actor_name ) && $this->userNameUtils->isTemp( $row->actor_name ) ) {
600 $vals[
'temp'] =
true;
603 if ( !$row->actor_user ) {
604 $vals[
'anon'] =
true;
610 if ( $this->fld_flags ) {
611 $vals[
'bot'] = (bool)$row->rc_bot;
612 $vals[
'new'] = $row->rc_type ==
RC_NEW;
613 $vals[
'minor'] = (bool)$row->rc_minor;
617 if ( $this->fld_sizes ) {
618 $vals[
'oldlen'] = (int)$row->rc_old_len;
619 $vals[
'newlen'] = (int)$row->rc_new_len;
623 if ( $this->fld_timestamp ) {
624 $vals[
'timestamp'] =
wfTimestamp( TS_ISO_8601, $row->rc_timestamp );
628 if ( $this->fld_comment || $this->fld_parsedcomment ) {
629 if ( $row->rc_deleted & RevisionRecord::DELETED_COMMENT ) {
630 $vals[
'commenthidden'] =
true;
633 if ( RevisionRecord::userCanBitfield(
634 $row->rc_deleted, RevisionRecord::DELETED_COMMENT, $user
636 if ( $this->fld_comment ) {
637 $vals[
'comment'] = $this->commentStore->getComment(
'rc_comment', $row )->text;
640 if ( $this->fld_parsedcomment ) {
641 $vals[
'parsedcomment'] = $this->formattedComments[$row->rc_id];
646 if ( $this->fld_redirect ) {
647 $vals[
'redirect'] = (bool)$row->page_is_redirect;
651 if ( $this->fld_patrolled ) {
652 $vals[
'patrolled'] = $row->rc_patrolled != RecentChange::PRC_UNPATROLLED;
653 $vals[
'unpatrolled'] = ChangesList::isUnpatrolled( $row, $user );
654 $vals[
'autopatrolled'] = $row->rc_patrolled == RecentChange::PRC_AUTOPATROLLED;
657 if ( $this->fld_loginfo && $row->rc_type ==
RC_LOG ) {
658 if ( $row->rc_deleted & LogPage::DELETED_ACTION ) {
659 $vals[
'actionhidden'] =
true;
662 if ( LogEventsList::userCanBitfield( $row->rc_deleted, LogPage::DELETED_ACTION, $user ) ) {
663 $vals[
'logid'] = (int)$row->rc_logid;
664 $vals[
'logtype'] = $row->rc_log_type;
665 $vals[
'logaction'] = $row->rc_log_action;
666 $vals[
'logparams'] = $this->logFormatterFactory->newFromRow( $row )->formatParametersForApi();
670 if ( $this->fld_tags ) {
671 if ( $row->ts_tags ) {
672 $tags = explode(
',', $row->ts_tags );
674 $vals[
'tags'] = $tags;
680 if ( $this->fld_sha1 && $row->rev_sha1 !==
null ) {
681 if ( $row->rev_deleted & RevisionRecord::DELETED_TEXT ) {
682 $vals[
'sha1hidden'] =
true;
685 if ( RevisionRecord::userCanBitfield(
686 $row->rev_deleted, RevisionRecord::DELETED_TEXT, $user
688 if ( $row->rev_sha1 !==
'' ) {
689 $vals[
'sha1'] = \Wikimedia\base_convert( $row->rev_sha1, 36, 16, 40 );
696 if ( $anyHidden && ( $row->rc_deleted & RevisionRecord::DELETED_RESTRICTED ) ) {
697 $vals[
'suppressed'] =
true;
707 private function includesPatrollingFlags( array $flagsArray ) {
708 return isset( $flagsArray[
'patrolled'] ) ||
709 isset( $flagsArray[
'!patrolled'] ) ||
710 isset( $flagsArray[
'unpatrolled'] ) ||
711 isset( $flagsArray[
'autopatrolled'] ) ||
712 isset( $flagsArray[
'!autopatrolled'] );
716 if ( isset(
$params[
'show'] ) &&
717 $this->includesPatrollingFlags( array_fill_keys(
$params[
'show'],
true ) )
724 if (
$params[
'prop'] !==
null && in_array(
'parsedcomment',
$params[
'prop'] ) ) {
726 return 'anon-public-user-private';
733 $slotRoles = $this->slotRoleRegistry->getKnownRoles();
734 sort( $slotRoles, SORT_STRING );
738 ParamValidator::PARAM_TYPE =>
'timestamp'
741 ParamValidator::PARAM_TYPE =>
'timestamp'
744 ParamValidator::PARAM_DEFAULT =>
'older',
745 ParamValidator::PARAM_TYPE => [
751 'newer' =>
'api-help-paramvalue-direction-newer',
752 'older' =>
'api-help-paramvalue-direction-older',
756 ParamValidator::PARAM_ISMULTI =>
true,
757 ParamValidator::PARAM_TYPE =>
'namespace',
761 ParamValidator::PARAM_TYPE =>
'user',
762 UserDef::PARAM_ALLOWED_USER_TYPES => [
'name',
'ip',
'temp',
'id',
'interwiki' ],
765 ParamValidator::PARAM_TYPE =>
'user',
766 UserDef::PARAM_ALLOWED_USER_TYPES => [
'name',
'ip',
'temp',
'id',
'interwiki' ],
770 ParamValidator::PARAM_ISMULTI =>
true,
771 ParamValidator::PARAM_DEFAULT =>
'title|timestamp|ids',
772 ParamValidator::PARAM_TYPE => [
791 ParamValidator::PARAM_ISMULTI =>
true,
792 ParamValidator::PARAM_TYPE => [
809 ParamValidator::PARAM_DEFAULT => 10,
810 ParamValidator::PARAM_TYPE =>
'limit',
811 IntegerDef::PARAM_MIN => 1,
816 ParamValidator::PARAM_DEFAULT =>
'edit|new|log|categorize',
817 ParamValidator::PARAM_ISMULTI =>
true,
818 ParamValidator::PARAM_TYPE => RecentChange::getChangeTypes()
825 'generaterevisions' =>
false,
827 ParamValidator::PARAM_TYPE => $slotRoles
834 'action=query&list=recentchanges'
835 =>
'apihelp-query+recentchanges-example-simple',
836 'action=query&generator=recentchanges&grcshow=!patrolled&prop=info'
837 =>
'apihelp-query+recentchanges-example-generator',
842 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Recentchanges';
847class_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.