33use Wikimedia\Timestamp\TimestampFormat as TS;
54 parent::__construct( $query, $moduleName,
'uc' );
57 private array $params;
58 private bool $multiUserMode;
59 private string $orderBy;
60 private array $parentLens;
63 private array $prop = [];
69 $this->prop = array_fill_keys( $this->params[
'prop'],
true );
71 $dbSecondary = $this->
getDB();
73 $sort = ( $this->params[
'dir'] ==
'newer' ?
74 SelectQueryBuilder::SORT_ASC : SelectQueryBuilder::SORT_DESC );
75 $op = ( $this->params[
'dir'] ==
'older' ?
'<=' :
'>=' );
81 if ( isset( $this->params[
'userprefix'] ) ) {
82 $this->multiUserMode =
true;
83 $this->orderBy =
'name';
89 $userIter = call_user_func(
function () use ( $dbSecondary, $sort, $op, $fname ) {
91 if ( $this->params[
'continue'] !==
null ) {
93 [
'string',
'string',
'string',
'int' ] );
95 $fromName = $continue[1];
100 $usersBatch = $this->userIdentityLookup
101 ->newSelectQueryBuilder()
104 ->whereUserNamePrefix( $this->params[
'userprefix'] )
105 ->where( $fromName !==
false
106 ? $dbSecondary->buildComparison( $op, [
'actor_name' => $fromName ] )
108 ->orderByName( $sort )
109 ->fetchUserIdentities();
113 foreach ( $usersBatch as $user ) {
114 if ( ++$count >= $limit ) {
115 $fromName = $user->getName();
120 }
while ( $fromName !==
false );
125 } elseif ( isset( $this->params[
'userids'] ) ) {
126 if ( $this->params[
'userids'] === [] ) {
128 $this->
dieWithError( [
'apierror-paramempty', $encParamName ],
"paramempty_$encParamName" );
132 foreach ( $this->params[
'userids'] as $uid ) {
134 $this->
dieWithError( [
'apierror-invaliduserid', $uid ],
'invaliduserid' );
139 $this->orderBy =
'actor';
140 $this->multiUserMode = count( $ids ) > 1;
143 if ( $this->multiUserMode && $this->params[
'continue'] !==
null ) {
145 [
'string',
'int',
'string',
'int' ] );
147 $fromId = $continue[1];
150 $userIter = $this->userIdentityLookup
151 ->newSelectQueryBuilder()
152 ->caller( __METHOD__ )
153 ->whereUserIds( $ids )
154 ->orderByUserId( $sort )
155 ->where( $fromId ? $dbSecondary->buildComparison( $op, [
'actor_id' => $fromId ] ) : [] )
156 ->fetchUserIdentities();
157 $batchSize = count( $ids );
158 } elseif ( isset( $this->params[
'iprange'] ) ) {
160 $ipRange = $this->params[
'iprange'];
162 if ( IPUtils::isIPv4( $ipRange ) ) {
164 $cidrLimit = $contribsCIDRLimit[
'IPv4'];
165 } elseif ( IPUtils::isIPv6( $ipRange ) ) {
167 $cidrLimit = $contribsCIDRLimit[
'IPv6'];
169 $this->
dieWithError( [
'apierror-invalidiprange', $ipRange ],
'invalidiprange' );
171 $range = IPUtils::parseCIDR( $ipRange )[1];
172 if ( $range ===
false ) {
173 $this->
dieWithError( [
'apierror-invalidiprange', $ipRange ],
'invalidiprange' );
174 } elseif ( $range < $cidrLimit ) {
175 $this->
dieWithError( [
'apierror-cidrtoobroad', $type, $cidrLimit ] );
178 $this->multiUserMode =
true;
179 $this->orderBy =
'name';
184 $userIter = call_user_func(
function () use ( $dbSecondary, $sort, $op, $fname, $ipRange ) {
185 [ $start, $end ] = IPUtils::parseRange( $ipRange );
186 if ( $this->params[
'continue'] !==
null ) {
188 [
'string',
'string',
'string',
'int' ] );
190 $fromName = $continue[1];
191 $fromIPHex = IPUtils::toHex( $fromName );
203 $res = $dbSecondary->newSelectQueryBuilder()
204 ->select(
'ipc_hex' )
205 ->from(
'ip_changes' )
206 ->where( $dbSecondary->expr(
'ipc_hex',
'>=', $start )->and(
'ipc_hex',
'<=', $end ) )
207 ->groupBy(
'ipc_hex' )
208 ->orderBy(
'ipc_hex', $sort )
215 foreach ( $res as $row ) {
216 $ipAddr = IPUtils::formatHex( $row->ipc_hex );
217 if ( ++$count >= $limit ) {
221 yield UserIdentityValue::newAnonymous( $ipAddr );
223 }
while ( $fromName !==
false );
230 if ( !count( $this->params[
'user'] ) ) {
233 [
'apierror-paramempty', $encParamName ],
"paramempty_$encParamName"
236 foreach ( $this->params[
'user'] as $u ) {
240 [
'apierror-paramempty', $encParamName ],
"paramempty_$encParamName"
244 if ( $this->userNameUtils->isIP( $u ) || ExternalUserNames::isExternal( $u ) ) {
247 $name = $this->userNameUtils->getCanonical( $u );
248 if ( $name ===
false ) {
251 [
'apierror-baduser', $encParamName,
wfEscapeWikiText( $u ) ],
"baduser_$encParamName"
254 $names[$name] =
null;
258 $this->orderBy =
'actor';
259 $this->multiUserMode = count( $names ) > 1;
262 if ( $this->multiUserMode && $this->params[
'continue'] !==
null ) {
264 [
'string',
'int',
'string',
'int' ] );
266 $fromId = $continue[1];
269 $userIter = $this->userIdentityLookup
270 ->newSelectQueryBuilder()
271 ->caller( __METHOD__ )
272 ->whereUserNames( array_keys( $names ) )
273 ->orderByName( $sort )
274 ->where( $fromId ? $dbSecondary->buildComparison( $op, [
'actor_id' => $fromId ] ) : [] )
275 ->fetchUserIdentities();
276 $batchSize = count( $names );
280 $limit = $this->params[
'limit'];
282 while ( $userIter->valid() ) {
284 while ( count( $users ) < $batchSize && $userIter->valid() ) {
285 $users[] = $userIter->current();
290 $this->prepareQuery( $users, $limit - $count );
291 $res = $this->
select( __METHOD__, [], $hookData );
293 if ( isset( $this->prop[
'title'] ) ) {
297 if ( isset( $this->prop[
'sizediff'] ) ) {
299 foreach ( $res as $row ) {
300 if ( $row->rev_parent_id ) {
301 $revIds[] = (int)$row->rev_parent_id;
304 $this->parentLens = $this->revisionStore->getRevisionSizes( $revIds );
307 foreach ( $res as $row ) {
308 if ( ++$count > $limit ) {
315 $vals = $this->extractRowInfo( $row );
316 $fit = $this->
processRow( $row, $vals, $hookData ) &&
333 private function prepareQuery( array $users, $limit ) {
335 $db = $this->
getDB();
337 $queryBuilder = $this->revisionStore->newSelectQueryBuilder( $db )->joinComment()->joinPage();
338 $revWhere = $this->actorMigration->getWhere( $db,
'rev_user', $users );
340 $orderUserField =
'rev_actor';
341 $userField = $this->orderBy ===
'actor' ?
'rev_actor' :
'actor_name';
342 $tsField =
'rev_timestamp';
346 $this->
addWhere( $revWhere[
'conds'] );
348 if ( isset( $revWhere[
'orconds'][
'newactor'] ) ) {
349 $this->
addOption(
'USE INDEX', [
'revision' =>
'rev_actor_timestamp' ] );
353 if ( $this->params[
'continue'] !==
null ) {
354 if ( $this->multiUserMode ) {
356 [
'string',
'string',
'timestamp',
'int' ] );
357 $modeFlag = array_shift( $continue );
359 $encUser = array_shift( $continue );
362 [
'timestamp',
'int' ] );
364 $op = ( $this->params[
'dir'] ==
'older' ?
'<=' :
'>=' );
365 if ( $this->multiUserMode ) {
366 $this->
addWhere( $db->buildComparison( $op, [
368 $userField => $encUser,
369 $tsField => $db->timestamp( $continue[0] ),
370 $idField => $continue[1],
373 $this->
addWhere( $db->buildComparison( $op, [
374 $tsField => $db->timestamp( $continue[0] ),
375 $idField => $continue[1],
382 if ( !$this->
getAuthority()->isAllowed(
'deletedhistory' ) ) {
383 $bitmask = RevisionRecord::DELETED_USER;
384 } elseif ( !$this->
getAuthority()->isAllowedAny(
'suppressrevision',
'viewsuppressed' ) ) {
385 $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
390 $this->
addWhere( $db->bitAnd(
'rev_deleted', $bitmask ) .
" != $bitmask" );
394 if ( count( $users ) > 1 ) {
395 $this->
addWhereRange( $orderUserField, $this->params[
'dir'],
null,
null );
400 $this->params[
'dir'], $this->params[
'start'], $this->params[
'end'] );
403 $this->
addWhereRange( $idField, $this->params[
'dir'],
null,
null );
405 $this->
addWhereFld(
'page_namespace', $this->params[
'namespace'] );
407 $show = $this->params[
'show'];
408 if ( $this->params[
'toponly'] ) {
411 if ( $show !==
null ) {
413 $show = array_fill_keys( $show,
true );
415 foreach ( $show as $key => $_ ) {
417 if ( str_starts_with( $key,
'!' ) && isset( $show[substr( $key, 1 )] ) ) {
421 if ( isset( $show[
'autopatrolled'] ) && isset( $show[
'!patrolled'] ) ) {
425 $this->
addWhereIf( [
'rev_minor_edit' => 0 ], isset( $show[
'!minor'] ) );
426 $this->
addWhereIf( $db->expr(
'rev_minor_edit',
'!=', 0 ), isset( $show[
'minor'] ) );
428 [
'rc_patrolled' => RecentChange::PRC_UNPATROLLED ],
429 isset( $show[
'!patrolled'] )
432 $db->expr(
'rc_patrolled',
'!=', RecentChange::PRC_UNPATROLLED ),
433 isset( $show[
'patrolled'] )
436 $db->expr(
'rc_patrolled',
'!=', RecentChange::PRC_AUTOPATROLLED ),
437 isset( $show[
'!autopatrolled'] )
440 [
'rc_patrolled' => RecentChange::PRC_AUTOPATROLLED ],
441 isset( $show[
'autopatrolled'] )
443 $this->
addWhereIf( $idField .
' != page_latest', isset( $show[
'!top'] ) );
444 $this->
addWhereIf( $idField .
' = page_latest', isset( $show[
'top'] ) );
445 $this->
addWhereIf( $db->expr(
'rev_parent_id',
'!=', 0 ), isset( $show[
'!new'] ) );
446 $this->
addWhereIf( [
'rev_parent_id' => 0 ], isset( $show[
'new'] ) );
450 if ( isset( $show[
'patrolled'] ) || isset( $show[
'!patrolled'] ) ||
451 isset( $show[
'autopatrolled'] ) || isset( $show[
'!autopatrolled'] ) ||
452 isset( $this->prop[
'patrolled'] )
455 if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
456 $this->
dieWithError(
'apierror-permissiondenied-patrolflag',
'permissiondenied' );
459 $isFilterset = isset( $show[
'patrolled'] ) || isset( $show[
'!patrolled'] ) ||
460 isset( $show[
'autopatrolled'] ) || isset( $show[
'!autopatrolled'] );
463 $isFilterset ?
'JOIN' :
'LEFT JOIN',
464 [
'rc_this_oldid = ' . $idField ]
468 $this->
addFieldsIf(
'rc_patrolled', isset( $this->prop[
'patrolled'] ) );
470 if ( isset( $this->prop[
'tags'] ) ) {
472 'ts_tags' => $this->changeTagsStore->makeTagSummarySubquery(
'revision' )
476 if ( isset( $this->params[
'tag'] ) ) {
479 [
'change_tag' => [
'JOIN', [ $idField .
' = ct_rev_id' ] ] ]
482 $this->
addWhereFld(
'ct_tag_id', $this->changeTagDefStore->getId( $this->params[
'tag'] ) );
483 }
catch ( NameTableAccessException ) {
489 'MAX_EXECUTION_TIME',
500 private function extractRowInfo( $row ) {
504 if ( $row->rev_deleted & RevisionRecord::DELETED_TEXT ) {
505 $vals[
'texthidden'] =
true;
510 $vals[
'userid'] = (int)$row->rev_user;
511 $vals[
'user'] = $row->rev_user_text;
512 if ( $row->rev_deleted & RevisionRecord::DELETED_USER ) {
513 $vals[
'userhidden'] =
true;
516 if ( $this->prop[
'ids'] ??
false ) {
517 $vals[
'pageid'] = (int)$row->rev_page;
518 $vals[
'revid'] = (int)$row->rev_id;
520 if ( $row->rev_parent_id !==
null ) {
521 $vals[
'parentid'] = (int)$row->rev_parent_id;
527 if ( isset( $this->prop[
'title'] ) ) {
531 if ( isset( $this->prop[
'timestamp'] ) ) {
532 $vals[
'timestamp'] =
wfTimestamp( TS::ISO_8601, $row->rev_timestamp );
535 if ( isset( $this->prop[
'flags'] ) ) {
536 $vals[
'new'] = $row->rev_parent_id == 0 && $row->rev_parent_id !==
null;
537 $vals[
'minor'] = (bool)$row->rev_minor_edit;
538 $vals[
'top'] = $row->page_latest == $row->rev_id;
541 if ( isset( $this->prop[
'comment'] ) || isset( $this->prop[
'parsedcomment'] ) ) {
542 if ( $row->rev_deleted & RevisionRecord::DELETED_COMMENT ) {
543 $vals[
'commenthidden'] =
true;
547 $userCanView = RevisionRecord::userCanBitfield(
549 RevisionRecord::DELETED_COMMENT, $this->getAuthority()
552 if ( $userCanView ) {
553 $comment = $this->commentStore->getComment(
'rev_comment', $row )->text;
554 if ( isset( $this->prop[
'comment'] ) ) {
555 $vals[
'comment'] = $comment;
558 if ( isset( $this->prop[
'parsedcomment'] ) ) {
559 $vals[
'parsedcomment'] = $this->commentFormatter->format( $comment, $title );
564 if ( isset( $this->prop[
'patrolled'] ) ) {
565 $vals[
'patrolled'] = $row->rc_patrolled != RecentChange::PRC_UNPATROLLED;
566 $vals[
'autopatrolled'] = $row->rc_patrolled == RecentChange::PRC_AUTOPATROLLED;
569 if ( isset( $this->prop[
'size'] ) && $row->rev_len !==
null ) {
570 $vals[
'size'] = (int)$row->rev_len;
573 if ( isset( $this->prop[
'sizediff'] )
574 && $row->rev_len !==
null
575 && $row->rev_parent_id !==
null
577 $parentLen = $this->parentLens[$row->rev_parent_id] ?? 0;
578 $vals[
'sizediff'] = (int)$row->rev_len - $parentLen;
581 if ( isset( $this->prop[
'tags'] ) ) {
582 if ( $row->ts_tags ) {
583 $tags = explode(
',', $row->ts_tags );
585 $vals[
'tags'] = $tags;
591 if ( $anyHidden && ( $row->rev_deleted & RevisionRecord::DELETED_RESTRICTED ) ) {
592 $vals[
'suppressed'] =
true;
598 private function continueStr( \stdClass $row ): string {
599 if ( $this->multiUserMode ) {
600 switch ( $this->orderBy ) {
602 return "name|$row->rev_user_text|$row->rev_timestamp|$row->rev_id";
604 return "actor|$row->rev_actor|$row->rev_timestamp|$row->rev_id";
607 return "$row->rev_timestamp|$row->rev_id";
615 return 'anon-public-user-private';
622 ParamValidator::PARAM_DEFAULT => 10,
623 ParamValidator::PARAM_TYPE =>
'limit',
624 IntegerDef::PARAM_MIN => 1,
629 ParamValidator::PARAM_TYPE =>
'timestamp'
632 ParamValidator::PARAM_TYPE =>
'timestamp'
638 ParamValidator::PARAM_TYPE =>
'user',
639 UserDef::PARAM_ALLOWED_USER_TYPES => [
'name',
'ip',
'temp',
'interwiki' ],
640 ParamValidator::PARAM_ISMULTI => true
643 ParamValidator::PARAM_TYPE =>
'integer',
644 ParamValidator::PARAM_ISMULTI => true
646 'userprefix' =>
null,
649 ParamValidator::PARAM_DEFAULT =>
'older',
650 ParamValidator::PARAM_TYPE => [
656 'newer' =>
'api-help-paramvalue-direction-newer',
657 'older' =>
'api-help-paramvalue-direction-older',
661 ParamValidator::PARAM_ISMULTI =>
true,
662 ParamValidator::PARAM_TYPE =>
'namespace'
665 ParamValidator::PARAM_ISMULTI =>
true,
666 ParamValidator::PARAM_DEFAULT =>
'ids|title|timestamp|comment|size|flags',
667 ParamValidator::PARAM_TYPE => [
682 ParamValidator::PARAM_ISMULTI =>
true,
683 ParamValidator::PARAM_TYPE => [
696 'apihelp-query+usercontribs-param-show',
702 ParamValidator::PARAM_DEFAULT =>
false,
703 ParamValidator::PARAM_DEPRECATED =>
true,
711 'action=query&list=usercontribs&ucuser=Example'
712 =>
'apihelp-query+usercontribs-example-user',
713 'action=query&list=usercontribs&ucuserprefix=192.0.2.'
714 =>
'apihelp-query+usercontribs-example-ipprefix',
720 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Usercontribs';
725class_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.
if(!defined('MW_SETUP_CALLBACK'))
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()