80 parent::__construct( $query, $moduleName,
'uc' );
81 $this->commentStore = $commentStore;
82 $this->userIdentityLookup = $userIdentityLookup;
83 $this->userNameUtils = $userNameUtils;
84 $this->revisionStore = $revisionStore;
85 $this->changeTagDefStore = $changeTagDefStore;
86 $this->actorMigration = $actorMigration;
87 $this->commentFormatter = $commentFormatter;
91 private bool $multiUserMode;
92 private string $orderBy;
93 private array $parentLens;
95 private bool $fld_ids =
false;
96 private bool $fld_title =
false;
97 private bool $fld_timestamp =
false;
98 private bool $fld_comment =
false;
99 private bool $fld_parsedcomment =
false;
100 private bool $fld_flags =
false;
101 private bool $fld_patrolled =
false;
102 private bool $fld_tags =
false;
103 private bool $fld_size =
false;
104 private bool $fld_sizediff =
false;
110 $prop = array_fill_keys( $this->params[
'prop'],
true );
111 $this->fld_ids = isset( $prop[
'ids'] );
112 $this->fld_title = isset( $prop[
'title'] );
113 $this->fld_comment = isset( $prop[
'comment'] );
114 $this->fld_parsedcomment = isset( $prop[
'parsedcomment'] );
115 $this->fld_size = isset( $prop[
'size'] );
116 $this->fld_sizediff = isset( $prop[
'sizediff'] );
117 $this->fld_flags = isset( $prop[
'flags'] );
118 $this->fld_timestamp = isset( $prop[
'timestamp'] );
119 $this->fld_patrolled = isset( $prop[
'patrolled'] );
120 $this->fld_tags = isset( $prop[
'tags'] );
122 $dbSecondary = $this->
getDB();
124 $sort = ( $this->params[
'dir'] ==
'newer' ?
125 SelectQueryBuilder::SORT_ASC : SelectQueryBuilder::SORT_DESC );
126 $op = ( $this->params[
'dir'] ==
'older' ?
'<=' :
'>=' );
132 if ( isset( $this->params[
'userprefix'] ) ) {
133 $this->multiUserMode =
true;
134 $this->orderBy =
'name';
140 $userIter = call_user_func(
function () use ( $dbSecondary, $sort, $op, $fname ) {
142 if ( $this->params[
'continue'] !==
null ) {
144 [
'string',
'string',
'string',
'int' ] );
146 $fromName = $continue[1];
151 $usersBatch = $this->userIdentityLookup
152 ->newSelectQueryBuilder()
155 ->whereUserNamePrefix( $this->params[
'userprefix'] )
156 ->where( $fromName !==
false
157 ? $dbSecondary->buildComparison( $op, [
'actor_name' => $fromName ] )
159 ->orderByName( $sort )
160 ->fetchUserIdentities();
164 foreach ( $usersBatch as $user ) {
165 if ( ++$count >= $limit ) {
166 $fromName = $user->getName();
171 }
while ( $fromName !==
false );
176 } elseif ( isset( $this->params[
'userids'] ) ) {
177 if ( $this->params[
'userids'] === [] ) {
179 $this->
dieWithError( [
'apierror-paramempty', $encParamName ],
"paramempty_$encParamName" );
183 foreach ( $this->params[
'userids'] as $uid ) {
185 $this->
dieWithError( [
'apierror-invaliduserid', $uid ],
'invaliduserid' );
190 $this->orderBy =
'actor';
191 $this->multiUserMode = count( $ids ) > 1;
194 if ( $this->multiUserMode && $this->params[
'continue'] !==
null ) {
196 [
'string',
'int',
'string',
'int' ] );
198 $fromId = $continue[1];
201 $userIter = $this->userIdentityLookup
202 ->newSelectQueryBuilder()
203 ->caller( __METHOD__ )
204 ->whereUserIds( $ids )
205 ->orderByUserId( $sort )
206 ->where( $fromId ? $dbSecondary->buildComparison( $op, [
'actor_id' => $fromId ] ) : [] )
207 ->fetchUserIdentities();
208 $batchSize = count( $ids );
209 } elseif ( isset( $this->params[
'iprange'] ) ) {
211 $ipRange = $this->params[
'iprange'];
212 $contribsCIDRLimit = $this->
getConfig()->get( MainConfigNames::RangeContributionsCIDRLimit );
213 if ( IPUtils::isIPv4( $ipRange ) ) {
215 $cidrLimit = $contribsCIDRLimit[
'IPv4'];
216 } elseif ( IPUtils::isIPv6( $ipRange ) ) {
218 $cidrLimit = $contribsCIDRLimit[
'IPv6'];
220 $this->
dieWithError( [
'apierror-invalidiprange', $ipRange ],
'invalidiprange' );
222 $range = IPUtils::parseCIDR( $ipRange )[1];
223 if ( $range ===
false ) {
224 $this->
dieWithError( [
'apierror-invalidiprange', $ipRange ],
'invalidiprange' );
225 } elseif ( $range < $cidrLimit ) {
226 $this->
dieWithError( [
'apierror-cidrtoobroad', $type, $cidrLimit ] );
229 $this->multiUserMode =
true;
230 $this->orderBy =
'name';
235 $userIter = call_user_func(
function () use ( $dbSecondary, $sort, $op, $fname, $ipRange ) {
236 [ $start, $end ] = IPUtils::parseRange( $ipRange );
237 if ( $this->params[
'continue'] !==
null ) {
239 [
'string',
'string',
'string',
'int' ] );
241 $fromName = $continue[1];
242 $fromIPHex = IPUtils::toHex( $fromName );
254 $res = $dbSecondary->newSelectQueryBuilder()
255 ->select(
'ipc_hex' )
256 ->from(
'ip_changes' )
257 ->where( $dbSecondary->expr(
'ipc_hex',
'>=', $start )->and(
'ipc_hex',
'<=', $end ) )
258 ->groupBy(
'ipc_hex' )
259 ->orderBy(
'ipc_hex', $sort )
266 foreach ( $res as $row ) {
267 $ipAddr = IPUtils::formatHex( $row->ipc_hex );
268 if ( ++$count >= $limit ) {
272 yield UserIdentityValue::newAnonymous( $ipAddr );
274 }
while ( $fromName !==
false );
281 if ( !count( $this->params[
'user'] ) ) {
284 [
'apierror-paramempty', $encParamName ],
"paramempty_$encParamName"
287 foreach ( $this->params[
'user'] as $u ) {
291 [
'apierror-paramempty', $encParamName ],
"paramempty_$encParamName"
295 if ( $this->userNameUtils->isIP( $u ) || ExternalUserNames::isExternal( $u ) ) {
298 $name = $this->userNameUtils->getCanonical( $u );
299 if ( $name ===
false ) {
302 [
'apierror-baduser', $encParamName,
wfEscapeWikiText( $u ) ],
"baduser_$encParamName"
305 $names[$name] =
null;
309 $this->orderBy =
'actor';
310 $this->multiUserMode = count( $names ) > 1;
313 if ( $this->multiUserMode && $this->params[
'continue'] !==
null ) {
315 [
'string',
'int',
'string',
'int' ] );
317 $fromId = $continue[1];
320 $userIter = $this->userIdentityLookup
321 ->newSelectQueryBuilder()
322 ->caller( __METHOD__ )
323 ->whereUserNames( array_keys( $names ) )
324 ->orderByName( $sort )
325 ->where( $fromId ? $dbSecondary->buildComparison( $op, [
'actor_id' => $fromId ] ) : [] )
326 ->fetchUserIdentities();
327 $batchSize = count( $names );
331 $limit = $this->params[
'limit'];
333 while ( $userIter->valid() ) {
335 while ( count( $users ) < $batchSize && $userIter->valid() ) {
336 $users[] = $userIter->current();
341 $this->prepareQuery( $users, $limit - $count );
342 $res = $this->
select( __METHOD__, [], $hookData );
344 if ( $this->fld_title ) {
348 if ( $this->fld_sizediff ) {
350 foreach ( $res as $row ) {
351 if ( $row->rev_parent_id ) {
352 $revIds[] = (int)$row->rev_parent_id;
355 $this->parentLens = $this->revisionStore->getRevisionSizes( $revIds );
358 foreach ( $res as $row ) {
359 if ( ++$count > $limit ) {
366 $vals = $this->extractRowInfo( $row );
367 $fit = $this->
processRow( $row, $vals, $hookData ) &&
384 private function prepareQuery( array $users, $limit ) {
386 $db = $this->
getDB();
388 $queryBuilder = $this->revisionStore->newSelectQueryBuilder( $db )->joinComment()->joinPage();
389 $revWhere = $this->actorMigration->getWhere( $db,
'rev_user', $users );
391 $orderUserField =
'rev_actor';
392 $userField = $this->orderBy ===
'actor' ?
'rev_actor' :
'actor_name';
393 $tsField =
'rev_timestamp';
397 $this->
addWhere( $revWhere[
'conds'] );
399 if ( isset( $revWhere[
'orconds'][
'newactor'] ) ) {
400 $this->
addOption(
'USE INDEX', [
'revision' =>
'rev_actor_timestamp' ] );
404 if ( $this->params[
'continue'] !==
null ) {
405 if ( $this->multiUserMode ) {
407 [
'string',
'string',
'timestamp',
'int' ] );
408 $modeFlag = array_shift( $continue );
410 $encUser = array_shift( $continue );
413 [
'timestamp',
'int' ] );
415 $op = ( $this->params[
'dir'] ==
'older' ?
'<=' :
'>=' );
416 if ( $this->multiUserMode ) {
417 $this->
addWhere( $db->buildComparison( $op, [
419 $userField => $encUser,
420 $tsField => $db->timestamp( $continue[0] ),
421 $idField => $continue[1],
424 $this->
addWhere( $db->buildComparison( $op, [
425 $tsField => $db->timestamp( $continue[0] ),
426 $idField => $continue[1],
433 if ( !$this->
getAuthority()->isAllowed(
'deletedhistory' ) ) {
434 $bitmask = RevisionRecord::DELETED_USER;
435 } elseif ( !$this->
getAuthority()->isAllowedAny(
'suppressrevision',
'viewsuppressed' ) ) {
436 $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
441 $this->
addWhere( $db->bitAnd(
'rev_deleted', $bitmask ) .
" != $bitmask" );
445 if ( count( $users ) > 1 ) {
446 $this->
addWhereRange( $orderUserField, $this->params[
'dir'],
null,
null );
451 $this->params[
'dir'], $this->params[
'start'], $this->params[
'end'] );
454 $this->
addWhereRange( $idField, $this->params[
'dir'],
null,
null );
456 $this->
addWhereFld(
'page_namespace', $this->params[
'namespace'] );
458 $show = $this->params[
'show'];
459 if ( $this->params[
'toponly'] ) {
462 if ( $show !==
null ) {
463 $show = array_fill_keys( $show,
true );
465 if ( ( isset( $show[
'minor'] ) && isset( $show[
'!minor'] ) )
466 || ( isset( $show[
'patrolled'] ) && isset( $show[
'!patrolled'] ) )
467 || ( isset( $show[
'autopatrolled'] ) && isset( $show[
'!autopatrolled'] ) )
468 || ( isset( $show[
'autopatrolled'] ) && isset( $show[
'!patrolled'] ) )
469 || ( isset( $show[
'top'] ) && isset( $show[
'!top'] ) )
470 || ( isset( $show[
'new'] ) && isset( $show[
'!new'] ) )
475 $this->
addWhereIf( [
'rev_minor_edit' => 0 ], isset( $show[
'!minor'] ) );
476 $this->
addWhereIf( $db->expr(
'rev_minor_edit',
'!=', 0 ), isset( $show[
'minor'] ) );
478 [
'rc_patrolled' => RecentChange::PRC_UNPATROLLED ],
479 isset( $show[
'!patrolled'] )
482 $db->expr(
'rc_patrolled',
'!=', RecentChange::PRC_UNPATROLLED ),
483 isset( $show[
'patrolled'] )
486 $db->expr(
'rc_patrolled',
'!=', RecentChange::PRC_AUTOPATROLLED ),
487 isset( $show[
'!autopatrolled'] )
490 [
'rc_patrolled' => RecentChange::PRC_AUTOPATROLLED ],
491 isset( $show[
'autopatrolled'] )
493 $this->
addWhereIf( $idField .
' != page_latest', isset( $show[
'!top'] ) );
494 $this->
addWhereIf( $idField .
' = page_latest', isset( $show[
'top'] ) );
495 $this->
addWhereIf( $db->expr(
'rev_parent_id',
'!=', 0 ), isset( $show[
'!new'] ) );
496 $this->
addWhereIf( [
'rev_parent_id' => 0 ], isset( $show[
'new'] ) );
500 if ( isset( $show[
'patrolled'] ) || isset( $show[
'!patrolled'] ) ||
501 isset( $show[
'autopatrolled'] ) || isset( $show[
'!autopatrolled'] ) || $this->fld_patrolled
504 if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
505 $this->
dieWithError(
'apierror-permissiondenied-patrolflag',
'permissiondenied' );
508 $isFilterset = isset( $show[
'patrolled'] ) || isset( $show[
'!patrolled'] ) ||
509 isset( $show[
'autopatrolled'] ) || isset( $show[
'!autopatrolled'] );
512 $isFilterset ?
'JOIN' :
'LEFT JOIN',
513 [
'rc_this_oldid = ' . $idField ]
517 $this->
addFieldsIf(
'rc_patrolled', $this->fld_patrolled );
519 if ( $this->fld_tags ) {
523 if ( isset( $this->params[
'tag'] ) ) {
526 [
'change_tag' => [
'JOIN', [ $idField .
' = ct_rev_id' ] ] ]
529 $this->
addWhereFld(
'ct_tag_id', $this->changeTagDefStore->getId( $this->params[
'tag'] ) );
536 'MAX_EXECUTION_TIME',
537 $this->
getConfig()->
get( MainConfigNames::MaxExecutionTimeForExpensiveQueries )
547 private function extractRowInfo( $row ) {
551 if ( $row->rev_deleted & RevisionRecord::DELETED_TEXT ) {
552 $vals[
'texthidden'] =
true;
557 $vals[
'userid'] = (int)$row->rev_user;
558 $vals[
'user'] = $row->rev_user_text;
559 if ( $row->rev_deleted & RevisionRecord::DELETED_USER ) {
560 $vals[
'userhidden'] =
true;
563 if ( $this->fld_ids ) {
564 $vals[
'pageid'] = (int)$row->rev_page;
565 $vals[
'revid'] = (int)$row->rev_id;
567 if ( $row->rev_parent_id !==
null ) {
568 $vals[
'parentid'] = (int)$row->rev_parent_id;
572 $title = Title::makeTitle( $row->page_namespace, $row->page_title );
574 if ( $this->fld_title ) {
578 if ( $this->fld_timestamp ) {
579 $vals[
'timestamp'] =
wfTimestamp( TS_ISO_8601, $row->rev_timestamp );
582 if ( $this->fld_flags ) {
583 $vals[
'new'] = $row->rev_parent_id == 0 && $row->rev_parent_id !==
null;
584 $vals[
'minor'] = (bool)$row->rev_minor_edit;
585 $vals[
'top'] = $row->page_latest == $row->rev_id;
588 if ( $this->fld_comment || $this->fld_parsedcomment ) {
589 if ( $row->rev_deleted & RevisionRecord::DELETED_COMMENT ) {
590 $vals[
'commenthidden'] =
true;
594 $userCanView = RevisionRecord::userCanBitfield(
596 RevisionRecord::DELETED_COMMENT, $this->getAuthority()
599 if ( $userCanView ) {
600 $comment = $this->commentStore->getComment(
'rev_comment', $row )->text;
601 if ( $this->fld_comment ) {
602 $vals[
'comment'] = $comment;
605 if ( $this->fld_parsedcomment ) {
606 $vals[
'parsedcomment'] = $this->commentFormatter->format( $comment, $title );
611 if ( $this->fld_patrolled ) {
616 if ( $this->fld_size && $row->rev_len !==
null ) {
617 $vals[
'size'] = (int)$row->rev_len;
620 if ( $this->fld_sizediff
621 && $row->rev_len !==
null
622 && $row->rev_parent_id !==
null
624 $parentLen = $this->parentLens[$row->rev_parent_id] ?? 0;
625 $vals[
'sizediff'] = (int)$row->rev_len - $parentLen;
628 if ( $this->fld_tags ) {
629 if ( $row->ts_tags ) {
630 $tags = explode(
',', $row->ts_tags );
632 $vals[
'tags'] = $tags;
638 if ( $anyHidden && ( $row->rev_deleted & RevisionRecord::DELETED_RESTRICTED ) ) {
639 $vals[
'suppressed'] =
true;
645 private function continueStr( $row ) {
646 if ( $this->multiUserMode ) {
647 switch ( $this->orderBy ) {
649 return "name|$row->rev_user_text|$row->rev_timestamp|$row->rev_id";
651 return "actor|$row->rev_actor|$row->rev_timestamp|$row->rev_id";
654 return "$row->rev_timestamp|$row->rev_id";
661 return 'anon-public-user-private';
667 ParamValidator::PARAM_DEFAULT => 10,
668 ParamValidator::PARAM_TYPE =>
'limit',
669 IntegerDef::PARAM_MIN => 1,
674 ParamValidator::PARAM_TYPE =>
'timestamp'
677 ParamValidator::PARAM_TYPE =>
'timestamp'
683 ParamValidator::PARAM_TYPE =>
'user',
684 UserDef::PARAM_ALLOWED_USER_TYPES => [
'name',
'ip',
'temp',
'interwiki' ],
685 ParamValidator::PARAM_ISMULTI => true
688 ParamValidator::PARAM_TYPE =>
'integer',
689 ParamValidator::PARAM_ISMULTI => true
691 'userprefix' =>
null,
694 ParamValidator::PARAM_DEFAULT =>
'older',
695 ParamValidator::PARAM_TYPE => [
701 'newer' =>
'api-help-paramvalue-direction-newer',
702 'older' =>
'api-help-paramvalue-direction-older',
706 ParamValidator::PARAM_ISMULTI =>
true,
707 ParamValidator::PARAM_TYPE =>
'namespace'
710 ParamValidator::PARAM_ISMULTI =>
true,
711 ParamValidator::PARAM_DEFAULT =>
'ids|title|timestamp|comment|size|flags',
712 ParamValidator::PARAM_TYPE => [
727 ParamValidator::PARAM_ISMULTI =>
true,
728 ParamValidator::PARAM_TYPE => [
741 'apihelp-query+usercontribs-param-show',
742 $this->
getConfig()->get( MainConfigNames::RCMaxAge )
747 ParamValidator::PARAM_DEFAULT =>
false,
748 ParamValidator::PARAM_DEPRECATED =>
true,
755 'action=query&list=usercontribs&ucuser=Example'
756 =>
'apihelp-query+usercontribs-example-user',
757 'action=query&list=usercontribs&ucuserprefix=192.0.2.'
758 =>
'apihelp-query+usercontribs-example-ipprefix',
763 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Usercontribs';
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.
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
encodeParamName( $paramName)
This method mangles parameter name based on the prefix supplied to the constructor.
dieContinueUsageIf( $condition)
Die with the 'badcontinue' error.
requireOnlyOneParameter( $params,... $required)
Die if 0 or more than one of a certain set of parameters is set and not false.
parseContinueParamOrDie(string $continue, array $types)
Parse the 'continue' parameter in the usual format and validate the types of each part,...
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, or 'string' with PARAM_ISMULTI,...
const LIMIT_BIG1
Fast query, standard limit.
getResult()
Get the result object.
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
const LIMIT_BIG2
Fast query, apihighlimits limit.
getModuleName()
Get the name of the module being executed by this instance.
This is a base class for all Query modules.
static addTitleInfo(&$arr, $title, $prefix='')
Add information (title and namespace) about a Title object to a result array.
setContinueEnumParameter( $paramName, $paramValue)
Set a query-continue value.
processRow( $row, array &$data, array &$hookData)
Call the ApiQueryBaseProcessRow hook.
resetQueryParams()
Blank the internal arrays with query parameters.
addWhereIf( $value, $condition)
Same as addWhere(), but add the WHERE clauses only if a condition is met.
addWhereRange( $field, $dir, $start, $end, $sort=true)
Add a WHERE clause corresponding to a range, and an ORDER BY clause to sort in the right direction.
getQueryBuilder()
Get the SelectQueryBuilder.
addFields( $value)
Add a set of fields to select to the internal array.
addOption( $name, $value=null)
Add an option such as LIMIT or USE INDEX.
addTables( $tables, $alias=null)
Add a set of tables to the internal array.
addTimestampWhereRange( $field, $dir, $start, $end, $sort=true)
Add a WHERE clause corresponding to a range, similar to addWhereRange, but converts $start and $end t...
getDB()
Get the Query database connection (read-only)
executeGenderCacheFromResultWrapper(IResultWrapper $res, $fname=__METHOD__, $fieldPrefix='page')
Preprocess the result set to fill the GenderCache with the necessary information before using self::a...
select( $method, $extraQuery=[], array &$hookData=null)
Execute a SELECT query based on the values in the internal arrays.
addFieldsIf( $value, $condition)
Same as addFields(), but add the fields only if a condition is met.
addJoinConds( $join_conds)
Add a set of JOIN conditions to the internal array.
addWhereFld( $field, $value)
Equivalent to addWhere( [ $field => $value ] )
addWhere( $value)
Add a set of WHERE clauses to the internal array.
This query action adds a list of a specified user's contributions to the output.
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
getCacheMode( $params)
Get the cache mode for the data generated by this module.
getHelpUrls()
Return links to more detailed help pages about the module.
__construct(ApiQuery $query, $moduleName, CommentStore $commentStore, UserIdentityLookup $userIdentityLookup, UserNameUtils $userNameUtils, RevisionStore $revisionStore, NameTableStore $changeTagDefStore, ActorMigration $actorMigration, CommentFormatter $commentFormatter)
getExamplesMessages()
Returns usage examples for this module.
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
This is the main query class.
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
A class containing constants representing the names of configuration variables.