76 parent::__construct( $query, $moduleName,
'uc' );
77 $this->commentStore = $commentStore;
78 $this->userIdentityLookup = $userIdentityLookup;
79 $this->userNameUtils = $userNameUtils;
80 $this->revisionStore = $revisionStore;
81 $this->changeTagDefStore = $changeTagDefStore;
82 $this->changeTagsStore = $changeTagsStore;
83 $this->actorMigration = $actorMigration;
84 $this->commentFormatter = $commentFormatter;
88 private bool $multiUserMode;
89 private string $orderBy;
90 private array $parentLens;
92 private bool $fld_ids =
false;
93 private bool $fld_title =
false;
94 private bool $fld_timestamp =
false;
95 private bool $fld_comment =
false;
96 private bool $fld_parsedcomment =
false;
97 private bool $fld_flags =
false;
98 private bool $fld_patrolled =
false;
99 private bool $fld_tags =
false;
100 private bool $fld_size =
false;
101 private bool $fld_sizediff =
false;
107 $prop = array_fill_keys( $this->params[
'prop'],
true );
108 $this->fld_ids = isset( $prop[
'ids'] );
109 $this->fld_title = isset( $prop[
'title'] );
110 $this->fld_comment = isset( $prop[
'comment'] );
111 $this->fld_parsedcomment = isset( $prop[
'parsedcomment'] );
112 $this->fld_size = isset( $prop[
'size'] );
113 $this->fld_sizediff = isset( $prop[
'sizediff'] );
114 $this->fld_flags = isset( $prop[
'flags'] );
115 $this->fld_timestamp = isset( $prop[
'timestamp'] );
116 $this->fld_patrolled = isset( $prop[
'patrolled'] );
117 $this->fld_tags = isset( $prop[
'tags'] );
119 $dbSecondary = $this->
getDB();
121 $sort = ( $this->params[
'dir'] ==
'newer' ?
122 SelectQueryBuilder::SORT_ASC : SelectQueryBuilder::SORT_DESC );
123 $op = ( $this->params[
'dir'] ==
'older' ?
'<=' :
'>=' );
129 if ( isset( $this->params[
'userprefix'] ) ) {
130 $this->multiUserMode =
true;
131 $this->orderBy =
'name';
137 $userIter = call_user_func(
function () use ( $dbSecondary, $sort, $op, $fname ) {
139 if ( $this->params[
'continue'] !==
null ) {
141 [
'string',
'string',
'string',
'int' ] );
143 $fromName = $continue[1];
148 $usersBatch = $this->userIdentityLookup
149 ->newSelectQueryBuilder()
152 ->whereUserNamePrefix( $this->params[
'userprefix'] )
153 ->where( $fromName !==
false
154 ? $dbSecondary->buildComparison( $op, [
'actor_name' => $fromName ] )
156 ->orderByName( $sort )
157 ->fetchUserIdentities();
161 foreach ( $usersBatch as $user ) {
162 if ( ++$count >= $limit ) {
163 $fromName = $user->getName();
168 }
while ( $fromName !==
false );
173 } elseif ( isset( $this->params[
'userids'] ) ) {
174 if ( $this->params[
'userids'] === [] ) {
176 $this->
dieWithError( [
'apierror-paramempty', $encParamName ],
"paramempty_$encParamName" );
180 foreach ( $this->params[
'userids'] as $uid ) {
182 $this->
dieWithError( [
'apierror-invaliduserid', $uid ],
'invaliduserid' );
187 $this->orderBy =
'actor';
188 $this->multiUserMode = count( $ids ) > 1;
191 if ( $this->multiUserMode && $this->params[
'continue'] !==
null ) {
193 [
'string',
'int',
'string',
'int' ] );
195 $fromId = $continue[1];
198 $userIter = $this->userIdentityLookup
199 ->newSelectQueryBuilder()
200 ->caller( __METHOD__ )
201 ->whereUserIds( $ids )
202 ->orderByUserId( $sort )
203 ->where( $fromId ? $dbSecondary->buildComparison( $op, [
'actor_id' => $fromId ] ) : [] )
204 ->fetchUserIdentities();
205 $batchSize = count( $ids );
206 } elseif ( isset( $this->params[
'iprange'] ) ) {
208 $ipRange = $this->params[
'iprange'];
210 if ( IPUtils::isIPv4( $ipRange ) ) {
212 $cidrLimit = $contribsCIDRLimit[
'IPv4'];
213 } elseif ( IPUtils::isIPv6( $ipRange ) ) {
215 $cidrLimit = $contribsCIDRLimit[
'IPv6'];
217 $this->
dieWithError( [
'apierror-invalidiprange', $ipRange ],
'invalidiprange' );
219 $range = IPUtils::parseCIDR( $ipRange )[1];
220 if ( $range ===
false ) {
221 $this->
dieWithError( [
'apierror-invalidiprange', $ipRange ],
'invalidiprange' );
222 } elseif ( $range < $cidrLimit ) {
223 $this->
dieWithError( [
'apierror-cidrtoobroad', $type, $cidrLimit ] );
226 $this->multiUserMode =
true;
227 $this->orderBy =
'name';
232 $userIter = call_user_func(
function () use ( $dbSecondary, $sort, $op, $fname, $ipRange ) {
233 [ $start, $end ] = IPUtils::parseRange( $ipRange );
234 if ( $this->params[
'continue'] !==
null ) {
236 [
'string',
'string',
'string',
'int' ] );
238 $fromName = $continue[1];
239 $fromIPHex = IPUtils::toHex( $fromName );
251 $res = $dbSecondary->newSelectQueryBuilder()
252 ->select(
'ipc_hex' )
253 ->from(
'ip_changes' )
254 ->where( $dbSecondary->expr(
'ipc_hex',
'>=', $start )->and(
'ipc_hex',
'<=', $end ) )
255 ->groupBy(
'ipc_hex' )
256 ->orderBy(
'ipc_hex', $sort )
263 foreach ( $res as $row ) {
264 $ipAddr = IPUtils::formatHex( $row->ipc_hex );
265 if ( ++$count >= $limit ) {
271 }
while ( $fromName !==
false );
278 if ( !count( $this->params[
'user'] ) ) {
281 [
'apierror-paramempty', $encParamName ],
"paramempty_$encParamName"
284 foreach ( $this->params[
'user'] as $u ) {
288 [
'apierror-paramempty', $encParamName ],
"paramempty_$encParamName"
292 if ( $this->userNameUtils->isIP( $u ) || ExternalUserNames::isExternal( $u ) ) {
295 $name = $this->userNameUtils->getCanonical( $u );
296 if ( $name ===
false ) {
299 [
'apierror-baduser', $encParamName,
wfEscapeWikiText( $u ) ],
"baduser_$encParamName"
302 $names[$name] =
null;
306 $this->orderBy =
'actor';
307 $this->multiUserMode = count( $names ) > 1;
310 if ( $this->multiUserMode && $this->params[
'continue'] !==
null ) {
312 [
'string',
'int',
'string',
'int' ] );
314 $fromId = $continue[1];
317 $userIter = $this->userIdentityLookup
318 ->newSelectQueryBuilder()
319 ->caller( __METHOD__ )
320 ->whereUserNames( array_keys( $names ) )
321 ->orderByName( $sort )
322 ->where( $fromId ? $dbSecondary->buildComparison( $op, [
'actor_id' => $fromId ] ) : [] )
323 ->fetchUserIdentities();
324 $batchSize = count( $names );
328 $limit = $this->params[
'limit'];
330 while ( $userIter->valid() ) {
332 while ( count( $users ) < $batchSize && $userIter->valid() ) {
333 $users[] = $userIter->current();
338 $this->prepareQuery( $users, $limit - $count );
339 $res = $this->
select( __METHOD__, [], $hookData );
341 if ( $this->fld_title ) {
345 if ( $this->fld_sizediff ) {
347 foreach ( $res as $row ) {
348 if ( $row->rev_parent_id ) {
349 $revIds[] = (int)$row->rev_parent_id;
352 $this->parentLens = $this->revisionStore->getRevisionSizes( $revIds );
355 foreach ( $res as $row ) {
356 if ( ++$count > $limit ) {
363 $vals = $this->extractRowInfo( $row );
364 $fit = $this->
processRow( $row, $vals, $hookData ) &&
381 private function prepareQuery( array $users, $limit ) {
383 $db = $this->
getDB();
385 $queryBuilder = $this->revisionStore->newSelectQueryBuilder( $db )->joinComment()->joinPage();
386 $revWhere = $this->actorMigration->getWhere( $db,
'rev_user', $users );
388 $orderUserField =
'rev_actor';
389 $userField = $this->orderBy ===
'actor' ?
'rev_actor' :
'actor_name';
390 $tsField =
'rev_timestamp';
394 $this->
addWhere( $revWhere[
'conds'] );
396 if ( isset( $revWhere[
'orconds'][
'newactor'] ) ) {
397 $this->
addOption(
'USE INDEX', [
'revision' =>
'rev_actor_timestamp' ] );
401 if ( $this->params[
'continue'] !==
null ) {
402 if ( $this->multiUserMode ) {
404 [
'string',
'string',
'timestamp',
'int' ] );
405 $modeFlag = array_shift( $continue );
407 $encUser = array_shift( $continue );
410 [
'timestamp',
'int' ] );
412 $op = ( $this->params[
'dir'] ==
'older' ?
'<=' :
'>=' );
413 if ( $this->multiUserMode ) {
414 $this->
addWhere( $db->buildComparison( $op, [
416 $userField => $encUser,
417 $tsField => $db->timestamp( $continue[0] ),
418 $idField => $continue[1],
421 $this->
addWhere( $db->buildComparison( $op, [
422 $tsField => $db->timestamp( $continue[0] ),
423 $idField => $continue[1],
430 if ( !$this->
getAuthority()->isAllowed(
'deletedhistory' ) ) {
431 $bitmask = RevisionRecord::DELETED_USER;
432 } elseif ( !$this->
getAuthority()->isAllowedAny(
'suppressrevision',
'viewsuppressed' ) ) {
433 $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
438 $this->
addWhere( $db->bitAnd(
'rev_deleted', $bitmask ) .
" != $bitmask" );
442 if ( count( $users ) > 1 ) {
443 $this->
addWhereRange( $orderUserField, $this->params[
'dir'],
null,
null );
448 $this->params[
'dir'], $this->params[
'start'], $this->params[
'end'] );
451 $this->
addWhereRange( $idField, $this->params[
'dir'],
null,
null );
453 $this->
addWhereFld(
'page_namespace', $this->params[
'namespace'] );
455 $show = $this->params[
'show'];
456 if ( $this->params[
'toponly'] ) {
459 if ( $show !==
null ) {
460 $show = array_fill_keys( $show,
true );
462 if ( ( isset( $show[
'minor'] ) && isset( $show[
'!minor'] ) )
463 || ( isset( $show[
'patrolled'] ) && isset( $show[
'!patrolled'] ) )
464 || ( isset( $show[
'autopatrolled'] ) && isset( $show[
'!autopatrolled'] ) )
465 || ( isset( $show[
'autopatrolled'] ) && isset( $show[
'!patrolled'] ) )
466 || ( isset( $show[
'top'] ) && isset( $show[
'!top'] ) )
467 || ( isset( $show[
'new'] ) && isset( $show[
'!new'] ) )
472 $this->
addWhereIf( [
'rev_minor_edit' => 0 ], isset( $show[
'!minor'] ) );
473 $this->
addWhereIf( $db->expr(
'rev_minor_edit',
'!=', 0 ), isset( $show[
'minor'] ) );
475 [
'rc_patrolled' => RecentChange::PRC_UNPATROLLED ],
476 isset( $show[
'!patrolled'] )
479 $db->expr(
'rc_patrolled',
'!=', RecentChange::PRC_UNPATROLLED ),
480 isset( $show[
'patrolled'] )
483 $db->expr(
'rc_patrolled',
'!=', RecentChange::PRC_AUTOPATROLLED ),
484 isset( $show[
'!autopatrolled'] )
487 [
'rc_patrolled' => RecentChange::PRC_AUTOPATROLLED ],
488 isset( $show[
'autopatrolled'] )
490 $this->
addWhereIf( $idField .
' != page_latest', isset( $show[
'!top'] ) );
491 $this->
addWhereIf( $idField .
' = page_latest', isset( $show[
'top'] ) );
492 $this->
addWhereIf( $db->expr(
'rev_parent_id',
'!=', 0 ), isset( $show[
'!new'] ) );
493 $this->
addWhereIf( [
'rev_parent_id' => 0 ], isset( $show[
'new'] ) );
497 if ( isset( $show[
'patrolled'] ) || isset( $show[
'!patrolled'] ) ||
498 isset( $show[
'autopatrolled'] ) || isset( $show[
'!autopatrolled'] ) || $this->fld_patrolled
501 if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
502 $this->
dieWithError(
'apierror-permissiondenied-patrolflag',
'permissiondenied' );
505 $isFilterset = isset( $show[
'patrolled'] ) || isset( $show[
'!patrolled'] ) ||
506 isset( $show[
'autopatrolled'] ) || isset( $show[
'!autopatrolled'] );
509 $isFilterset ?
'JOIN' :
'LEFT JOIN',
510 [
'rc_this_oldid = ' . $idField ]
514 $this->
addFieldsIf(
'rc_patrolled', $this->fld_patrolled );
516 if ( $this->fld_tags ) {
518 'ts_tags' => $this->changeTagsStore->makeTagSummarySubquery(
'revision' )
522 if ( isset( $this->params[
'tag'] ) ) {
525 [
'change_tag' => [
'JOIN', [ $idField .
' = ct_rev_id' ] ] ]
528 $this->
addWhereFld(
'ct_tag_id', $this->changeTagDefStore->getId( $this->params[
'tag'] ) );
529 }
catch ( NameTableAccessException $exception ) {
535 'MAX_EXECUTION_TIME',
546 private function extractRowInfo( $row ) {
550 if ( $row->rev_deleted & RevisionRecord::DELETED_TEXT ) {
551 $vals[
'texthidden'] =
true;
556 $vals[
'userid'] = (int)$row->rev_user;
557 $vals[
'user'] = $row->rev_user_text;
558 if ( $row->rev_deleted & RevisionRecord::DELETED_USER ) {
559 $vals[
'userhidden'] =
true;
562 if ( $this->fld_ids ) {
563 $vals[
'pageid'] = (int)$row->rev_page;
564 $vals[
'revid'] = (int)$row->rev_id;
566 if ( $row->rev_parent_id !==
null ) {
567 $vals[
'parentid'] = (int)$row->rev_parent_id;
571 $title = Title::makeTitle( $row->page_namespace, $row->page_title );
573 if ( $this->fld_title ) {
577 if ( $this->fld_timestamp ) {
578 $vals[
'timestamp'] =
wfTimestamp( TS_ISO_8601, $row->rev_timestamp );
581 if ( $this->fld_flags ) {
582 $vals[
'new'] = $row->rev_parent_id == 0 && $row->rev_parent_id !==
null;
583 $vals[
'minor'] = (bool)$row->rev_minor_edit;
584 $vals[
'top'] = $row->page_latest == $row->rev_id;
587 if ( $this->fld_comment || $this->fld_parsedcomment ) {
588 if ( $row->rev_deleted & RevisionRecord::DELETED_COMMENT ) {
589 $vals[
'commenthidden'] =
true;
593 $userCanView = RevisionRecord::userCanBitfield(
595 RevisionRecord::DELETED_COMMENT, $this->getAuthority()
598 if ( $userCanView ) {
599 $comment = $this->commentStore->getComment(
'rev_comment', $row )->text;
600 if ( $this->fld_comment ) {
601 $vals[
'comment'] = $comment;
604 if ( $this->fld_parsedcomment ) {
605 $vals[
'parsedcomment'] = $this->commentFormatter->format( $comment, $title );
610 if ( $this->fld_patrolled ) {
615 if ( $this->fld_size && $row->rev_len !==
null ) {
616 $vals[
'size'] = (int)$row->rev_len;
619 if ( $this->fld_sizediff
620 && $row->rev_len !==
null
621 && $row->rev_parent_id !==
null
623 $parentLen = $this->parentLens[$row->rev_parent_id] ?? 0;
624 $vals[
'sizediff'] = (int)$row->rev_len - $parentLen;
627 if ( $this->fld_tags ) {
628 if ( $row->ts_tags ) {
629 $tags = explode(
',', $row->ts_tags );
631 $vals[
'tags'] = $tags;
637 if ( $anyHidden && ( $row->rev_deleted & RevisionRecord::DELETED_RESTRICTED ) ) {
638 $vals[
'suppressed'] =
true;
644 private function continueStr( $row ) {
645 if ( $this->multiUserMode ) {
646 switch ( $this->orderBy ) {
648 return "name|$row->rev_user_text|$row->rev_timestamp|$row->rev_id";
650 return "actor|$row->rev_actor|$row->rev_timestamp|$row->rev_id";
653 return "$row->rev_timestamp|$row->rev_id";
660 return 'anon-public-user-private';
666 ParamValidator::PARAM_DEFAULT => 10,
667 ParamValidator::PARAM_TYPE =>
'limit',
668 IntegerDef::PARAM_MIN => 1,
673 ParamValidator::PARAM_TYPE =>
'timestamp'
676 ParamValidator::PARAM_TYPE =>
'timestamp'
682 ParamValidator::PARAM_TYPE =>
'user',
683 UserDef::PARAM_ALLOWED_USER_TYPES => [
'name',
'ip',
'temp',
'interwiki' ],
684 ParamValidator::PARAM_ISMULTI => true
687 ParamValidator::PARAM_TYPE =>
'integer',
688 ParamValidator::PARAM_ISMULTI => true
690 'userprefix' =>
null,
693 ParamValidator::PARAM_DEFAULT =>
'older',
694 ParamValidator::PARAM_TYPE => [
700 'newer' =>
'api-help-paramvalue-direction-newer',
701 'older' =>
'api-help-paramvalue-direction-older',
705 ParamValidator::PARAM_ISMULTI =>
true,
706 ParamValidator::PARAM_TYPE =>
'namespace'
709 ParamValidator::PARAM_ISMULTI =>
true,
710 ParamValidator::PARAM_DEFAULT =>
'ids|title|timestamp|comment|size|flags',
711 ParamValidator::PARAM_TYPE => [
726 ParamValidator::PARAM_ISMULTI =>
true,
727 ParamValidator::PARAM_TYPE => [
740 'apihelp-query+usercontribs-param-show',
746 ParamValidator::PARAM_DEFAULT =>
false,
747 ParamValidator::PARAM_DEPRECATED =>
true,
754 'action=query&list=usercontribs&ucuser=Example'
755 =>
'apihelp-query+usercontribs-example-user',
756 'action=query&list=usercontribs&ucuserprefix=192.0.2.'
757 =>
'apihelp-query+usercontribs-example-ipprefix',
762 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Usercontribs';
767class_alias( ApiQueryUserContribs::class,
'ApiQueryUserContribs' );
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.
A class containing constants representing the names of configuration variables.
const RCMaxAge
Name constant for the RCMaxAge setting, for use with Config::get()
const MaxExecutionTimeForExpensiveQueries
Name constant for the MaxExecutionTimeForExpensiveQueries setting, for use with Config::get()
const RangeContributionsCIDRLimit
Name constant for the RangeContributionsCIDRLimit setting, for use with Config::get()
Utility class for creating and reading rows in the recentchanges table.