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