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