MediaWiki  master
SpecialContributions.php
Go to the documentation of this file.
1 <?php
37 use Wikimedia\IPUtils;
39 
46  protected $opts;
47 
50 
53 
55  private $loadBalancer;
56 
58  private $actorMigration;
59 
61  private $revisionStore;
62 
64  private $namespaceInfo;
65 
67  private $userNameUtils;
68 
71 
74 
77 
79  private $userFactory;
80 
82  private $pager = null;
83 
97  public function __construct(
109  ) {
110  parent::__construct( 'Contributions' );
111  // This class is extended and therefore falls back to global state - T269521
112  $services = MediaWikiServices::getInstance();
113  $this->linkBatchFactory = $linkBatchFactory ?? $services->getLinkBatchFactory();
114  $this->permissionManager = $permissionManager ?? $services->getPermissionManager();
115  $this->loadBalancer = $loadBalancer ?? $services->getDBLoadBalancer();
116  $this->actorMigration = $actorMigration ?? $services->getActorMigration();
117  $this->revisionStore = $revisionStore ?? $services->getRevisionStore();
118  $this->namespaceInfo = $namespaceInfo ?? $services->getNamespaceInfo();
119  $this->userNameUtils = $userNameUtils ?? $services->getUserNameUtils();
120  $this->userNamePrefixSearch = $userNamePrefixSearch ?? $services->getUserNamePrefixSearch();
121  $this->userOptionsLookup = $userOptionsLookup ?? $services->getUserOptionsLookup();
122  $this->commentFormatter = $commentFormatter ?? $services->getCommentFormatter();
123  $this->userFactory = $userFactory ?? $services->getUserFactory();
124  }
125 
126  public function execute( $par ) {
127  $this->setHeaders();
128  $this->outputHeader();
129  $out = $this->getOutput();
130  // Modules required for viewing the list of contributions (also when included on other pages)
131  $out->addModuleStyles( [
132  'jquery.makeCollapsible.styles',
133  'mediawiki.interface.helpers.styles',
134  'mediawiki.special',
135  'mediawiki.special.changeslist',
136  ] );
137  $out->addModules( [
138  // Certain skins e.g. Minerva might have disabled this module.
139  'mediawiki.page.ready'
140  ] );
141  $this->addHelpLink( 'Help:User contributions' );
142 
143  $this->opts = [];
144  $request = $this->getRequest();
145 
146  $target = $par ?? $request->getVal( 'target', '' );
147  '@phan-var string $target'; // getVal does not return null here
148 
149  $this->opts['deletedOnly'] = $request->getBool( 'deletedOnly' );
150 
151  if ( !strlen( $target ) ) {
152  if ( !$this->including() ) {
153  $out->addHTML( $this->getForm( $this->opts ) );
154  }
155 
156  return;
157  }
158 
159  $user = $this->getUser();
160 
161  $this->opts['limit'] = $request->getInt( 'limit', $this->userOptionsLookup->getIntOption( $user, 'rclimit' ) );
162  $this->opts['target'] = $target;
163  $this->opts['topOnly'] = $request->getBool( 'topOnly' );
164  $this->opts['newOnly'] = $request->getBool( 'newOnly' );
165  $this->opts['hideMinor'] = $request->getBool( 'hideMinor' );
166 
167  $ns = $request->getVal( 'namespace', null );
168  if ( $ns !== null && $ns !== '' && $ns !== 'all' ) {
169  $this->opts['namespace'] = intval( $ns );
170  } else {
171  $this->opts['namespace'] = '';
172  }
173 
174  // Backwards compatibility: Before using OOUI form the old HTML form had
175  // fields for nsInvert and associated. These have now been replaced with the
176  // wpFilters query string parameters. These are retained to keep old URIs working.
177  $this->opts['associated'] = $request->getBool( 'associated' );
178  $this->opts['nsInvert'] = (bool)$request->getVal( 'nsInvert' );
179  $nsFilters = $request->getArray( 'wpfilters', null );
180  if ( $nsFilters !== null ) {
181  $this->opts['associated'] = in_array( 'associated', $nsFilters );
182  $this->opts['nsInvert'] = in_array( 'nsInvert', $nsFilters );
183  }
184 
185  $this->opts['tagfilter'] = array_filter( explode(
186  '|',
187  (string)$request->getVal( 'tagfilter' )
188  ), static function ( $el ) {
189  return $el !== '';
190  } );
191 
192  // Allows reverts to have the bot flag in recent changes. It is just here to
193  // be passed in the form at the top of the page
194  if ( $this->permissionManager->userHasRight( $user, 'markbotedits' ) && $request->getBool( 'bot' ) ) {
195  $this->opts['bot'] = '1';
196  }
197 
198  $skip = $request->getText( 'offset' ) || $request->getText( 'dir' ) == 'prev';
199  # Offset overrides year/month selection
200  if ( !$skip ) {
201  $this->opts['year'] = $request->getIntOrNull( 'year' );
202  $this->opts['month'] = $request->getIntOrNull( 'month' );
203 
204  $this->opts['start'] = $request->getVal( 'start' );
205  $this->opts['end'] = $request->getVal( 'end' );
206  }
207 
208  $id = 0;
209  if ( ExternalUserNames::isExternal( $target ) ) {
210  $userObj = $this->userFactory->newFromName( $target, UserRigorOptions::RIGOR_NONE );
211  if ( !$userObj ) {
212  $out->addHTML( $this->getForm( $this->opts ) );
213  return;
214  }
215 
216  $out->addSubtitle( $this->contributionsSub( $userObj, $target ) );
217  $out->setPageTitle( $this->msg( 'contributions-title', $target ) );
218  } else {
219  $nt = Title::makeTitleSafe( NS_USER, $target );
220  if ( !$nt ) {
221  $out->addHTML( $this->getForm( $this->opts ) );
222  return;
223  }
224  $userObj = $this->userFactory->newFromName(
225  $nt->getText(), UserRigorOptions::RIGOR_NONE );
226  if ( !$userObj ) {
227  $out->addHTML( $this->getForm( $this->opts ) );
228  return;
229  }
230  $id = $userObj->getId();
231 
232  $target = $nt->getText();
233  $out->addSubtitle( $this->contributionsSub( $userObj, $target ) );
234  $out->setPageTitle( $this->msg( 'contributions-title', $target ) );
235 
236  # For IP ranges, we want the contributionsSub, but not the skin-dependent
237  # links under 'Tools', which may include irrelevant links like 'Logs'.
238  if ( !IPUtils::isValidRange( $target ) &&
239  ( $this->userNameUtils->isIP( $target ) || $userObj->isRegistered() )
240  ) {
241  // Don't add non-existent users, because hidden users
242  // that we add here will be removed later to pretend
243  // that they don't exist, and if users that actually don't
244  // exist are added here and then not removed, it exposes
245  // which users exist and are hidden vs. which actually don't
246  // exist. But, do set the relevant user for single IPs.
247  $this->getSkin()->setRelevantUser( $userObj );
248  }
249  }
250 
251  $this->opts = ContribsPager::processDateFilter( $this->opts );
252 
253  if ( $this->opts['namespace'] !== '' && $this->opts['namespace'] < NS_MAIN ) {
254  $this->getOutput()->wrapWikiMsg(
255  "<div class=\"mw-negative-namespace-not-supported error\">\n\$1\n</div>",
256  [ 'negative-namespace-not-supported' ]
257  );
258  $out->addHTML( $this->getForm( $this->opts ) );
259  return;
260  }
261 
262  $feedType = $request->getVal( 'feed' );
263 
264  $feedParams = [
265  'action' => 'feedcontributions',
266  'user' => $target,
267  ];
268  if ( $this->opts['topOnly'] ) {
269  $feedParams['toponly'] = true;
270  }
271  if ( $this->opts['newOnly'] ) {
272  $feedParams['newonly'] = true;
273  }
274  if ( $this->opts['hideMinor'] ) {
275  $feedParams['hideminor'] = true;
276  }
277  if ( $this->opts['deletedOnly'] ) {
278  $feedParams['deletedonly'] = true;
279  }
280 
281  if ( $this->opts['tagfilter'] !== [] ) {
282  $feedParams['tagfilter'] = $this->opts['tagfilter'];
283  }
284  if ( $this->opts['namespace'] !== '' ) {
285  $feedParams['namespace'] = $this->opts['namespace'];
286  }
287  // Don't use year and month for the feed URL, but pass them on if
288  // we redirect to API (if $feedType is specified)
289  if ( $feedType && isset( $this->opts['year'] ) ) {
290  $feedParams['year'] = $this->opts['year'];
291  }
292  if ( $feedType && isset( $this->opts['month'] ) ) {
293  $feedParams['month'] = $this->opts['month'];
294  }
295 
296  if ( $feedType ) {
297  // Maintain some level of backwards compatibility
298  // If people request feeds using the old parameters, redirect to API
299  $feedParams['feedformat'] = $feedType;
300  $url = wfAppendQuery( wfScript( 'api' ), $feedParams );
301 
302  $out->redirect( $url, '301' );
303 
304  return;
305  }
306 
307  // Add RSS/atom links
308  $this->addFeedLinks( $feedParams );
309 
310  if ( $this->getHookRunner()->onSpecialContributionsBeforeMainOutput(
311  $id, $userObj, $this )
312  ) {
313  if ( !$this->including() ) {
314  $out->addHTML( $this->getForm( $this->opts ) );
315  }
316  $pager = $this->getPager( $userObj );
317  if ( IPUtils::isValidRange( $target ) && !$pager->isQueryableRange( $target ) ) {
318  // Valid range, but outside CIDR limit.
319  $limits = $this->getConfig()->get( MainConfigNames::RangeContributionsCIDRLimit );
320  $limit = $limits[ IPUtils::isIPv4( $target ) ? 'IPv4' : 'IPv6' ];
321  $out->addWikiMsg( 'sp-contributions-outofrange', $limit );
322  } else {
323  // @todo We just want a wiki ID here, not a "DB domain", but
324  // current status of MediaWiki conflates the two. See T235955.
325  $poolKey = $this->loadBalancer->getLocalDomainID() . ':SpecialContributions:';
326  if ( $this->getUser()->isAnon() ) {
327  $poolKey .= 'a:' . $this->getUser()->getName();
328  } else {
329  $poolKey .= 'u:' . $this->getUser()->getId();
330  }
331  $work = new PoolCounterWorkViaCallback( 'SpecialContributions', $poolKey, [
332  'doWork' => function () use ( $pager, $out, $target ) {
333  if ( !$pager->getNumRows() ) {
334  $out->addWikiMsg( 'nocontribs', $target );
335  } else {
336  # Show a message about replica DB lag, if applicable
337  $lag = $pager->getDatabase()->getSessionLagStatus()['lag'];
338  if ( $lag > 0 ) {
339  $out->showLagWarning( $lag );
340  }
341 
342  $output = $pager->getBody();
343  if ( !$this->including() ) {
344  $output = $pager->getNavigationBar() .
345  $output .
347  }
348  $out->addHTML( $output );
349  }
350  },
351  'error' => function () use ( $out ) {
352  $msg = $this->getUser()->isAnon()
353  ? 'sp-contributions-concurrency-ip'
354  : 'sp-contributions-concurrency-user';
355  $out->addHTML(
357  $out->msg( $msg )->parse()
358  )
359  );
360  }
361  ] );
362  $work->execute();
363  }
364 
365  $out->setPreventClickjacking( $pager->getPreventClickjacking() );
366 
367  # Show the appropriate "footer" message - WHOIS tools, etc.
368  if ( IPUtils::isValidRange( $target ) && $pager->isQueryableRange( $target ) ) {
369  $message = 'sp-contributions-footer-anon-range';
370  } elseif ( IPUtils::isIPAddress( $target ) ) {
371  $message = 'sp-contributions-footer-anon';
372  } elseif ( $userObj->isAnon() ) {
373  // No message for non-existing users
374  $message = '';
375  } elseif ( $userObj->isHidden() &&
376  !$this->permissionManager->userHasRight( $this->getUser(), 'hideuser' )
377  ) {
378  // User is registered, but make sure that the viewer can see them, to avoid
379  // having different behavior for missing and hidden users; see T120883
380  $message = '';
381  } else {
382  // Not hidden, or hidden but the viewer can still see it
383  $message = 'sp-contributions-footer';
384  }
385 
386  if ( $message && !$this->including() && !$this->msg( $message, $target )->isDisabled() ) {
387  $out->wrapWikiMsg(
388  "<div class='mw-contributions-footer'>\n$1\n</div>",
389  [ $message, $target ] );
390  }
391  }
392  }
393 
403  protected function contributionsSub( $userObj, $targetName ) {
404  $isAnon = $userObj->isAnon();
405  if ( !$isAnon && $userObj->isHidden() &&
406  !$this->permissionManager->userHasRight( $this->getUser(), 'hideuser' )
407  ) {
408  // T120883 if the user is hidden and the viewer cannot see hidden
409  // users, pretend like it does not exist at all.
410  $isAnon = true;
411  }
412 
413  if ( $isAnon ) {
414  // Show a warning message that the user being searched for doesn't exist.
415  // UserNameUtils::isIP returns true for IP address and usemod IPs like '123.123.123.xxx',
416  // but returns false for IP ranges. We don't want to suggest either of these are
417  // valid usernames which we would with the 'contributions-userdoesnotexist' message.
418  if ( !$this->userNameUtils->isIP( $userObj->getName() )
419  && !IPUtils::isValidRange( $userObj->getName() )
420  ) {
421  $this->getOutput()->addHtml( Html::warningBox(
422  $this->getOutput()->msg( 'contributions-userdoesnotexist',
423  wfEscapeWikiText( $userObj->getName() ) )->parse(),
424  'mw-userpage-userdoesnotexist'
425  ) );
426  if ( !$this->including() ) {
427  $this->getOutput()->setStatusCode( 404 );
428  }
429  }
430  $user = htmlspecialchars( $userObj->getName() );
431  } else {
432  $user = $this->getLinkRenderer()->makeLink( $userObj->getUserPage(), $userObj->getName() );
433  }
434  $nt = $userObj->getUserPage();
435  $talk = $userObj->getTalkPage();
436  $links = '';
437 
438  // T211910. Don't show action links if a range is outside block limit
439  $showForIp = IPUtils::isValid( $userObj ) ||
440  ( IPUtils::isValidRange( $userObj ) && $this->getPager( $userObj )->isQueryableRange( $userObj ) );
441 
442  // T276306. if the user is hidden and the viewer cannot see hidden, pretend that it does not exist
443  $registeredAndVisible = $userObj->isRegistered() && ( !$userObj->isHidden()
444  || $this->permissionManager->userHasRight( $this->getUser(), 'hideuser' ) );
445 
446  if ( $talk && ( $registeredAndVisible || $showForIp ) ) {
447  $tools = self::getUserLinks(
448  $this,
449  $userObj,
450  $this->permissionManager,
451  $this->getHookRunner()
452  );
453  $links = Html::openElement( 'span', [ 'class' => 'mw-changeslist-links' ] );
454  foreach ( $tools as $tool ) {
455  $links .= Html::rawElement( 'span', [], $tool ) . ' ';
456  }
457  $links = trim( $links ) . Html::closeElement( 'span' );
458 
459  // Show a note if the user is blocked and display the last block log entry.
460  // Do not expose the autoblocks, since that may lead to a leak of accounts' IPs,
461  // and also this will display a totally irrelevant log entry as a current block.
462  if ( !$this->including() ) {
463  // For IP ranges you must give DatabaseBlock::newFromTarget the CIDR string
464  // and not a user object.
465  if ( IPUtils::isValidRange( $userObj->getName() ) ) {
466  $block = DatabaseBlock::newFromTarget( $userObj->getName(), $userObj->getName() );
467  } else {
468  $block = DatabaseBlock::newFromTarget( $userObj, $userObj );
469  }
470 
471  if ( $block !== null && $block->getType() != DatabaseBlock::TYPE_AUTO ) {
472  if ( $block->getType() == DatabaseBlock::TYPE_RANGE ) {
473  $nt = $this->namespaceInfo->getCanonicalName( NS_USER )
474  . ':' . $block->getTargetName();
475  }
476 
477  $out = $this->getOutput(); // showLogExtract() wants first parameter by reference
478  if ( $userObj->isAnon() ) {
479  $msgKey = $block->isSitewide() ?
480  'sp-contributions-blocked-notice-anon' :
481  'sp-contributions-blocked-notice-anon-partial';
482  } else {
483  $msgKey = $block->isSitewide() ?
484  'sp-contributions-blocked-notice' :
485  'sp-contributions-blocked-notice-partial';
486  }
487  // Allow local styling overrides for different types of block
488  $class = $block->isSitewide() ?
489  'mw-contributions-blocked-notice' :
490  'mw-contributions-blocked-notice-partial';
492  $out,
493  'block',
494  $nt,
495  '',
496  [
497  'lim' => 1,
498  'showIfEmpty' => false,
499  'msgKey' => [
500  $msgKey,
501  $userObj->getName() # Support GENDER in 'sp-contributions-blocked-notice'
502  ],
503  'offset' => '', # don't use WebRequest parameter offset
504  'wrap' => Html::rawElement(
505  'div',
506  [ 'class' => $class ],
507  '$1'
508  ),
509  ]
510  );
511  }
512  }
513  }
514 
515  return Html::rawElement( 'div', [ 'class' => 'mw-contributions-user-tools' ],
516  $this->msg( 'contributions-subtitle' )->rawParams( $user )->params( $userObj->getName() )
517  . ' ' . $links
518  );
519  }
520 
531  public static function getUserLinks(
532  SpecialPage $sp,
533  User $target,
534  PermissionManager $permissionManager = null,
535  HookRunner $hookRunner = null
536  ) {
537  // Fallback to global state, if not provided
538  $permissionManager = $permissionManager ?? MediaWikiServices::getInstance()->getPermissionManager();
539  $hookRunner = $hookRunner ?? Hooks::runner();
540 
541  $id = $target->getId();
542  $username = $target->getName();
543  $userpage = $target->getUserPage();
544  $talkpage = $target->getTalkPage();
545  $isIP = IPUtils::isValid( $username );
546  $isRange = IPUtils::isValidRange( $username );
547 
548  $linkRenderer = $sp->getLinkRenderer();
549 
550  $tools = [];
551  # No talk pages for IP ranges.
552  if ( !$isRange ) {
553  $tools['user-talk'] = $linkRenderer->makeLink(
554  $talkpage,
555  $sp->msg( 'sp-contributions-talk' )->text(),
556  [ 'class' => 'mw-contributions-link-talk' ]
557  );
558  }
559 
560  # Block / Change block / Unblock links
561  if ( $permissionManager->userHasRight( $sp->getUser(), 'block' ) ) {
562  if ( $target->getBlock() && $target->getBlock()->getType() != DatabaseBlock::TYPE_AUTO ) {
563  $tools['block'] = $linkRenderer->makeKnownLink( # Change block link
564  SpecialPage::getTitleFor( 'Block', $username ),
565  $sp->msg( 'change-blocklink' )->text(),
566  [ 'class' => 'mw-contributions-link-change-block' ]
567  );
568  $tools['unblock'] = $linkRenderer->makeKnownLink( # Unblock link
569  SpecialPage::getTitleFor( 'Unblock', $username ),
570  $sp->msg( 'unblocklink' )->text(),
571  [ 'class' => 'mw-contributions-link-unblock' ]
572  );
573  } else { # User is not blocked
574  $tools['block'] = $linkRenderer->makeKnownLink( # Block link
575  SpecialPage::getTitleFor( 'Block', $username ),
576  $sp->msg( 'blocklink' )->text(),
577  [ 'class' => 'mw-contributions-link-block' ]
578  );
579  }
580  }
581 
582  # Block log link
583  $tools['log-block'] = $linkRenderer->makeKnownLink(
584  SpecialPage::getTitleFor( 'Log', 'block' ),
585  $sp->msg( 'sp-contributions-blocklog' )->text(),
586  [ 'class' => 'mw-contributions-link-block-log' ],
587  [ 'page' => $userpage->getPrefixedText() ]
588  );
589 
590  # Suppression log link (T61120)
591  if ( $permissionManager->userHasRight( $sp->getUser(), 'suppressionlog' ) ) {
592  $tools['log-suppression'] = $linkRenderer->makeKnownLink(
593  SpecialPage::getTitleFor( 'Log', 'suppress' ),
594  $sp->msg( 'sp-contributions-suppresslog', $username )->text(),
595  [ 'class' => 'mw-contributions-link-suppress-log' ],
596  [ 'offender' => $username ]
597  );
598  }
599 
600  # Don't show some links for IP ranges
601  if ( !$isRange ) {
602  # Uploads: hide if IPs cannot upload (T220674)
603  if ( !$isIP || $permissionManager->userHasRight( $target, 'upload' ) ) {
604  $tools['uploads'] = $linkRenderer->makeKnownLink(
605  SpecialPage::getTitleFor( 'Listfiles', $username ),
606  $sp->msg( 'sp-contributions-uploads' )->text(),
607  [ 'class' => 'mw-contributions-link-uploads' ]
608  );
609  }
610 
611  # Other logs link
612  # Todo: T146628
613  $tools['logs'] = $linkRenderer->makeKnownLink(
614  SpecialPage::getTitleFor( 'Log', $username ),
615  $sp->msg( 'sp-contributions-logs' )->text(),
616  [ 'class' => 'mw-contributions-link-logs' ]
617  );
618 
619  # Add link to deleted user contributions for privileged users
620  # Todo: T183457
621  if ( $permissionManager->userHasRight( $sp->getUser(), 'deletedhistory' ) ) {
622  $tools['deletedcontribs'] = $linkRenderer->makeKnownLink(
623  SpecialPage::getTitleFor( 'DeletedContributions', $username ),
624  $sp->msg( 'sp-contributions-deleted', $username )->text(),
625  [ 'class' => 'mw-contributions-link-deleted-contribs' ]
626  );
627  }
628  }
629 
630  # Add a link to change user rights for privileged users
631  $userrightsPage = new UserrightsPage();
632  $userrightsPage->setContext( $sp->getContext() );
633  if ( $userrightsPage->userCanChangeRights( $target ) ) {
634  $tools['userrights'] = $linkRenderer->makeKnownLink(
635  SpecialPage::getTitleFor( 'Userrights', $username ),
636  $sp->msg( 'sp-contributions-userrights', $username )->text(),
637  [ 'class' => 'mw-contributions-link-user-rights' ]
638  );
639  }
640 
641  $hookRunner->onContributionsToolLinks( $id, $userpage, $tools, $sp );
642 
643  return $tools;
644  }
645 
652  protected function getForm( array $pagerOptions ) {
653  // Modules required only for the form
654  $this->getOutput()->addModules( [
655  'mediawiki.special.contributions',
656  ] );
657  $this->getOutput()->addModuleStyles( 'mediawiki.widgets.DateInputWidget.styles' );
658  $this->getOutput()->enableOOUI();
659  $fields = [];
660 
661  # Add hidden params for tracking except for parameters in $skipParameters
662  $skipParameters = [
663  'namespace',
664  'nsInvert',
665  'deletedOnly',
666  'target',
667  'year',
668  'month',
669  'start',
670  'end',
671  'topOnly',
672  'newOnly',
673  'hideMinor',
674  'associated',
675  'tagfilter',
676  'title',
677  ];
678 
679  foreach ( $this->opts as $name => $value ) {
680  if ( in_array( $name, $skipParameters ) ) {
681  continue;
682  }
683 
684  $fields[$name] = [
685  'name' => $name,
686  'type' => 'hidden',
687  'default' => $value,
688  ];
689  }
690 
691  $target = $this->opts['target'] ?? null;
692  $fields['target'] = [
693  'type' => 'user',
694  'default' => $target ?
695  str_replace( '_', ' ', $target ) : '' ,
696  'label' => $this->msg( 'sp-contributions-username' )->text(),
697  'name' => 'target',
698  'id' => 'mw-target-user-or-ip',
699  'size' => 40,
700  'autofocus' => !$target,
701  'section' => 'contribs-top',
702  ];
703 
704  $ns = $this->opts['namespace'] ?? null;
705  $fields['namespace'] = [
706  'type' => 'namespaceselect',
707  'label' => $this->msg( 'namespace' )->text(),
708  'name' => 'namespace',
709  'cssclass' => 'namespaceselector',
710  'default' => $ns,
711  'id' => 'namespace',
712  'section' => 'contribs-top',
713  ];
714  $fields['nsFilters'] = [
715  'class' => HTMLMultiSelectField::class,
716  'label' => '',
717  'name' => 'wpfilters',
718  'flatlist' => true,
719  // Only shown when namespaces are selected.
720  'hide-if' => [ '===', 'namespace', 'all' ],
721  'options-messages' => [
722  'invert' => 'nsInvert',
723  'namespace_association' => 'associated',
724  ],
725  'section' => 'contribs-top',
726  ];
727  $fields['tagfilter'] = [
728  'type' => 'tagfilter',
729  'cssclass' => 'mw-tagfilter-input',
730  'id' => 'tagfilter',
731  'label-message' => [ 'tag-filter', 'parse' ],
732  'name' => 'tagfilter',
733  'size' => 20,
734  'section' => 'contribs-top',
735  ];
736 
737  if ( $this->permissionManager->userHasRight( $this->getUser(), 'deletedhistory' ) ) {
738  $fields['deletedOnly'] = [
739  'type' => 'check',
740  'id' => 'mw-show-deleted-only',
741  'label' => $this->msg( 'history-show-deleted' )->text(),
742  'name' => 'deletedOnly',
743  'section' => 'contribs-top',
744  ];
745  }
746 
747  $fields['topOnly'] = [
748  'type' => 'check',
749  'id' => 'mw-show-top-only',
750  'label' => $this->msg( 'sp-contributions-toponly' )->text(),
751  'name' => 'topOnly',
752  'section' => 'contribs-top',
753  ];
754  $fields['newOnly'] = [
755  'type' => 'check',
756  'id' => 'mw-show-new-only',
757  'label' => $this->msg( 'sp-contributions-newonly' )->text(),
758  'name' => 'newOnly',
759  'section' => 'contribs-top',
760  ];
761  $fields['hideMinor'] = [
762  'type' => 'check',
763  'cssclass' => 'mw-hide-minor-edits',
764  'id' => 'mw-show-new-only',
765  'label' => $this->msg( 'sp-contributions-hideminor' )->text(),
766  'name' => 'hideMinor',
767  'section' => 'contribs-top',
768  ];
769 
770  // Allow additions at this point to the filters.
771  $rawFilters = [];
772  $this->getHookRunner()->onSpecialContributions__getForm__filters(
773  $this, $rawFilters );
774  foreach ( $rawFilters as $filter ) {
775  // Backwards compatibility support for previous hook function signature.
776  if ( is_string( $filter ) ) {
777  $fields[] = [
778  'type' => 'info',
779  'default' => $filter,
780  'raw' => true,
781  'section' => 'contribs-top',
782  ];
784  'A SpecialContributions::getForm::filters hook handler returned ' .
785  'an array of strings, this is deprecated since MediaWiki 1.33',
786  '1.33', false, false
787  );
788  } else {
789  // Preferred append method.
790  $fields[] = $filter;
791  }
792  }
793 
794  $fields['start'] = [
795  'type' => 'date',
796  'default' => '',
797  'id' => 'mw-date-start',
798  'label' => $this->msg( 'date-range-from' )->text(),
799  'name' => 'start',
800  'section' => 'contribs-date',
801  ];
802  $fields['end'] = [
803  'type' => 'date',
804  'default' => '',
805  'id' => 'mw-date-end',
806  'label' => $this->msg( 'date-range-to' )->text(),
807  'name' => 'end',
808  'section' => 'contribs-date',
809  ];
810 
811  $htmlForm = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
812  $htmlForm
813  ->setMethod( 'get' )
814  ->setTitle( $this->getPageTitle() )
815  // When offset is defined, the user is paging through results
816  // so we hide the form by default to allow users to focus on browsing
817  // rather than defining search parameters
818  ->setCollapsibleOptions(
819  ( $pagerOptions['target'] ?? null ) ||
820  ( $pagerOptions['start'] ?? null ) ||
821  ( $pagerOptions['end'] ?? null )
822  )
823  ->setAction( wfScript() )
824  ->setSubmitTextMsg( 'sp-contributions-submit' )
825  ->setWrapperLegendMsg( 'sp-contributions-search' );
826 
827  $explain = $this->msg( 'sp-contributions-explain' );
828  if ( !$explain->isBlank() ) {
829  $htmlForm->addFooterText( "<p id='mw-sp-contributions-explain'>{$explain->parse()}</p>" );
830  }
831 
832  $htmlForm->prepareForm();
833 
834  return $htmlForm->getHTML( false );
835  }
836 
845  public function prefixSearchSubpages( $search, $limit, $offset ) {
846  $search = $this->userNameUtils->getCanonical( $search );
847  if ( !$search ) {
848  // No prefix suggestion for invalid user
849  return [];
850  }
851  // Autocomplete subpage as user list - public to allow caching
852  return $this->userNamePrefixSearch
853  ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset );
854  }
855 
860  private function getPager( $targetUser ) {
861  if ( $this->pager === null ) {
862  $options = [
863  'namespace' => $this->opts['namespace'],
864  'tagfilter' => $this->opts['tagfilter'],
865  'start' => $this->opts['start'] ?? '',
866  'end' => $this->opts['end'] ?? '',
867  'deletedOnly' => $this->opts['deletedOnly'],
868  'topOnly' => $this->opts['topOnly'],
869  'newOnly' => $this->opts['newOnly'],
870  'hideMinor' => $this->opts['hideMinor'],
871  'nsInvert' => $this->opts['nsInvert'],
872  'associated' => $this->opts['associated'],
873  ];
874 
875  $this->pager = new ContribsPager(
876  $this->getContext(),
877  $options,
878  $this->getLinkRenderer(),
879  $this->linkBatchFactory,
880  $this->getHookContainer(),
881  $this->loadBalancer,
882  $this->actorMigration,
883  $this->revisionStore,
884  $this->namespaceInfo,
885  $targetUser,
886  $this->commentFormatter
887  );
888  }
889 
890  return $this->pager;
891  }
892 
893  protected function getGroupName() {
894  return 'users';
895  }
896 }
getUser()
const NS_USER
Definition: Defines.php:66
const NS_MAIN
Definition: Defines.php:64
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
getContext()
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...
Pager for Special:Contributions.
static processDateFilter(array $opts)
Set up date filter options, given request data.
isQueryableRange( $ipRange)
Is the given IP a range and within the CIDR limit?
static isExternal( $username)
Tells whether the username is external or not.
static factory( $displayFormat, $descriptor, IContextSource $context, $messagePrefix='')
Construct a HTMLForm object for given display type.
Definition: HTMLForm.php:338
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:214
static warningBox( $html, $className='')
Return a warning box.
Definition: Html.php:775
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing '/'...
Definition: Html.php:256
static errorBox( $html, $heading='', $className='')
Return an error box.
Definition: Html.php:788
static closeElement( $element)
Returns "</$element>".
Definition: Html.php:320
Shortcut to construct an includable special page.
getBody()
Get the formatted result list.
Definition: IndexPager.php:608
getNumRows()
Get the number of rows in the result set.
Definition: IndexPager.php:776
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
A DatabaseBlock (unlike a SystemBlock) is stored in the database, may give rise to autoblocks and may...
This is the main service interface for converting single-line comments from various DB comment fields...
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:562
A class containing constants representing the names of configuration variables.
MediaWikiServices is the service locator for the application scope of MediaWiki.
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
Service for looking up page revisions.
Creates User objects.
Definition: UserFactory.php:38
Handles searching prefixes of user names.
UserNameUtils service.
Provides access to user options.
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Convenience class for dealing with PoolCounters using callbacks.
execute( $skipcache=false)
Get the result of the work (whatever it is), or the result of the error() function.
Special:Contributions, show user contributions in a paged list.
ContribsPager null $pager
LinkBatchFactory $linkBatchFactory
ActorMigration $actorMigration
UserOptionsLookup $userOptionsLookup
__construct(LinkBatchFactory $linkBatchFactory=null, PermissionManager $permissionManager=null, ILoadBalancer $loadBalancer=null, ActorMigration $actorMigration=null, RevisionStore $revisionStore=null, NamespaceInfo $namespaceInfo=null, UserNameUtils $userNameUtils=null, UserNamePrefixSearch $userNamePrefixSearch=null, UserOptionsLookup $userOptionsLookup=null, CommentFormatter $commentFormatter=null, UserFactory $userFactory=null)
prefixSearchSubpages( $search, $limit, $offset)
Return an array of subpages beginning with $search that this special page will accept.
getForm(array $pagerOptions)
Generates the namespace selector form with hidden attributes.
execute( $par)
Default execute method Checks user permissions.
PermissionManager $permissionManager
CommentFormatter $commentFormatter
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
UserNamePrefixSearch $userNamePrefixSearch
contributionsSub( $userObj, $targetName)
Generates the subheading with links.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
getOutput()
Get the OutputPage being used for this instance.
getUser()
Shortcut to get the User executing this instance.
getSkin()
Shortcut to get the skin being used for this instance.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
addFeedLinks( $params)
Adds RSS/atom links.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getConfig()
Shortcut to get main config object.
getRequest()
Get the WebRequest being used for this instance.
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
including( $x=null)
Whether the special page is being evaluated via transclusion.
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:663
Special page to allow managing user group membership.
Shared interface for rigor levels when dealing with User methods.
Database cluster connection, tracking, load balancing, and transaction manager interface.