MediaWiki master
ApiQueryUserContribs.php
Go to the documentation of this file.
1<?php
9namespace MediaWiki\Api;
10
28use stdClass;
29use Wikimedia\IPUtils;
33use Wikimedia\Timestamp\TimestampFormat as TS;
34
41
42 public function __construct(
43 ApiQuery $query,
44 string $moduleName,
45 private readonly CommentStore $commentStore,
46 private readonly UserIdentityLookup $userIdentityLookup,
47 private readonly UserNameUtils $userNameUtils,
48 private readonly RevisionStore $revisionStore,
49 private readonly NameTableStore $changeTagDefStore,
50 private readonly ChangeTagsStore $changeTagsStore,
51 private readonly ActorMigration $actorMigration,
52 private readonly CommentFormatter $commentFormatter,
53 ) {
54 parent::__construct( $query, $moduleName, 'uc' );
55 }
56
57 private array $params;
58 private bool $multiUserMode;
59 private string $orderBy;
60 private array $parentLens;
61
63 private array $prop = [];
64
65 public function execute() {
66 // Parse some parameters
67 $this->params = $this->extractRequestParams();
68
69 $this->prop = array_fill_keys( $this->params['prop'], true );
70
71 $dbSecondary = $this->getDB(); // any random replica DB
72
73 $sort = ( $this->params['dir'] == 'newer' ?
74 SelectQueryBuilder::SORT_ASC : SelectQueryBuilder::SORT_DESC );
75 $op = ( $this->params['dir'] == 'older' ? '<=' : '>=' );
76
77 // Create an Iterator that produces the UserIdentity objects we need, depending
78 // on which of the 'userprefix', 'userids', 'iprange', or 'user' params
79 // was specified.
80 $this->requireOnlyOneParameter( $this->params, 'userprefix', 'userids', 'iprange', 'user' );
81 if ( isset( $this->params['userprefix'] ) ) {
82 $this->multiUserMode = true;
83 $this->orderBy = 'name';
84 $fname = __METHOD__;
85
86 // Because 'userprefix' might produce a huge number of users (e.g.
87 // a wiki with users "Test00000001" to "Test99999999"), use a
88 // generator with batched lookup and continuation.
89 $userIter = call_user_func( function () use ( $dbSecondary, $sort, $op, $fname ) {
90 $fromName = false;
91 if ( $this->params['continue'] !== null ) {
92 $continue = $this->parseContinueParamOrDie( $this->params['continue'],
93 [ 'string', 'string', 'string', 'int' ] );
94 $this->dieContinueUsageIf( $continue[0] !== 'name' );
95 $fromName = $continue[1];
96 }
97
98 $limit = 501;
99 do {
100 $usersBatch = $this->userIdentityLookup
101 ->newSelectQueryBuilder()
102 ->caller( $fname )
103 ->limit( $limit )
104 ->whereUserNamePrefix( $this->params['userprefix'] )
105 ->where( $fromName !== false
106 ? $dbSecondary->buildComparison( $op, [ 'actor_name' => $fromName ] )
107 : [] )
108 ->orderByName( $sort )
109 ->fetchUserIdentities();
110
111 $count = 0;
112 $fromName = false;
113 foreach ( $usersBatch as $user ) {
114 if ( ++$count >= $limit ) {
115 $fromName = $user->getName();
116 break;
117 }
118 yield $user;
119 }
120 } while ( $fromName !== false );
121 } );
122 // Do the actual sorting client-side, because otherwise
123 // prepareQuery might try to sort by actor and confuse everything.
124 $batchSize = 1;
125 } elseif ( isset( $this->params['userids'] ) ) {
126 if ( $this->params['userids'] === [] ) {
127 $encParamName = $this->encodeParamName( 'userids' );
128 $this->dieWithError( [ 'apierror-paramempty', $encParamName ], "paramempty_$encParamName" );
129 }
130
131 $ids = [];
132 foreach ( $this->params['userids'] as $uid ) {
133 if ( $uid <= 0 ) {
134 $this->dieWithError( [ 'apierror-invaliduserid', $uid ], 'invaliduserid' );
135 }
136 $ids[] = $uid;
137 }
138
139 $this->orderBy = 'actor';
140 $this->multiUserMode = count( $ids ) > 1;
141
142 $fromId = false;
143 if ( $this->multiUserMode && $this->params['continue'] !== null ) {
144 $continue = $this->parseContinueParamOrDie( $this->params['continue'],
145 [ 'string', 'int', 'string', 'int' ] );
146 $this->dieContinueUsageIf( $continue[0] !== 'actor' );
147 $fromId = $continue[1];
148 }
149
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'] ) ) {
159 // Make sure it is a valid range and within the CIDR limit
160 $ipRange = $this->params['iprange'];
161 $contribsCIDRLimit = $this->getConfig()->get( MainConfigNames::RangeContributionsCIDRLimit );
162 if ( IPUtils::isIPv4( $ipRange ) ) {
163 $type = 'IPv4';
164 $cidrLimit = $contribsCIDRLimit['IPv4'];
165 } elseif ( IPUtils::isIPv6( $ipRange ) ) {
166 $type = 'IPv6';
167 $cidrLimit = $contribsCIDRLimit['IPv6'];
168 } else {
169 $this->dieWithError( [ 'apierror-invalidiprange', $ipRange ], 'invalidiprange' );
170 }
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 ] );
176 }
177
178 $this->multiUserMode = true;
179 $this->orderBy = 'name';
180 $fname = __METHOD__;
181
182 // Because 'iprange' might produce a huge number of ips, use a
183 // generator with batched lookup and continuation.
184 $userIter = call_user_func( function () use ( $dbSecondary, $sort, $op, $fname, $ipRange ) {
185 [ $start, $end ] = IPUtils::parseRange( $ipRange );
186 if ( $this->params['continue'] !== null ) {
187 $continue = $this->parseContinueParamOrDie( $this->params['continue'],
188 [ 'string', 'string', 'string', 'int' ] );
189 $this->dieContinueUsageIf( $continue[0] !== 'name' );
190 $fromName = $continue[1];
191 $fromIPHex = IPUtils::toHex( $fromName );
192 $this->dieContinueUsageIf( $fromIPHex === false );
193 if ( $op == '<=' ) {
194 $end = $fromIPHex;
195 } else {
196 $start = $fromIPHex;
197 }
198 }
199
200 $limit = 501;
201
202 do {
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 )
209 ->limit( $limit )
210 ->caller( $fname )
211 ->fetchResultSet();
212
213 $count = 0;
214 $fromName = false;
215 foreach ( $res as $row ) {
216 $ipAddr = IPUtils::formatHex( $row->ipc_hex );
217 if ( ++$count >= $limit ) {
218 $fromName = $ipAddr;
219 break;
220 }
221 yield UserIdentityValue::newAnonymous( $ipAddr );
222 }
223 } while ( $fromName !== false );
224 } );
225 // Do the actual sorting client-side, because otherwise
226 // prepareQuery might try to sort by actor and confuse everything.
227 $batchSize = 1;
228 } else {
229 $names = [];
230 if ( !count( $this->params['user'] ) ) {
231 $encParamName = $this->encodeParamName( 'user' );
232 $this->dieWithError(
233 [ 'apierror-paramempty', $encParamName ], "paramempty_$encParamName"
234 );
235 }
236 foreach ( $this->params['user'] as $u ) {
237 if ( $u === '' ) {
238 $encParamName = $this->encodeParamName( 'user' );
239 $this->dieWithError(
240 [ 'apierror-paramempty', $encParamName ], "paramempty_$encParamName"
241 );
242 }
243
244 if ( $this->userNameUtils->isIP( $u ) || ExternalUserNames::isExternal( $u ) ) {
245 $names[$u] = null;
246 } else {
247 $name = $this->userNameUtils->getCanonical( $u );
248 if ( $name === false ) {
249 $encParamName = $this->encodeParamName( 'user' );
250 $this->dieWithError(
251 [ 'apierror-baduser', $encParamName, wfEscapeWikiText( $u ) ], "baduser_$encParamName"
252 );
253 }
254 $names[$name] = null;
255 }
256 }
257
258 $this->orderBy = 'actor';
259 $this->multiUserMode = count( $names ) > 1;
260
261 $fromId = false;
262 if ( $this->multiUserMode && $this->params['continue'] !== null ) {
263 $continue = $this->parseContinueParamOrDie( $this->params['continue'],
264 [ 'string', 'int', 'string', 'int' ] );
265 $this->dieContinueUsageIf( $continue[0] !== 'actor' );
266 $fromId = $continue[1];
267 }
268
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 );
277 }
278
279 $count = 0;
280 $limit = $this->params['limit'];
281 $userIter->rewind();
282 while ( $userIter->valid() ) {
283 $users = [];
284 while ( count( $users ) < $batchSize && $userIter->valid() ) {
285 $users[] = $userIter->current();
286 $userIter->next();
287 }
288
289 $hookData = [];
290 $this->prepareQuery( $users, $limit - $count );
291 $res = $this->select( __METHOD__, [], $hookData );
292
293 if ( isset( $this->prop['title'] ) ) {
294 $this->executeGenderCacheFromResultWrapper( $res, __METHOD__ );
295 }
296
297 if ( isset( $this->prop['sizediff'] ) ) {
298 $revIds = [];
299 foreach ( $res as $row ) {
300 if ( $row->rev_parent_id ) {
301 $revIds[] = (int)$row->rev_parent_id;
302 }
303 }
304 $this->parentLens = $this->revisionStore->getRevisionSizes( $revIds );
305 }
306
307 foreach ( $res as $row ) {
308 if ( ++$count > $limit ) {
309 // We've reached the one extra which shows that there are
310 // additional pages to be had. Stop here...
311 $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) );
312 break 2;
313 }
314
315 $vals = $this->extractRowInfo( $row );
316 $fit = $this->processRow( $row, $vals, $hookData ) &&
317 $this->getResult()->addValue( [ 'query', $this->getModuleName() ], null, $vals );
318 if ( !$fit ) {
319 $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) );
320 break 2;
321 }
322 }
323 }
324
325 $this->getResult()->addIndexedTagName( [ 'query', $this->getModuleName() ], 'item' );
326 }
327
333 private function prepareQuery( array $users, $limit ) {
334 $this->resetQueryParams();
335 $db = $this->getDB();
336
337 $queryBuilder = $this->revisionStore->newSelectQueryBuilder( $db )->joinComment()->joinPage();
338 $revWhere = $this->actorMigration->getWhere( $db, 'rev_user', $users );
339
340 $orderUserField = 'rev_actor';
341 $userField = $this->orderBy === 'actor' ? 'rev_actor' : 'actor_name';
342 $tsField = 'rev_timestamp';
343 $idField = 'rev_id';
344
345 $this->getQueryBuilder()->merge( $queryBuilder );
346 $this->addWhere( $revWhere['conds'] );
347 // Force the appropriate index to avoid bad query plans (T307815 and T307295)
348 if ( isset( $revWhere['orconds']['newactor'] ) ) {
349 $this->addOption( 'USE INDEX', [ 'revision' => 'rev_actor_timestamp' ] );
350 }
351
352 // Handle continue parameter
353 if ( $this->params['continue'] !== null ) {
354 if ( $this->multiUserMode ) {
355 $continue = $this->parseContinueParamOrDie( $this->params['continue'],
356 [ 'string', 'string', 'timestamp', 'int' ] );
357 $modeFlag = array_shift( $continue );
358 $this->dieContinueUsageIf( $modeFlag !== $this->orderBy );
359 $encUser = array_shift( $continue );
360 } else {
361 $continue = $this->parseContinueParamOrDie( $this->params['continue'],
362 [ 'timestamp', 'int' ] );
363 }
364 $op = ( $this->params['dir'] == 'older' ? '<=' : '>=' );
365 if ( $this->multiUserMode ) {
366 $this->addWhere( $db->buildComparison( $op, [
367 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable encUser is set when used
368 $userField => $encUser,
369 $tsField => $db->timestamp( $continue[0] ),
370 $idField => $continue[1],
371 ] ) );
372 } else {
373 $this->addWhere( $db->buildComparison( $op, [
374 $tsField => $db->timestamp( $continue[0] ),
375 $idField => $continue[1],
376 ] ) );
377 }
378 }
379
380 // Don't include any revisions where we're not supposed to be able to
381 // see the username.
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;
386 } else {
387 $bitmask = 0;
388 }
389 if ( $bitmask ) {
390 $this->addWhere( $db->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask" );
391 }
392
393 // Add the user field to ORDER BY if there are multiple users
394 if ( count( $users ) > 1 ) {
395 $this->addWhereRange( $orderUserField, $this->params['dir'], null, null );
396 }
397
398 // Then timestamp
399 $this->addTimestampWhereRange( $tsField,
400 $this->params['dir'], $this->params['start'], $this->params['end'] );
401
402 // Then rev_id for a total ordering
403 $this->addWhereRange( $idField, $this->params['dir'], null, null );
404
405 $this->addWhereFld( 'page_namespace', $this->params['namespace'] );
406
407 $show = $this->params['show'];
408 if ( $this->params['toponly'] ) { // deprecated/old param
409 $show[] = 'top';
410 }
411 if ( $show !== null ) {
413 $show = array_fill_keys( $show, true );
414
415 foreach ( $show as $key => $_ ) {
416 // If there is a negated and non-negated option the same time
417 if ( str_starts_with( $key, '!' ) && isset( $show[substr( $key, 1 )] ) ) {
418 $this->dieWithError( 'apierror-show' );
419 }
420 }
421 if ( isset( $show['autopatrolled'] ) && isset( $show['!patrolled'] ) ) {
422 $this->dieWithError( 'apierror-show' );
423 }
424
425 $this->addWhereIf( [ 'rev_minor_edit' => 0 ], isset( $show['!minor'] ) );
426 $this->addWhereIf( $db->expr( 'rev_minor_edit', '!=', 0 ), isset( $show['minor'] ) );
427 $this->addWhereIf(
428 [ 'rc_patrolled' => RecentChange::PRC_UNPATROLLED ],
429 isset( $show['!patrolled'] )
430 );
431 $this->addWhereIf(
432 $db->expr( 'rc_patrolled', '!=', RecentChange::PRC_UNPATROLLED ),
433 isset( $show['patrolled'] )
434 );
435 $this->addWhereIf(
436 $db->expr( 'rc_patrolled', '!=', RecentChange::PRC_AUTOPATROLLED ),
437 isset( $show['!autopatrolled'] )
438 );
439 $this->addWhereIf(
440 [ 'rc_patrolled' => RecentChange::PRC_AUTOPATROLLED ],
441 isset( $show['autopatrolled'] )
442 );
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'] ) );
447 }
448 $this->addOption( 'LIMIT', $limit + 1 );
449
450 if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ||
451 isset( $show['autopatrolled'] ) || isset( $show['!autopatrolled'] ) ||
452 isset( $this->prop['patrolled'] )
453 ) {
454 $user = $this->getUser();
455 if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
456 $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' );
457 }
458
459 $isFilterset = isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ||
460 isset( $show['autopatrolled'] ) || isset( $show['!autopatrolled'] );
461 $this->addTables( 'recentchanges' );
462 $this->addJoinConds( [ 'recentchanges' => [
463 $isFilterset ? 'JOIN' : 'LEFT JOIN',
464 [ 'rc_this_oldid = ' . $idField ]
465 ] ] );
466 }
467
468 $this->addFieldsIf( 'rc_patrolled', isset( $this->prop['patrolled'] ) );
469
470 if ( isset( $this->prop['tags'] ) ) {
471 $this->addFields( [
472 'ts_tags' => $this->changeTagsStore->makeTagSummarySubquery( 'revision' )
473 ] );
474 }
475
476 if ( isset( $this->params['tag'] ) ) {
477 $this->addTables( 'change_tag' );
478 $this->addJoinConds(
479 [ 'change_tag' => [ 'JOIN', [ $idField . ' = ct_rev_id' ] ] ]
480 );
481 try {
482 $this->addWhereFld( 'ct_tag_id', $this->changeTagDefStore->getId( $this->params['tag'] ) );
483 } catch ( NameTableAccessException ) {
484 // Return nothing.
485 $this->addWhere( '1=0' );
486 }
487 }
488 $this->addOption(
489 'MAX_EXECUTION_TIME',
491 );
492 }
493
500 private function extractRowInfo( $row ) {
501 $vals = [];
502 $anyHidden = false;
503
504 if ( $row->rev_deleted & RevisionRecord::DELETED_TEXT ) {
505 $vals['texthidden'] = true;
506 $anyHidden = true;
507 }
508
509 // Any rows where we can't view the user were filtered out in the query.
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;
514 $anyHidden = true;
515 }
516 if ( $this->prop['ids'] ?? false ) {
517 $vals['pageid'] = (int)$row->rev_page;
518 $vals['revid'] = (int)$row->rev_id;
519
520 if ( $row->rev_parent_id !== null ) {
521 $vals['parentid'] = (int)$row->rev_parent_id;
522 }
523 }
524
525 $title = Title::makeTitle( $row->page_namespace, $row->page_title );
526
527 if ( isset( $this->prop['title'] ) ) {
528 ApiQueryBase::addTitleInfo( $vals, $title );
529 }
530
531 if ( isset( $this->prop['timestamp'] ) ) {
532 $vals['timestamp'] = wfTimestamp( TS::ISO_8601, $row->rev_timestamp );
533 }
534
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;
539 }
540
541 if ( isset( $this->prop['comment'] ) || isset( $this->prop['parsedcomment'] ) ) {
542 if ( $row->rev_deleted & RevisionRecord::DELETED_COMMENT ) {
543 $vals['commenthidden'] = true;
544 $anyHidden = true;
545 }
546
547 $userCanView = RevisionRecord::userCanBitfield(
548 $row->rev_deleted,
549 RevisionRecord::DELETED_COMMENT, $this->getAuthority()
550 );
551
552 if ( $userCanView ) {
553 $comment = $this->commentStore->getComment( 'rev_comment', $row )->text;
554 if ( isset( $this->prop['comment'] ) ) {
555 $vals['comment'] = $comment;
556 }
557
558 if ( isset( $this->prop['parsedcomment'] ) ) {
559 $vals['parsedcomment'] = $this->commentFormatter->format( $comment, $title );
560 }
561 }
562 }
563
564 if ( isset( $this->prop['patrolled'] ) ) {
565 $vals['patrolled'] = $row->rc_patrolled != RecentChange::PRC_UNPATROLLED;
566 $vals['autopatrolled'] = $row->rc_patrolled == RecentChange::PRC_AUTOPATROLLED;
567 }
568
569 if ( isset( $this->prop['size'] ) && $row->rev_len !== null ) {
570 $vals['size'] = (int)$row->rev_len;
571 }
572
573 if ( isset( $this->prop['sizediff'] )
574 && $row->rev_len !== null
575 && $row->rev_parent_id !== null
576 ) {
577 $parentLen = $this->parentLens[$row->rev_parent_id] ?? 0;
578 $vals['sizediff'] = (int)$row->rev_len - $parentLen;
579 }
580
581 if ( isset( $this->prop['tags'] ) ) {
582 if ( $row->ts_tags ) {
583 $tags = explode( ',', $row->ts_tags );
584 ApiResult::setIndexedTagName( $tags, 'tag' );
585 $vals['tags'] = $tags;
586 } else {
587 $vals['tags'] = [];
588 }
589 }
590
591 if ( $anyHidden && ( $row->rev_deleted & RevisionRecord::DELETED_RESTRICTED ) ) {
592 $vals['suppressed'] = true;
593 }
594
595 return $vals;
596 }
597
598 private function continueStr( \stdClass $row ): string {
599 if ( $this->multiUserMode ) {
600 switch ( $this->orderBy ) {
601 case 'name':
602 return "name|$row->rev_user_text|$row->rev_timestamp|$row->rev_id";
603 case 'actor':
604 return "actor|$row->rev_actor|$row->rev_timestamp|$row->rev_id";
605 }
606 } else {
607 return "$row->rev_timestamp|$row->rev_id";
608 }
609 }
610
612 public function getCacheMode( $params ) {
613 // This module provides access to deleted revisions and patrol flags if
614 // the requester is logged in
615 return 'anon-public-user-private';
616 }
617
619 public function getAllowedParams() {
620 return [
621 'limit' => [
622 ParamValidator::PARAM_DEFAULT => 10,
623 ParamValidator::PARAM_TYPE => 'limit',
624 IntegerDef::PARAM_MIN => 1,
625 IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
626 IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2
627 ],
628 'start' => [
629 ParamValidator::PARAM_TYPE => 'timestamp'
630 ],
631 'end' => [
632 ParamValidator::PARAM_TYPE => 'timestamp'
633 ],
634 'continue' => [
635 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
636 ],
637 'user' => [
638 ParamValidator::PARAM_TYPE => 'user',
639 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'interwiki' ],
640 ParamValidator::PARAM_ISMULTI => true
641 ],
642 'userids' => [
643 ParamValidator::PARAM_TYPE => 'integer',
644 ParamValidator::PARAM_ISMULTI => true
645 ],
646 'userprefix' => null,
647 'iprange' => null,
648 'dir' => [
649 ParamValidator::PARAM_DEFAULT => 'older',
650 ParamValidator::PARAM_TYPE => [
651 'newer',
652 'older'
653 ],
654 ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
656 'newer' => 'api-help-paramvalue-direction-newer',
657 'older' => 'api-help-paramvalue-direction-older',
658 ],
659 ],
660 'namespace' => [
661 ParamValidator::PARAM_ISMULTI => true,
662 ParamValidator::PARAM_TYPE => 'namespace'
663 ],
664 'prop' => [
665 ParamValidator::PARAM_ISMULTI => true,
666 ParamValidator::PARAM_DEFAULT => 'ids|title|timestamp|comment|size|flags',
667 ParamValidator::PARAM_TYPE => [
668 'ids',
669 'title',
670 'timestamp',
671 'comment',
672 'parsedcomment',
673 'size',
674 'sizediff',
675 'flags',
676 'patrolled',
677 'tags'
678 ],
680 ],
681 'show' => [
682 ParamValidator::PARAM_ISMULTI => true,
683 ParamValidator::PARAM_TYPE => [
684 'minor',
685 '!minor',
686 'patrolled',
687 '!patrolled',
688 'autopatrolled',
689 '!autopatrolled',
690 'top',
691 '!top',
692 'new',
693 '!new',
694 ],
696 'apihelp-query+usercontribs-param-show',
697 $this->getConfig()->get( MainConfigNames::RCMaxAge )
698 ],
699 ],
700 'tag' => null,
701 'toponly' => [
702 ParamValidator::PARAM_DEFAULT => false,
703 ParamValidator::PARAM_DEPRECATED => true,
704 ],
705 ];
706 }
707
709 protected function getExamplesMessages() {
710 return [
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',
715 ];
716 }
717
719 public function getHelpUrls() {
720 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Usercontribs';
721 }
722}
723
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'))
Definition WebStart.php:69
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1506
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:542
dieContinueUsageIf( $condition)
Die with the 'badcontinue' error.
Definition ApiBase.php:1730
parseContinueParamOrDie(string $continue, array $types)
Parse the 'continue' parameter in the usual format and validate the types of each part,...
Definition ApiBase.php:1691
getResult()
Get the result object.
Definition ApiBase.php:681
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, or 'string' with PARAM_ISMULTI,...
Definition ApiBase.php:206
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition ApiBase.php:166
const LIMIT_BIG2
Fast query, apihighlimits limit.
Definition ApiBase.php:233
encodeParamName( $paramName)
This method mangles parameter name based on the prefix supplied to the constructor.
Definition ApiBase.php:800
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:822
requireOnlyOneParameter( $params,... $required)
Die if 0 or more than one of a certain set of parameters is set and not false.
Definition ApiBase.php:960
const LIMIT_BIG1
Fast query, standard limit.
Definition ApiBase.php:231
This is a base class for all Query modules.
addOption( $name, $value=null)
Add an option such as LIMIT or USE INDEX.
addFieldsIf( $value, $condition)
Same as addFields(), but add the fields only if a condition is met.
static addTitleInfo(&$arr, $title, $prefix='')
Add information (title and namespace) about a Title object to a result array.
addWhereIf( $value, $condition)
Same as addWhere(), but add the WHERE clauses only if a condition is met.
addTables( $tables, $alias=null)
Add a set of tables to the internal array.
addJoinConds( $join_conds)
Add a set of JOIN conditions to the internal array.
getDB()
Get the Query database connection (read-only).
select( $method, $extraQuery=[], ?array &$hookData=null)
Execute a SELECT query based on the values in the internal arrays.
addWhere( $value)
Add a set of WHERE clauses to the internal array.
executeGenderCacheFromResultWrapper(IResultWrapper $res, $fname=__METHOD__, $fieldPrefix='page')
Preprocess the result set to fill the GenderCache with the necessary information before using self::a...
addTimestampWhereRange( $field, $dir, $start, $end, $sort=true)
Add a WHERE clause corresponding to a range, similar to addWhereRange, but converts $start and $end t...
getQueryBuilder()
Get the SelectQueryBuilder.
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.
addWhereFld( $field, $value)
Equivalent to addWhere( [ $field => $value ] )
addFields( $value)
Add a set of fields to select to the internal array.
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.
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...
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
getHelpUrls()
Return links to more detailed help pages about the module.1.25, returning boolean false is deprecated...
getExamplesMessages()
Returns usage examples for this module.Return value has query strings as keys, with values being eith...
__construct(ApiQuery $query, string $moduleName, private readonly CommentStore $commentStore, private readonly UserIdentityLookup $userIdentityLookup, private readonly UserNameUtils $userNameUtils, private readonly RevisionStore $revisionStore, private readonly NameTableStore $changeTagDefStore, private readonly ChangeTagsStore $changeTagsStore, private readonly ActorMigration $actorMigration, private readonly CommentFormatter $commentFormatter,)
getCacheMode( $params)
Get the cache mode for the data generated by this module.Override this in the module subclass....
This is the main query class.
Definition ApiQuery.php:36
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
Read-write access to the change_tags table.
This is the main service interface for converting single-line comments from various DB comment fields...
Handle database storage of comments such as edit summaries and log reasons.
makeTitle( $linkId)
Convert a link ID to a Title.to override Title
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()
Type definition for user types.
Definition UserDef.php:27
Utility class for creating and reading rows in the recentchanges table.
Page revision base class.
Service for looking up page revisions.
Exception representing a failure to look up a row from a name table.
Represents a title within MediaWiki.
Definition Title.php:69
This is not intended to be a long-term part of MediaWiki; it will be deprecated and removed once acto...
Class to parse and build external user names.
Value object representing a user's identity.
UserNameUtils service.
Service for formatting and validating API parameters.
Type definition for integer types.
Build SELECT queries with a fluent interface.
Service for looking up UserIdentity.
Interface for objects representing user identity.