MediaWiki master
ApiQueryUserContribs.php
Go to the documentation of this file.
1<?php
23namespace MediaWiki\Api;
24
41use RecentChange;
42use stdClass;
43use Wikimedia\IPUtils;
47
54
55 private CommentStore $commentStore;
56 private UserIdentityLookup $userIdentityLookup;
57 private UserNameUtils $userNameUtils;
58 private RevisionStore $revisionStore;
59 private NameTableStore $changeTagDefStore;
60 private ChangeTagsStore $changeTagsStore;
61 private ActorMigration $actorMigration;
62 private CommentFormatter $commentFormatter;
63
64 public function __construct(
65 ApiQuery $query,
66 string $moduleName,
67 CommentStore $commentStore,
68 UserIdentityLookup $userIdentityLookup,
69 UserNameUtils $userNameUtils,
70 RevisionStore $revisionStore,
71 NameTableStore $changeTagDefStore,
72 ChangeTagsStore $changeTagsStore,
73 ActorMigration $actorMigration,
74 CommentFormatter $commentFormatter
75 ) {
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;
85 }
86
87 private array $params;
88 private bool $multiUserMode;
89 private string $orderBy;
90 private array $parentLens;
91
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;
102
103 public function execute() {
104 // Parse some parameters
105 $this->params = $this->extractRequestParams();
106
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'] );
118
119 $dbSecondary = $this->getDB(); // any random replica DB
120
121 $sort = ( $this->params['dir'] == 'newer' ?
122 SelectQueryBuilder::SORT_ASC : SelectQueryBuilder::SORT_DESC );
123 $op = ( $this->params['dir'] == 'older' ? '<=' : '>=' );
124
125 // Create an Iterator that produces the UserIdentity objects we need, depending
126 // on which of the 'userprefix', 'userids', 'iprange', or 'user' params
127 // was specified.
128 $this->requireOnlyOneParameter( $this->params, 'userprefix', 'userids', 'iprange', 'user' );
129 if ( isset( $this->params['userprefix'] ) ) {
130 $this->multiUserMode = true;
131 $this->orderBy = 'name';
132 $fname = __METHOD__;
133
134 // Because 'userprefix' might produce a huge number of users (e.g.
135 // a wiki with users "Test00000001" to "Test99999999"), use a
136 // generator with batched lookup and continuation.
137 $userIter = call_user_func( function () use ( $dbSecondary, $sort, $op, $fname ) {
138 $fromName = false;
139 if ( $this->params['continue'] !== null ) {
140 $continue = $this->parseContinueParamOrDie( $this->params['continue'],
141 [ 'string', 'string', 'string', 'int' ] );
142 $this->dieContinueUsageIf( $continue[0] !== 'name' );
143 $fromName = $continue[1];
144 }
145
146 $limit = 501;
147 do {
148 $usersBatch = $this->userIdentityLookup
149 ->newSelectQueryBuilder()
150 ->caller( $fname )
151 ->limit( $limit )
152 ->whereUserNamePrefix( $this->params['userprefix'] )
153 ->where( $fromName !== false
154 ? $dbSecondary->buildComparison( $op, [ 'actor_name' => $fromName ] )
155 : [] )
156 ->orderByName( $sort )
157 ->fetchUserIdentities();
158
159 $count = 0;
160 $fromName = false;
161 foreach ( $usersBatch as $user ) {
162 if ( ++$count >= $limit ) {
163 $fromName = $user->getName();
164 break;
165 }
166 yield $user;
167 }
168 } while ( $fromName !== false );
169 } );
170 // Do the actual sorting client-side, because otherwise
171 // prepareQuery might try to sort by actor and confuse everything.
172 $batchSize = 1;
173 } elseif ( isset( $this->params['userids'] ) ) {
174 if ( $this->params['userids'] === [] ) {
175 $encParamName = $this->encodeParamName( 'userids' );
176 $this->dieWithError( [ 'apierror-paramempty', $encParamName ], "paramempty_$encParamName" );
177 }
178
179 $ids = [];
180 foreach ( $this->params['userids'] as $uid ) {
181 if ( $uid <= 0 ) {
182 $this->dieWithError( [ 'apierror-invaliduserid', $uid ], 'invaliduserid' );
183 }
184 $ids[] = $uid;
185 }
186
187 $this->orderBy = 'actor';
188 $this->multiUserMode = count( $ids ) > 1;
189
190 $fromId = false;
191 if ( $this->multiUserMode && $this->params['continue'] !== null ) {
192 $continue = $this->parseContinueParamOrDie( $this->params['continue'],
193 [ 'string', 'int', 'string', 'int' ] );
194 $this->dieContinueUsageIf( $continue[0] !== 'actor' );
195 $fromId = $continue[1];
196 }
197
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'] ) ) {
207 // Make sure it is a valid range and within the CIDR limit
208 $ipRange = $this->params['iprange'];
209 $contribsCIDRLimit = $this->getConfig()->get( MainConfigNames::RangeContributionsCIDRLimit );
210 if ( IPUtils::isIPv4( $ipRange ) ) {
211 $type = 'IPv4';
212 $cidrLimit = $contribsCIDRLimit['IPv4'];
213 } elseif ( IPUtils::isIPv6( $ipRange ) ) {
214 $type = 'IPv6';
215 $cidrLimit = $contribsCIDRLimit['IPv6'];
216 } else {
217 $this->dieWithError( [ 'apierror-invalidiprange', $ipRange ], 'invalidiprange' );
218 }
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 ] );
224 }
225
226 $this->multiUserMode = true;
227 $this->orderBy = 'name';
228 $fname = __METHOD__;
229
230 // Because 'iprange' might produce a huge number of ips, use a
231 // generator with batched lookup and continuation.
232 $userIter = call_user_func( function () use ( $dbSecondary, $sort, $op, $fname, $ipRange ) {
233 [ $start, $end ] = IPUtils::parseRange( $ipRange );
234 if ( $this->params['continue'] !== null ) {
235 $continue = $this->parseContinueParamOrDie( $this->params['continue'],
236 [ 'string', 'string', 'string', 'int' ] );
237 $this->dieContinueUsageIf( $continue[0] !== 'name' );
238 $fromName = $continue[1];
239 $fromIPHex = IPUtils::toHex( $fromName );
240 $this->dieContinueUsageIf( $fromIPHex === false );
241 if ( $op == '<=' ) {
242 $end = $fromIPHex;
243 } else {
244 $start = $fromIPHex;
245 }
246 }
247
248 $limit = 501;
249
250 do {
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 )
257 ->limit( $limit )
258 ->caller( $fname )
259 ->fetchResultSet();
260
261 $count = 0;
262 $fromName = false;
263 foreach ( $res as $row ) {
264 $ipAddr = IPUtils::formatHex( $row->ipc_hex );
265 if ( ++$count >= $limit ) {
266 $fromName = $ipAddr;
267 break;
268 }
269 yield UserIdentityValue::newAnonymous( $ipAddr );
270 }
271 } while ( $fromName !== false );
272 } );
273 // Do the actual sorting client-side, because otherwise
274 // prepareQuery might try to sort by actor and confuse everything.
275 $batchSize = 1;
276 } else {
277 $names = [];
278 if ( !count( $this->params['user'] ) ) {
279 $encParamName = $this->encodeParamName( 'user' );
280 $this->dieWithError(
281 [ 'apierror-paramempty', $encParamName ], "paramempty_$encParamName"
282 );
283 }
284 foreach ( $this->params['user'] as $u ) {
285 if ( $u === '' ) {
286 $encParamName = $this->encodeParamName( 'user' );
287 $this->dieWithError(
288 [ 'apierror-paramempty', $encParamName ], "paramempty_$encParamName"
289 );
290 }
291
292 if ( $this->userNameUtils->isIP( $u ) || ExternalUserNames::isExternal( $u ) ) {
293 $names[$u] = null;
294 } else {
295 $name = $this->userNameUtils->getCanonical( $u );
296 if ( $name === false ) {
297 $encParamName = $this->encodeParamName( 'user' );
298 $this->dieWithError(
299 [ 'apierror-baduser', $encParamName, wfEscapeWikiText( $u ) ], "baduser_$encParamName"
300 );
301 }
302 $names[$name] = null;
303 }
304 }
305
306 $this->orderBy = 'actor';
307 $this->multiUserMode = count( $names ) > 1;
308
309 $fromId = false;
310 if ( $this->multiUserMode && $this->params['continue'] !== null ) {
311 $continue = $this->parseContinueParamOrDie( $this->params['continue'],
312 [ 'string', 'int', 'string', 'int' ] );
313 $this->dieContinueUsageIf( $continue[0] !== 'actor' );
314 $fromId = $continue[1];
315 }
316
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 );
325 }
326
327 $count = 0;
328 $limit = $this->params['limit'];
329 $userIter->rewind();
330 while ( $userIter->valid() ) {
331 $users = [];
332 while ( count( $users ) < $batchSize && $userIter->valid() ) {
333 $users[] = $userIter->current();
334 $userIter->next();
335 }
336
337 $hookData = [];
338 $this->prepareQuery( $users, $limit - $count );
339 $res = $this->select( __METHOD__, [], $hookData );
340
341 if ( $this->fld_title ) {
342 $this->executeGenderCacheFromResultWrapper( $res, __METHOD__ );
343 }
344
345 if ( $this->fld_sizediff ) {
346 $revIds = [];
347 foreach ( $res as $row ) {
348 if ( $row->rev_parent_id ) {
349 $revIds[] = (int)$row->rev_parent_id;
350 }
351 }
352 $this->parentLens = $this->revisionStore->getRevisionSizes( $revIds );
353 }
354
355 foreach ( $res as $row ) {
356 if ( ++$count > $limit ) {
357 // We've reached the one extra which shows that there are
358 // additional pages to be had. Stop here...
359 $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) );
360 break 2;
361 }
362
363 $vals = $this->extractRowInfo( $row );
364 $fit = $this->processRow( $row, $vals, $hookData ) &&
365 $this->getResult()->addValue( [ 'query', $this->getModuleName() ], null, $vals );
366 if ( !$fit ) {
367 $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) );
368 break 2;
369 }
370 }
371 }
372
373 $this->getResult()->addIndexedTagName( [ 'query', $this->getModuleName() ], 'item' );
374 }
375
381 private function prepareQuery( array $users, $limit ) {
382 $this->resetQueryParams();
383 $db = $this->getDB();
384
385 $queryBuilder = $this->revisionStore->newSelectQueryBuilder( $db )->joinComment()->joinPage();
386 $revWhere = $this->actorMigration->getWhere( $db, 'rev_user', $users );
387
388 $orderUserField = 'rev_actor';
389 $userField = $this->orderBy === 'actor' ? 'rev_actor' : 'actor_name';
390 $tsField = 'rev_timestamp';
391 $idField = 'rev_id';
392
393 $this->getQueryBuilder()->merge( $queryBuilder );
394 $this->addWhere( $revWhere['conds'] );
395 // Force the appropriate index to avoid bad query plans (T307815 and T307295)
396 if ( isset( $revWhere['orconds']['newactor'] ) ) {
397 $this->addOption( 'USE INDEX', [ 'revision' => 'rev_actor_timestamp' ] );
398 }
399
400 // Handle continue parameter
401 if ( $this->params['continue'] !== null ) {
402 if ( $this->multiUserMode ) {
403 $continue = $this->parseContinueParamOrDie( $this->params['continue'],
404 [ 'string', 'string', 'timestamp', 'int' ] );
405 $modeFlag = array_shift( $continue );
406 $this->dieContinueUsageIf( $modeFlag !== $this->orderBy );
407 $encUser = array_shift( $continue );
408 } else {
409 $continue = $this->parseContinueParamOrDie( $this->params['continue'],
410 [ 'timestamp', 'int' ] );
411 }
412 $op = ( $this->params['dir'] == 'older' ? '<=' : '>=' );
413 if ( $this->multiUserMode ) {
414 $this->addWhere( $db->buildComparison( $op, [
415 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable encUser is set when used
416 $userField => $encUser,
417 $tsField => $db->timestamp( $continue[0] ),
418 $idField => $continue[1],
419 ] ) );
420 } else {
421 $this->addWhere( $db->buildComparison( $op, [
422 $tsField => $db->timestamp( $continue[0] ),
423 $idField => $continue[1],
424 ] ) );
425 }
426 }
427
428 // Don't include any revisions where we're not supposed to be able to
429 // see the username.
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;
434 } else {
435 $bitmask = 0;
436 }
437 if ( $bitmask ) {
438 $this->addWhere( $db->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask" );
439 }
440
441 // Add the user field to ORDER BY if there are multiple users
442 if ( count( $users ) > 1 ) {
443 $this->addWhereRange( $orderUserField, $this->params['dir'], null, null );
444 }
445
446 // Then timestamp
447 $this->addTimestampWhereRange( $tsField,
448 $this->params['dir'], $this->params['start'], $this->params['end'] );
449
450 // Then rev_id for a total ordering
451 $this->addWhereRange( $idField, $this->params['dir'], null, null );
452
453 $this->addWhereFld( 'page_namespace', $this->params['namespace'] );
454
455 $show = $this->params['show'];
456 if ( $this->params['toponly'] ) { // deprecated/old param
457 $show[] = 'top';
458 }
459 if ( $show !== null ) {
460 $show = array_fill_keys( $show, true );
461
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'] ) )
468 ) {
469 $this->dieWithError( 'apierror-show' );
470 }
471
472 $this->addWhereIf( [ 'rev_minor_edit' => 0 ], isset( $show['!minor'] ) );
473 $this->addWhereIf( $db->expr( 'rev_minor_edit', '!=', 0 ), isset( $show['minor'] ) );
474 $this->addWhereIf(
475 [ 'rc_patrolled' => RecentChange::PRC_UNPATROLLED ],
476 isset( $show['!patrolled'] )
477 );
478 $this->addWhereIf(
479 $db->expr( 'rc_patrolled', '!=', RecentChange::PRC_UNPATROLLED ),
480 isset( $show['patrolled'] )
481 );
482 $this->addWhereIf(
483 $db->expr( 'rc_patrolled', '!=', RecentChange::PRC_AUTOPATROLLED ),
484 isset( $show['!autopatrolled'] )
485 );
486 $this->addWhereIf(
487 [ 'rc_patrolled' => RecentChange::PRC_AUTOPATROLLED ],
488 isset( $show['autopatrolled'] )
489 );
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'] ) );
494 }
495 $this->addOption( 'LIMIT', $limit + 1 );
496
497 if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ||
498 isset( $show['autopatrolled'] ) || isset( $show['!autopatrolled'] ) || $this->fld_patrolled
499 ) {
500 $user = $this->getUser();
501 if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
502 $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' );
503 }
504
505 $isFilterset = isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ||
506 isset( $show['autopatrolled'] ) || isset( $show['!autopatrolled'] );
507 $this->addTables( 'recentchanges' );
508 $this->addJoinConds( [ 'recentchanges' => [
509 $isFilterset ? 'JOIN' : 'LEFT JOIN',
510 [ 'rc_this_oldid = ' . $idField ]
511 ] ] );
512 }
513
514 $this->addFieldsIf( 'rc_patrolled', $this->fld_patrolled );
515
516 if ( $this->fld_tags ) {
517 $this->addFields( [
518 'ts_tags' => $this->changeTagsStore->makeTagSummarySubquery( 'revision' )
519 ] );
520 }
521
522 if ( isset( $this->params['tag'] ) ) {
523 $this->addTables( 'change_tag' );
524 $this->addJoinConds(
525 [ 'change_tag' => [ 'JOIN', [ $idField . ' = ct_rev_id' ] ] ]
526 );
527 try {
528 $this->addWhereFld( 'ct_tag_id', $this->changeTagDefStore->getId( $this->params['tag'] ) );
529 } catch ( NameTableAccessException $exception ) {
530 // Return nothing.
531 $this->addWhere( '1=0' );
532 }
533 }
534 $this->addOption(
535 'MAX_EXECUTION_TIME',
537 );
538 }
539
546 private function extractRowInfo( $row ) {
547 $vals = [];
548 $anyHidden = false;
549
550 if ( $row->rev_deleted & RevisionRecord::DELETED_TEXT ) {
551 $vals['texthidden'] = true;
552 $anyHidden = true;
553 }
554
555 // Any rows where we can't view the user were filtered out in the query.
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;
560 $anyHidden = true;
561 }
562 if ( $this->fld_ids ) {
563 $vals['pageid'] = (int)$row->rev_page;
564 $vals['revid'] = (int)$row->rev_id;
565
566 if ( $row->rev_parent_id !== null ) {
567 $vals['parentid'] = (int)$row->rev_parent_id;
568 }
569 }
570
571 $title = Title::makeTitle( $row->page_namespace, $row->page_title );
572
573 if ( $this->fld_title ) {
574 ApiQueryBase::addTitleInfo( $vals, $title );
575 }
576
577 if ( $this->fld_timestamp ) {
578 $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->rev_timestamp );
579 }
580
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;
585 }
586
587 if ( $this->fld_comment || $this->fld_parsedcomment ) {
588 if ( $row->rev_deleted & RevisionRecord::DELETED_COMMENT ) {
589 $vals['commenthidden'] = true;
590 $anyHidden = true;
591 }
592
593 $userCanView = RevisionRecord::userCanBitfield(
594 $row->rev_deleted,
595 RevisionRecord::DELETED_COMMENT, $this->getAuthority()
596 );
597
598 if ( $userCanView ) {
599 $comment = $this->commentStore->getComment( 'rev_comment', $row )->text;
600 if ( $this->fld_comment ) {
601 $vals['comment'] = $comment;
602 }
603
604 if ( $this->fld_parsedcomment ) {
605 $vals['parsedcomment'] = $this->commentFormatter->format( $comment, $title );
606 }
607 }
608 }
609
610 if ( $this->fld_patrolled ) {
611 $vals['patrolled'] = $row->rc_patrolled != RecentChange::PRC_UNPATROLLED;
612 $vals['autopatrolled'] = $row->rc_patrolled == RecentChange::PRC_AUTOPATROLLED;
613 }
614
615 if ( $this->fld_size && $row->rev_len !== null ) {
616 $vals['size'] = (int)$row->rev_len;
617 }
618
619 if ( $this->fld_sizediff
620 && $row->rev_len !== null
621 && $row->rev_parent_id !== null
622 ) {
623 $parentLen = $this->parentLens[$row->rev_parent_id] ?? 0;
624 $vals['sizediff'] = (int)$row->rev_len - $parentLen;
625 }
626
627 if ( $this->fld_tags ) {
628 if ( $row->ts_tags ) {
629 $tags = explode( ',', $row->ts_tags );
630 ApiResult::setIndexedTagName( $tags, 'tag' );
631 $vals['tags'] = $tags;
632 } else {
633 $vals['tags'] = [];
634 }
635 }
636
637 if ( $anyHidden && ( $row->rev_deleted & RevisionRecord::DELETED_RESTRICTED ) ) {
638 $vals['suppressed'] = true;
639 }
640
641 return $vals;
642 }
643
644 private function continueStr( $row ) {
645 if ( $this->multiUserMode ) {
646 switch ( $this->orderBy ) {
647 case 'name':
648 return "name|$row->rev_user_text|$row->rev_timestamp|$row->rev_id";
649 case 'actor':
650 return "actor|$row->rev_actor|$row->rev_timestamp|$row->rev_id";
651 }
652 } else {
653 return "$row->rev_timestamp|$row->rev_id";
654 }
655 }
656
657 public function getCacheMode( $params ) {
658 // This module provides access to deleted revisions and patrol flags if
659 // the requester is logged in
660 return 'anon-public-user-private';
661 }
662
663 public function getAllowedParams() {
664 return [
665 'limit' => [
666 ParamValidator::PARAM_DEFAULT => 10,
667 ParamValidator::PARAM_TYPE => 'limit',
668 IntegerDef::PARAM_MIN => 1,
669 IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
670 IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2
671 ],
672 'start' => [
673 ParamValidator::PARAM_TYPE => 'timestamp'
674 ],
675 'end' => [
676 ParamValidator::PARAM_TYPE => 'timestamp'
677 ],
678 'continue' => [
679 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
680 ],
681 'user' => [
682 ParamValidator::PARAM_TYPE => 'user',
683 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'interwiki' ],
684 ParamValidator::PARAM_ISMULTI => true
685 ],
686 'userids' => [
687 ParamValidator::PARAM_TYPE => 'integer',
688 ParamValidator::PARAM_ISMULTI => true
689 ],
690 'userprefix' => null,
691 'iprange' => null,
692 'dir' => [
693 ParamValidator::PARAM_DEFAULT => 'older',
694 ParamValidator::PARAM_TYPE => [
695 'newer',
696 'older'
697 ],
698 ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
700 'newer' => 'api-help-paramvalue-direction-newer',
701 'older' => 'api-help-paramvalue-direction-older',
702 ],
703 ],
704 'namespace' => [
705 ParamValidator::PARAM_ISMULTI => true,
706 ParamValidator::PARAM_TYPE => 'namespace'
707 ],
708 'prop' => [
709 ParamValidator::PARAM_ISMULTI => true,
710 ParamValidator::PARAM_DEFAULT => 'ids|title|timestamp|comment|size|flags',
711 ParamValidator::PARAM_TYPE => [
712 'ids',
713 'title',
714 'timestamp',
715 'comment',
716 'parsedcomment',
717 'size',
718 'sizediff',
719 'flags',
720 'patrolled',
721 'tags'
722 ],
724 ],
725 'show' => [
726 ParamValidator::PARAM_ISMULTI => true,
727 ParamValidator::PARAM_TYPE => [
728 'minor',
729 '!minor',
730 'patrolled',
731 '!patrolled',
732 'autopatrolled',
733 '!autopatrolled',
734 'top',
735 '!top',
736 'new',
737 '!new',
738 ],
740 'apihelp-query+usercontribs-param-show',
741 $this->getConfig()->get( MainConfigNames::RCMaxAge )
742 ],
743 ],
744 'tag' => null,
745 'toponly' => [
746 ParamValidator::PARAM_DEFAULT => false,
747 ParamValidator::PARAM_DEPRECATED => true,
748 ],
749 ];
750 }
751
752 protected function getExamplesMessages() {
753 return [
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',
758 ];
759 }
760
761 public function getHelpUrls() {
762 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Usercontribs';
763 }
764}
765
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.
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1565
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:571
dieContinueUsageIf( $condition)
Die with the 'badcontinue' error.
Definition ApiBase.php:1795
parseContinueParamOrDie(string $continue, array $types)
Parse the 'continue' parameter in the usual format and validate the types of each part,...
Definition ApiBase.php:1756
getResult()
Get the result object.
Definition ApiBase.php:710
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, or 'string' with PARAM_ISMULTI,...
Definition ApiBase.php:224
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition ApiBase.php:184
const LIMIT_BIG2
Fast query, apihighlimits limit.
Definition ApiBase.php:251
encodeParamName( $paramName)
This method mangles parameter name based on the prefix supplied to the constructor.
Definition ApiBase.php:829
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:851
requireOnlyOneParameter( $params,... $required)
Die if 0 or more than one of a certain set of parameters is set and not false.
Definition ApiBase.php:990
const LIMIT_BIG1
Fast query, standard limit.
Definition ApiBase.php:249
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.
getExamplesMessages()
Returns usage examples for this module.
getCacheMode( $params)
Get the cache mode for the data generated by this module.
__construct(ApiQuery $query, string $moduleName, CommentStore $commentStore, UserIdentityLookup $userIdentityLookup, UserNameUtils $userNameUtils, RevisionStore $revisionStore, NameTableStore $changeTagDefStore, ChangeTagsStore $changeTagsStore, ActorMigration $actorMigration, CommentFormatter $commentFormatter)
This is the main query class.
Definition ApiQuery.php:48
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.
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
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:78
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.
static newAnonymous(string $name, $wikiId=self::LOCAL)
Create UserIdentity for an anonymous user.
UserNameUtils service.
Utility class for creating and reading rows in the recentchanges table.
const PRC_UNPATROLLED
const PRC_AUTOPATROLLED
Service for formatting and validating API parameters.
Type definition for integer types.
Build SELECT queries with a fluent interface.
Interface for objects representing user identity.