62 parent::__construct( $query, $moduleName,
'uc' );
63 $this->commentStore = $commentStore;
64 $this->userIdentityLookup = $userIdentityLookup;
65 $this->userNameUtils = $userNameUtils;
66 $this->revisionStore = $revisionStore;
67 $this->changeTagDefStore = $changeTagDefStore;
68 $this->changeTagsStore = $changeTagsStore;
69 $this->actorMigration = $actorMigration;
70 $this->commentFormatter = $commentFormatter;
73 private array $params;
74 private bool $multiUserMode;
75 private string $orderBy;
76 private array $parentLens;
78 private bool $fld_ids =
false;
79 private bool $fld_title =
false;
80 private bool $fld_timestamp =
false;
81 private bool $fld_comment =
false;
82 private bool $fld_parsedcomment =
false;
83 private bool $fld_flags =
false;
84 private bool $fld_patrolled =
false;
85 private bool $fld_tags =
false;
86 private bool $fld_size =
false;
87 private bool $fld_sizediff =
false;
93 $prop = array_fill_keys( $this->params[
'prop'],
true );
94 $this->fld_ids = isset( $prop[
'ids'] );
95 $this->fld_title = isset( $prop[
'title'] );
96 $this->fld_comment = isset( $prop[
'comment'] );
97 $this->fld_parsedcomment = isset( $prop[
'parsedcomment'] );
98 $this->fld_size = isset( $prop[
'size'] );
99 $this->fld_sizediff = isset( $prop[
'sizediff'] );
100 $this->fld_flags = isset( $prop[
'flags'] );
101 $this->fld_timestamp = isset( $prop[
'timestamp'] );
102 $this->fld_patrolled = isset( $prop[
'patrolled'] );
103 $this->fld_tags = isset( $prop[
'tags'] );
105 $dbSecondary = $this->
getDB();
107 $sort = ( $this->params[
'dir'] ==
'newer' ?
108 SelectQueryBuilder::SORT_ASC : SelectQueryBuilder::SORT_DESC );
109 $op = ( $this->params[
'dir'] ==
'older' ?
'<=' :
'>=' );
115 if ( isset( $this->params[
'userprefix'] ) ) {
116 $this->multiUserMode =
true;
117 $this->orderBy =
'name';
123 $userIter = call_user_func(
function () use ( $dbSecondary, $sort, $op, $fname ) {
125 if ( $this->params[
'continue'] !==
null ) {
127 [
'string',
'string',
'string',
'int' ] );
129 $fromName = $continue[1];
134 $usersBatch = $this->userIdentityLookup
135 ->newSelectQueryBuilder()
138 ->whereUserNamePrefix( $this->params[
'userprefix'] )
139 ->where( $fromName !==
false
140 ? $dbSecondary->buildComparison( $op, [
'actor_name' => $fromName ] )
142 ->orderByName( $sort )
143 ->fetchUserIdentities();
147 foreach ( $usersBatch as $user ) {
148 if ( ++$count >= $limit ) {
149 $fromName = $user->getName();
154 }
while ( $fromName !==
false );
159 } elseif ( isset( $this->params[
'userids'] ) ) {
160 if ( $this->params[
'userids'] === [] ) {
162 $this->
dieWithError( [
'apierror-paramempty', $encParamName ],
"paramempty_$encParamName" );
166 foreach ( $this->params[
'userids'] as $uid ) {
168 $this->
dieWithError( [
'apierror-invaliduserid', $uid ],
'invaliduserid' );
173 $this->orderBy =
'actor';
174 $this->multiUserMode = count( $ids ) > 1;
177 if ( $this->multiUserMode && $this->params[
'continue'] !==
null ) {
179 [
'string',
'int',
'string',
'int' ] );
181 $fromId = $continue[1];
184 $userIter = $this->userIdentityLookup
185 ->newSelectQueryBuilder()
186 ->caller( __METHOD__ )
187 ->whereUserIds( $ids )
188 ->orderByUserId( $sort )
189 ->where( $fromId ? $dbSecondary->buildComparison( $op, [
'actor_id' => $fromId ] ) : [] )
190 ->fetchUserIdentities();
191 $batchSize = count( $ids );
192 } elseif ( isset( $this->params[
'iprange'] ) ) {
194 $ipRange = $this->params[
'iprange'];
196 if ( IPUtils::isIPv4( $ipRange ) ) {
198 $cidrLimit = $contribsCIDRLimit[
'IPv4'];
199 } elseif ( IPUtils::isIPv6( $ipRange ) ) {
201 $cidrLimit = $contribsCIDRLimit[
'IPv6'];
203 $this->
dieWithError( [
'apierror-invalidiprange', $ipRange ],
'invalidiprange' );
205 $range = IPUtils::parseCIDR( $ipRange )[1];
206 if ( $range ===
false ) {
207 $this->
dieWithError( [
'apierror-invalidiprange', $ipRange ],
'invalidiprange' );
208 } elseif ( $range < $cidrLimit ) {
209 $this->
dieWithError( [
'apierror-cidrtoobroad', $type, $cidrLimit ] );
212 $this->multiUserMode =
true;
213 $this->orderBy =
'name';
218 $userIter = call_user_func(
function () use ( $dbSecondary, $sort, $op, $fname, $ipRange ) {
219 [ $start, $end ] = IPUtils::parseRange( $ipRange );
220 if ( $this->params[
'continue'] !==
null ) {
222 [
'string',
'string',
'string',
'int' ] );
224 $fromName = $continue[1];
225 $fromIPHex = IPUtils::toHex( $fromName );
237 $res = $dbSecondary->newSelectQueryBuilder()
238 ->select(
'ipc_hex' )
239 ->from(
'ip_changes' )
240 ->where( $dbSecondary->expr(
'ipc_hex',
'>=', $start )->and(
'ipc_hex',
'<=', $end ) )
241 ->groupBy(
'ipc_hex' )
242 ->orderBy(
'ipc_hex', $sort )
249 foreach ( $res as $row ) {
250 $ipAddr = IPUtils::formatHex( $row->ipc_hex );
251 if ( ++$count >= $limit ) {
255 yield UserIdentityValue::newAnonymous( $ipAddr );
257 }
while ( $fromName !==
false );
264 if ( !count( $this->params[
'user'] ) ) {
267 [
'apierror-paramempty', $encParamName ],
"paramempty_$encParamName"
270 foreach ( $this->params[
'user'] as $u ) {
274 [
'apierror-paramempty', $encParamName ],
"paramempty_$encParamName"
278 if ( $this->userNameUtils->isIP( $u ) || ExternalUserNames::isExternal( $u ) ) {
281 $name = $this->userNameUtils->getCanonical( $u );
282 if ( $name ===
false ) {
285 [
'apierror-baduser', $encParamName,
wfEscapeWikiText( $u ) ],
"baduser_$encParamName"
288 $names[$name] =
null;
292 $this->orderBy =
'actor';
293 $this->multiUserMode = count( $names ) > 1;
296 if ( $this->multiUserMode && $this->params[
'continue'] !==
null ) {
298 [
'string',
'int',
'string',
'int' ] );
300 $fromId = $continue[1];
303 $userIter = $this->userIdentityLookup
304 ->newSelectQueryBuilder()
305 ->caller( __METHOD__ )
306 ->whereUserNames( array_keys( $names ) )
307 ->orderByName( $sort )
308 ->where( $fromId ? $dbSecondary->buildComparison( $op, [
'actor_id' => $fromId ] ) : [] )
309 ->fetchUserIdentities();
310 $batchSize = count( $names );
314 $limit = $this->params[
'limit'];
316 while ( $userIter->valid() ) {
318 while ( count( $users ) < $batchSize && $userIter->valid() ) {
319 $users[] = $userIter->current();
324 $this->prepareQuery( $users, $limit - $count );
325 $res = $this->
select( __METHOD__, [], $hookData );
327 if ( $this->fld_title ) {
331 if ( $this->fld_sizediff ) {
333 foreach ( $res as $row ) {
334 if ( $row->rev_parent_id ) {
335 $revIds[] = (int)$row->rev_parent_id;
338 $this->parentLens = $this->revisionStore->getRevisionSizes( $revIds );
341 foreach ( $res as $row ) {
342 if ( ++$count > $limit ) {
349 $vals = $this->extractRowInfo( $row );
350 $fit = $this->
processRow( $row, $vals, $hookData ) &&
367 private function prepareQuery( array $users, $limit ) {
369 $db = $this->
getDB();
371 $queryBuilder = $this->revisionStore->newSelectQueryBuilder( $db )->joinComment()->joinPage();
372 $revWhere = $this->actorMigration->getWhere( $db,
'rev_user', $users );
374 $orderUserField =
'rev_actor';
375 $userField = $this->orderBy ===
'actor' ?
'rev_actor' :
'actor_name';
376 $tsField =
'rev_timestamp';
380 $this->
addWhere( $revWhere[
'conds'] );
382 if ( isset( $revWhere[
'orconds'][
'newactor'] ) ) {
383 $this->
addOption(
'USE INDEX', [
'revision' =>
'rev_actor_timestamp' ] );
387 if ( $this->params[
'continue'] !==
null ) {
388 if ( $this->multiUserMode ) {
390 [
'string',
'string',
'timestamp',
'int' ] );
391 $modeFlag = array_shift( $continue );
393 $encUser = array_shift( $continue );
396 [
'timestamp',
'int' ] );
398 $op = ( $this->params[
'dir'] ==
'older' ?
'<=' :
'>=' );
399 if ( $this->multiUserMode ) {
400 $this->
addWhere( $db->buildComparison( $op, [
402 $userField => $encUser,
403 $tsField => $db->timestamp( $continue[0] ),
404 $idField => $continue[1],
407 $this->
addWhere( $db->buildComparison( $op, [
408 $tsField => $db->timestamp( $continue[0] ),
409 $idField => $continue[1],
416 if ( !$this->
getAuthority()->isAllowed(
'deletedhistory' ) ) {
417 $bitmask = RevisionRecord::DELETED_USER;
418 } elseif ( !$this->
getAuthority()->isAllowedAny(
'suppressrevision',
'viewsuppressed' ) ) {
419 $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
424 $this->
addWhere( $db->bitAnd(
'rev_deleted', $bitmask ) .
" != $bitmask" );
428 if ( count( $users ) > 1 ) {
429 $this->
addWhereRange( $orderUserField, $this->params[
'dir'],
null,
null );
434 $this->params[
'dir'], $this->params[
'start'], $this->params[
'end'] );
437 $this->
addWhereRange( $idField, $this->params[
'dir'],
null,
null );
439 $this->
addWhereFld(
'page_namespace', $this->params[
'namespace'] );
441 $show = $this->params[
'show'];
442 if ( $this->params[
'toponly'] ) {
445 if ( $show !==
null ) {
446 $show = array_fill_keys( $show,
true );
448 if ( ( isset( $show[
'minor'] ) && isset( $show[
'!minor'] ) )
449 || ( isset( $show[
'patrolled'] ) && isset( $show[
'!patrolled'] ) )
450 || ( isset( $show[
'autopatrolled'] ) && isset( $show[
'!autopatrolled'] ) )
451 || ( isset( $show[
'autopatrolled'] ) && isset( $show[
'!patrolled'] ) )
452 || ( isset( $show[
'top'] ) && isset( $show[
'!top'] ) )
453 || ( isset( $show[
'new'] ) && isset( $show[
'!new'] ) )
458 $this->
addWhereIf( [
'rev_minor_edit' => 0 ], isset( $show[
'!minor'] ) );
459 $this->
addWhereIf( $db->expr(
'rev_minor_edit',
'!=', 0 ), isset( $show[
'minor'] ) );
461 [
'rc_patrolled' => RecentChange::PRC_UNPATROLLED ],
462 isset( $show[
'!patrolled'] )
465 $db->expr(
'rc_patrolled',
'!=', RecentChange::PRC_UNPATROLLED ),
466 isset( $show[
'patrolled'] )
469 $db->expr(
'rc_patrolled',
'!=', RecentChange::PRC_AUTOPATROLLED ),
470 isset( $show[
'!autopatrolled'] )
473 [
'rc_patrolled' => RecentChange::PRC_AUTOPATROLLED ],
474 isset( $show[
'autopatrolled'] )
476 $this->
addWhereIf( $idField .
' != page_latest', isset( $show[
'!top'] ) );
477 $this->
addWhereIf( $idField .
' = page_latest', isset( $show[
'top'] ) );
478 $this->
addWhereIf( $db->expr(
'rev_parent_id',
'!=', 0 ), isset( $show[
'!new'] ) );
479 $this->
addWhereIf( [
'rev_parent_id' => 0 ], isset( $show[
'new'] ) );
483 if ( isset( $show[
'patrolled'] ) || isset( $show[
'!patrolled'] ) ||
484 isset( $show[
'autopatrolled'] ) || isset( $show[
'!autopatrolled'] ) || $this->fld_patrolled
487 if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
488 $this->
dieWithError(
'apierror-permissiondenied-patrolflag',
'permissiondenied' );
491 $isFilterset = isset( $show[
'patrolled'] ) || isset( $show[
'!patrolled'] ) ||
492 isset( $show[
'autopatrolled'] ) || isset( $show[
'!autopatrolled'] );
495 $isFilterset ?
'JOIN' :
'LEFT JOIN',
496 [
'rc_this_oldid = ' . $idField ]
500 $this->
addFieldsIf(
'rc_patrolled', $this->fld_patrolled );
502 if ( $this->fld_tags ) {
504 'ts_tags' => $this->changeTagsStore->makeTagSummarySubquery(
'revision' )
508 if ( isset( $this->params[
'tag'] ) ) {
511 [
'change_tag' => [
'JOIN', [ $idField .
' = ct_rev_id' ] ] ]
514 $this->
addWhereFld(
'ct_tag_id', $this->changeTagDefStore->getId( $this->params[
'tag'] ) );
515 }
catch ( NameTableAccessException ) {
521 'MAX_EXECUTION_TIME',
532 private function extractRowInfo( $row ) {
536 if ( $row->rev_deleted & RevisionRecord::DELETED_TEXT ) {
537 $vals[
'texthidden'] =
true;
542 $vals[
'userid'] = (int)$row->rev_user;
543 $vals[
'user'] = $row->rev_user_text;
544 if ( $row->rev_deleted & RevisionRecord::DELETED_USER ) {
545 $vals[
'userhidden'] =
true;
548 if ( $this->fld_ids ) {
549 $vals[
'pageid'] = (int)$row->rev_page;
550 $vals[
'revid'] = (int)$row->rev_id;
552 if ( $row->rev_parent_id !==
null ) {
553 $vals[
'parentid'] = (int)$row->rev_parent_id;
559 if ( $this->fld_title ) {
563 if ( $this->fld_timestamp ) {
564 $vals[
'timestamp'] =
wfTimestamp( TS_ISO_8601, $row->rev_timestamp );
567 if ( $this->fld_flags ) {
568 $vals[
'new'] = $row->rev_parent_id == 0 && $row->rev_parent_id !==
null;
569 $vals[
'minor'] = (bool)$row->rev_minor_edit;
570 $vals[
'top'] = $row->page_latest == $row->rev_id;
573 if ( $this->fld_comment || $this->fld_parsedcomment ) {
574 if ( $row->rev_deleted & RevisionRecord::DELETED_COMMENT ) {
575 $vals[
'commenthidden'] =
true;
579 $userCanView = RevisionRecord::userCanBitfield(
581 RevisionRecord::DELETED_COMMENT, $this->getAuthority()
584 if ( $userCanView ) {
585 $comment = $this->commentStore->getComment(
'rev_comment', $row )->text;
586 if ( $this->fld_comment ) {
587 $vals[
'comment'] = $comment;
590 if ( $this->fld_parsedcomment ) {
591 $vals[
'parsedcomment'] = $this->commentFormatter->format( $comment, $title );
596 if ( $this->fld_patrolled ) {
597 $vals[
'patrolled'] = $row->rc_patrolled != RecentChange::PRC_UNPATROLLED;
598 $vals[
'autopatrolled'] = $row->rc_patrolled == RecentChange::PRC_AUTOPATROLLED;
601 if ( $this->fld_size && $row->rev_len !==
null ) {
602 $vals[
'size'] = (int)$row->rev_len;
605 if ( $this->fld_sizediff
606 && $row->rev_len !==
null
607 && $row->rev_parent_id !==
null
609 $parentLen = $this->parentLens[$row->rev_parent_id] ?? 0;
610 $vals[
'sizediff'] = (int)$row->rev_len - $parentLen;
613 if ( $this->fld_tags ) {
614 if ( $row->ts_tags ) {
615 $tags = explode(
',', $row->ts_tags );
617 $vals[
'tags'] = $tags;
623 if ( $anyHidden && ( $row->rev_deleted & RevisionRecord::DELETED_RESTRICTED ) ) {
624 $vals[
'suppressed'] =
true;
630 private function continueStr( \stdClass $row ): string {
631 if ( $this->multiUserMode ) {
632 switch ( $this->orderBy ) {
634 return "name|$row->rev_user_text|$row->rev_timestamp|$row->rev_id";
636 return "actor|$row->rev_actor|$row->rev_timestamp|$row->rev_id";
639 return "$row->rev_timestamp|$row->rev_id";
647 return 'anon-public-user-private';
654 ParamValidator::PARAM_DEFAULT => 10,
655 ParamValidator::PARAM_TYPE =>
'limit',
656 IntegerDef::PARAM_MIN => 1,
661 ParamValidator::PARAM_TYPE =>
'timestamp'
664 ParamValidator::PARAM_TYPE =>
'timestamp'
670 ParamValidator::PARAM_TYPE =>
'user',
671 UserDef::PARAM_ALLOWED_USER_TYPES => [
'name',
'ip',
'temp',
'interwiki' ],
672 ParamValidator::PARAM_ISMULTI => true
675 ParamValidator::PARAM_TYPE =>
'integer',
676 ParamValidator::PARAM_ISMULTI => true
678 'userprefix' =>
null,
681 ParamValidator::PARAM_DEFAULT =>
'older',
682 ParamValidator::PARAM_TYPE => [
688 'newer' =>
'api-help-paramvalue-direction-newer',
689 'older' =>
'api-help-paramvalue-direction-older',
693 ParamValidator::PARAM_ISMULTI =>
true,
694 ParamValidator::PARAM_TYPE =>
'namespace'
697 ParamValidator::PARAM_ISMULTI =>
true,
698 ParamValidator::PARAM_DEFAULT =>
'ids|title|timestamp|comment|size|flags',
699 ParamValidator::PARAM_TYPE => [
714 ParamValidator::PARAM_ISMULTI =>
true,
715 ParamValidator::PARAM_TYPE => [
728 'apihelp-query+usercontribs-param-show',
734 ParamValidator::PARAM_DEFAULT =>
false,
735 ParamValidator::PARAM_DEPRECATED =>
true,
743 'action=query&list=usercontribs&ucuser=Example'
744 =>
'apihelp-query+usercontribs-example-user',
745 'action=query&list=usercontribs&ucuserprefix=192.0.2.'
746 =>
'apihelp-query+usercontribs-example-ipprefix',
752 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Usercontribs';
757class_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()