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