96 parent::__construct(
'Contributions' );
98 $services = MediaWikiServices::getInstance();
101 $this->loadBalancer =
$loadBalancer ?? $services->getDBLoadBalancer();
102 $this->actorMigration =
$actorMigration ?? $services->getActorMigration();
103 $this->revisionStore =
$revisionStore ?? $services->getRevisionStore();
104 $this->namespaceInfo =
$namespaceInfo ?? $services->getNamespaceInfo();
105 $this->userNameUtils =
$userNameUtils ?? $services->getUserNameUtils();
115 $out->addModuleStyles( [
116 'jquery.makeCollapsible.styles',
117 'mediawiki.interface.helpers.styles',
119 'mediawiki.special.changeslist',
122 'mediawiki.special.recentchanges',
124 'mediawiki.page.ready'
131 $target = $par ?? $request->getVal(
'target' );
133 $this->opts[
'deletedOnly'] = $request->getBool(
'deletedOnly' );
135 if ( !strlen( $target ) ) {
137 $out->addHTML( $this->
getForm( $this->opts ) );
145 $this->opts[
'limit'] = $request->getInt(
'limit', $this->userOptionsLookup->getIntOption( $user,
'rclimit' ) );
146 $this->opts[
'target'] = $target;
147 $this->opts[
'topOnly'] = $request->getBool(
'topOnly' );
148 $this->opts[
'newOnly'] = $request->getBool(
'newOnly' );
149 $this->opts[
'hideMinor'] = $request->getBool(
'hideMinor' );
151 $ns = $request->getVal(
'namespace',
null );
152 if ( $ns !==
null && $ns !==
'' && $ns !==
'all' ) {
153 $this->opts[
'namespace'] = intval( $ns );
155 $this->opts[
'namespace'] =
'';
161 $this->opts[
'associated'] = $request->getBool(
'associated' );
162 $this->opts[
'nsInvert'] = (bool)$request->getVal(
'nsInvert' );
163 $nsFilters = $request->getArray(
'wpfilters',
null );
164 if ( $nsFilters !==
null ) {
165 $this->opts[
'associated'] = in_array(
'associated', $nsFilters );
166 $this->opts[
'nsInvert'] = in_array(
'nsInvert', $nsFilters );
169 $this->opts[
'tagfilter'] = (string)$request->getVal(
'tagfilter' );
173 if ( $this->permissionManager->userHasRight( $user,
'markbotedits' ) && $request->getBool(
'bot' ) ) {
174 $this->opts[
'bot'] =
'1';
177 $skip = $request->getText(
'offset' ) || $request->getText(
'dir' ) ==
'prev';
178 # Offset overrides year/month selection
180 $this->opts[
'year'] = $request->getVal(
'year' );
181 $this->opts[
'month'] = $request->getVal(
'month' );
183 $this->opts[
'start'] = $request->getVal(
'start' );
184 $this->opts[
'end'] = $request->getVal(
'end' );
188 if ( ExternalUserNames::isExternal( $target ) ) {
191 $out->addHTML( $this->
getForm( $this->opts ) );
196 $out->setPageTitle( $this->
msg(
'contributions-title', $target )->escaped() );
198 $nt = Title::makeTitleSafe(
NS_USER, $target );
200 $out->addHTML( $this->
getForm( $this->opts ) );
205 $out->addHTML( $this->
getForm( $this->opts ) );
208 $id = $userObj->getId();
210 $target = $nt->getText();
212 $out->setPageTitle( $this->
msg(
'contributions-title', $target )->escaped() );
214 # For IP ranges, we want the contributionsSub, but not the skin-dependent
215 # links under 'Tools', which may include irrelevant links like 'Logs'.
216 if ( !IPUtils::isValidRange( $target ) &&
217 ( $this->userNameUtils->isIP( $target ) || $userObj->isRegistered() )
225 $this->
getSkin()->setRelevantUser( $userObj );
229 $this->opts = ContribsPager::processDateFilter( $this->opts );
231 if ( $this->opts[
'namespace'] !==
'' && $this->opts[
'namespace'] <
NS_MAIN ) {
233 "<div class=\"mw-negative-namespace-not-supported error\">\n\$1\n</div>",
234 [
'negative-namespace-not-supported' ]
236 $out->addHTML( $this->
getForm( $this->opts ) );
240 $feedType = $request->getVal(
'feed' );
243 'action' =>
'feedcontributions',
246 if ( $this->opts[
'topOnly'] ) {
247 $feedParams[
'toponly'] =
true;
249 if ( $this->opts[
'newOnly'] ) {
250 $feedParams[
'newonly'] =
true;
252 if ( $this->opts[
'hideMinor'] ) {
253 $feedParams[
'hideminor'] =
true;
255 if ( $this->opts[
'deletedOnly'] ) {
256 $feedParams[
'deletedonly'] =
true;
258 if ( $this->opts[
'tagfilter'] !==
'' ) {
259 $feedParams[
'tagfilter'] = $this->opts[
'tagfilter'];
261 if ( $this->opts[
'namespace'] !==
'' ) {
262 $feedParams[
'namespace'] = $this->opts[
'namespace'];
266 if ( $feedType && isset( $this->opts[
'year'] ) ) {
267 $feedParams[
'year'] = $this->opts[
'year'];
269 if ( $feedType && isset( $this->opts[
'month'] ) ) {
270 $feedParams[
'month'] = $this->opts[
'month'];
276 $feedParams[
'feedformat'] = $feedType;
279 $out->redirect( $url,
'301' );
287 if ( $this->
getHookRunner()->onSpecialContributionsBeforeMainOutput(
288 $id, $userObj, $this )
291 $out->addHTML( $this->
getForm( $this->opts ) );
296 $limits = $this->
getConfig()->get(
'RangeContributionsCIDRLimit' );
297 $limit = $limits[ IPUtils::isIPv4( $target ) ?
'IPv4' :
'IPv6' ];
298 $out->addWikiMsg(
'sp-contributions-outofrange', $limit );
302 $poolKey = $this->loadBalancer->getLocalDomainID() .
':SpecialContributions:';
303 if ( $this->
getUser()->isAnon() ) {
304 $poolKey .=
'a:' . $this->
getUser()->getName();
306 $poolKey .=
'u:' . $this->
getUser()->getId();
309 'doWork' =>
function () use (
$pager, $out, $target ) {
311 $out->addWikiMsg(
'nocontribs', $target );
313 # Show a message about replica DB lag, if applicable
314 $lag = $pager->getDatabase()->getSessionLagStatus()[
'lag'];
316 $out->showLagWarning( $lag );
325 $out->addHTML( $output );
328 'error' =>
function () use ( $out ) {
329 $msg = $this->
getUser()->isAnon()
330 ?
'sp-contributions-concurrency-ip'
331 :
'sp-contributions-concurrency-user';
332 $out->wrapWikiMsg(
"<div class='errorbox'>\n$1\n</div>", $msg );
340 # Show the appropriate "footer" message - WHOIS tools, etc.
342 $message =
'sp-contributions-footer-anon-range';
343 } elseif ( IPUtils::isIPAddress( $target ) ) {
344 $message =
'sp-contributions-footer-anon';
345 } elseif ( $userObj->isAnon() ) {
348 } elseif ( $userObj->isHidden() &&
349 !$this->permissionManager->userHasRight( $this->getUser(),
'hideuser' )
356 $message =
'sp-contributions-footer';
359 if ( $message && !$this->
including() && !$this->
msg( $message, $target )->isDisabled() ) {
361 "<div class='mw-contributions-footer'>\n$1\n</div>",
362 [ $message, $target ] );
377 $isAnon = $userObj->isAnon();
378 if ( !$isAnon && $userObj->isHidden() &&
379 !$this->permissionManager->userHasRight( $this->getUser(),
'hideuser' )
391 if ( !$this->userNameUtils->isIP( $userObj->getName() )
392 && !IPUtils::isValidRange( $userObj->getName() )
394 $this->getOutput()->wrapWikiMsg(
395 "<div class=\"mw-userpage-userdoesnotexist error\">\n\$1\n</div>",
397 'contributions-userdoesnotexist',
401 if ( !$this->including() ) {
402 $this->getOutput()->setStatusCode( 404 );
405 $user = htmlspecialchars( $userObj->getName() );
407 $user = $this->getLinkRenderer()->makeLink( $userObj->getUserPage(), $userObj->getName() );
409 $nt = $userObj->getUserPage();
410 $talk = $userObj->getTalkPage();
414 $showForIp = IPUtils::isValid( $userObj ) ||
415 ( IPUtils::isValidRange( $userObj ) && $this->getPager( $userObj )->isQueryableRange( $userObj ) );
418 $registeredAndVisible = $userObj->isRegistered() && ( !$userObj->isHidden()
419 || $this->permissionManager->userHasRight( $this->getUser(),
'hideuser' ) );
421 if ( $talk && ( $registeredAndVisible || $showForIp ) ) {
422 $tools = self::getUserLinks(
425 $this->permissionManager,
426 $this->getHookRunner()
428 $links = Html::openElement(
'span', [
'class' =>
'mw-changeslist-links' ] );
429 foreach ( $tools as $tool ) {
430 $links .= Html::rawElement(
'span', [], $tool ) .
' ';
432 $links = trim( $links ) . Html::closeElement(
'span' );
437 if ( !$this->including() ) {
440 if ( IPUtils::isValidRange( $userObj->getName() ) ) {
441 $block = DatabaseBlock::newFromTarget( $userObj->getName(), $userObj->getName() );
443 $block = DatabaseBlock::newFromTarget( $userObj, $userObj );
446 if ( $block !==
null && $block->getType() != DatabaseBlock::TYPE_AUTO ) {
447 if ( $block->getType() == DatabaseBlock::TYPE_RANGE ) {
448 $nt = $this->namespaceInfo->getCanonicalName(
NS_USER )
449 .
':' . $block->getTargetName();
452 $out = $this->getOutput();
453 if ( $userObj->isAnon() ) {
454 $msgKey = $block->isSitewide() ?
455 'sp-contributions-blocked-notice-anon' :
456 'sp-contributions-blocked-notice-anon-partial';
458 $msgKey = $block->isSitewide() ?
459 'sp-contributions-blocked-notice' :
460 'sp-contributions-blocked-notice-partial';
463 $class = $block->isSitewide() ?
464 'mw-contributions-blocked-notice' :
465 'mw-contributions-blocked-notice-partial';
466 LogEventsList::showLogExtract(
473 'showIfEmpty' =>
false,
476 $userObj->getName() # Support GENDER in
'sp-contributions-blocked-notice'
478 'offset' =>
'', # don
't use WebRequest parameter offset
479 'wrap
' => Html::rawElement(
481 [ 'class' => $class ],
490 return Html::rawElement( 'div
', [ 'class' => 'mw-contributions-user-tools
' ],
491 $this->msg( 'contributions-subtitle
' )->rawParams( $user )->params( $userObj->getName() )
506 public static function getUserLinks(
509 PermissionManager $permissionManager = null,
510 HookRunner $hookRunner = null
512 // Fallback to global state, if not provided
513 $permissionManager = $permissionManager ?? MediaWikiServices::getInstance()->getPermissionManager();
514 $hookRunner = $hookRunner ?? Hooks::runner();
516 $id = $target->getId();
517 $username = $target->getName();
518 $userpage = $target->getUserPage();
519 $talkpage = $target->getTalkPage();
520 $isIP = IPUtils::isValid( $username );
521 $isRange = IPUtils::isValidRange( $username );
523 $linkRenderer = $sp->getLinkRenderer();
526 # No talk pages for IP ranges.
528 $tools['user-talk
'] = $linkRenderer->makeLink(
530 $sp->msg( 'sp-contributions-talk
' )->text()
534 # Block / Change block / Unblock links
535 if ( $permissionManager->userHasRight( $sp->getUser(), 'block
' ) ) {
536 if ( $target->getBlock() && $target->getBlock()->getType() != DatabaseBlock::TYPE_AUTO ) {
537 $tools['block
'] = $linkRenderer->makeKnownLink( # Change block link
538 SpecialPage::getTitleFor( 'Block
', $username ),
539 $sp->msg( 'change-blocklink
' )->text()
541 $tools['unblock
'] = $linkRenderer->makeKnownLink( # Unblock link
542 SpecialPage::getTitleFor( 'Unblock
', $username ),
543 $sp->msg( 'unblocklink
' )->text()
545 } else { # User is not blocked
546 $tools['block
'] = $linkRenderer->makeKnownLink( # Block link
547 SpecialPage::getTitleFor( 'Block
', $username ),
548 $sp->msg( 'blocklink
' )->text()
554 $tools['log-block
'] = $linkRenderer->makeKnownLink(
555 SpecialPage::getTitleFor( 'Log
', 'block
' ),
556 $sp->msg( 'sp-contributions-blocklog
' )->text(),
558 [ 'page
' => $userpage->getPrefixedText() ]
561 # Suppression log link (T61120)
562 if ( $permissionManager->userHasRight( $sp->getUser(), 'suppressionlog
' ) ) {
563 $tools['log-suppression
'] = $linkRenderer->makeKnownLink(
564 SpecialPage::getTitleFor( 'Log
', 'suppress
' ),
565 $sp->msg( 'sp-contributions-suppresslog
', $username )->text(),
567 [ 'offender
' => $username ]
571 # Don't show some links
for IP ranges
573 # Uploads: hide if IPs cannot upload (T220674)
574 if ( !$isIP || $permissionManager->userHasRight( $target,
'upload' ) ) {
575 $tools[
'uploads'] = $linkRenderer->makeKnownLink(
576 SpecialPage::getTitleFor(
'Listfiles', $username ),
577 $sp->msg(
'sp-contributions-uploads' )->text()
583 $tools[
'logs'] = $linkRenderer->makeKnownLink(
585 $sp->msg(
'sp-contributions-logs' )->text()
588 # Add link to deleted user contributions for priviledged users
590 if ( $permissionManager->userHasRight( $sp->getUser(),
'deletedhistory' ) ) {
591 $tools[
'deletedcontribs'] = $linkRenderer->makeKnownLink(
593 $sp->msg(
'sp-contributions-deleted', $username )->text()
598 # Add a link to change user rights for privileged users
600 $userrightsPage->setContext( $sp->getContext() );
601 if ( $userrightsPage->userCanChangeRights( $target ) ) {
602 $tools[
'userrights'] = $linkRenderer->makeKnownLink(
604 $sp->msg(
'sp-contributions-userrights', $username )->text()
608 $hookRunner->onContributionsToolLinks( $id, $userpage, $tools, $sp );
619 protected function getForm( array $pagerOptions ) {
620 $this->opts[
'title'] = $this->getPageTitle()->getPrefixedText();
622 $this->getOutput()->addModules( [
623 'mediawiki.special.contributions',
625 $this->getOutput()->addModuleStyles(
'mediawiki.widgets.DateInputWidget.styles' );
626 $this->getOutput()->enableOOUI();
629 # Add hidden params for tracking except for parameters in $skipParameters
646 foreach ( $this->opts as $name => $value ) {
647 if ( in_array( $name, $skipParameters ) ) {
658 $target = $this->opts[
'target'] ??
null;
659 $fields[
'target'] = [
661 'default' => $target ?
662 str_replace(
'_',
' ', $target ) :
'' ,
663 'label' => $this->msg(
'sp-contributions-username' )->text(),
665 'id' =>
'mw-target-user-or-ip',
667 'autofocus' => !$target,
668 'section' =>
'contribs-top',
671 $ns = $this->opts[
'namespace'] ??
null;
672 $fields[
'namespace'] = [
673 'type' =>
'namespaceselect',
674 'label' => $this->msg(
'namespace' )->text(),
675 'name' =>
'namespace',
676 'cssclass' =>
'namespaceselector',
679 'section' =>
'contribs-top',
681 $fields[
'nsFilters'] = [
682 'class' => HTMLMultiSelectField::class,
684 'name' =>
'wpfilters',
687 'cssclass' => $ns ===
'' ?
688 'contribs-ns-filters mw-input-with-label mw-input-hidden' :
689 'contribs-ns-filters mw-input-with-label',
693 'options-messages' => [
694 'invert' =>
'nsInvert',
695 'namespace_association' =>
'associated',
697 'section' =>
'contribs-top',
699 $fields[
'tagfilter'] = [
700 'type' =>
'tagfilter',
701 'cssclass' =>
'mw-tagfilter-input',
703 'label-message' => [
'tag-filter',
'parse' ],
704 'name' =>
'tagfilter',
706 'section' =>
'contribs-top',
709 if ( $this->permissionManager->userHasRight( $this->getUser(),
'deletedhistory' ) ) {
710 $fields[
'deletedOnly'] = [
712 'id' =>
'mw-show-deleted-only',
713 'label' => $this->msg(
'history-show-deleted' )->text(),
714 'name' =>
'deletedOnly',
715 'section' =>
'contribs-top',
719 $fields[
'topOnly'] = [
721 'id' =>
'mw-show-top-only',
722 'label' => $this->msg(
'sp-contributions-toponly' )->text(),
724 'section' =>
'contribs-top',
726 $fields[
'newOnly'] = [
728 'id' =>
'mw-show-new-only',
729 'label' => $this->msg(
'sp-contributions-newonly' )->text(),
731 'section' =>
'contribs-top',
733 $fields[
'hideMinor'] = [
735 'cssclass' =>
'mw-hide-minor-edits',
736 'id' =>
'mw-show-new-only',
737 'label' => $this->msg(
'sp-contributions-hideminor' )->text(),
738 'name' =>
'hideMinor',
739 'section' =>
'contribs-top',
744 $this->getHookRunner()->onSpecialContributions__getForm__filters(
745 $this, $rawFilters );
746 foreach ( $rawFilters as $filter ) {
748 if ( is_string( $filter ) ) {
751 'default' => $filter,
753 'section' =>
'contribs-top',
756 'A SpecialContributions::getForm::filters hook handler returned ' .
757 'an array of strings, this is deprecated since MediaWiki 1.33',
769 'id' =>
'mw-date-start',
770 'label' => $this->msg(
'date-range-from' )->text(),
772 'section' =>
'contribs-date',
777 'id' =>
'mw-date-end',
778 'label' => $this->msg(
'date-range-to' )->text(),
780 'section' =>
'contribs-date',
783 $htmlForm = HTMLForm::factory(
'ooui', $fields, $this->
getContext() );
789 ->setCollapsibleOptions(
790 ( $pagerOptions[
'target'] ??
null ) ||
791 ( $pagerOptions[
'start'] ??
null ) ||
792 ( $pagerOptions[
'end'] ??
null )
795 ->setSubmitTextMsg(
'sp-contributions-submit' )
796 ->setWrapperLegendMsg(
'sp-contributions-search' );
798 $explain = $this->msg(
'sp-contributions-explain' );
799 if ( !$explain->isBlank() ) {
800 $htmlForm->addFooterText(
"<p id='mw-sp-contributions-explain'>{$explain->parse()}</p>" );
803 $htmlForm->loadData();
805 return $htmlForm->getHTML(
false );
817 $search = $this->userNameUtils->getCanonical( $search );
823 return $this->userNamePrefixSearch
824 ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset );
832 if ( $this->pager ===
null ) {
834 'namespace' => $this->opts[
'namespace'],
835 'tagfilter' => $this->opts[
'tagfilter'],
836 'start' => $this->opts[
'start'] ??
'',
837 'end' => $this->opts[
'end'] ??
'',
838 'deletedOnly' => $this->opts[
'deletedOnly'],
839 'topOnly' => $this->opts[
'topOnly'],
840 'newOnly' => $this->opts[
'newOnly'],
841 'hideMinor' => $this->opts[
'hideMinor'],
842 'nsInvert' => $this->opts[
'nsInvert'],
843 'associated' => $this->opts[
'associated'],
849 $this->getLinkRenderer(),
850 $this->linkBatchFactory,
851 $this->getHookContainer(),
853 $this->actorMigration,
854 $this->revisionStore,
855 $this->namespaceInfo,
UserOptionsLookup $userOptionsLookup
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,...
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
if(ini_get('mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
This is not intended to be a long-term part of MediaWiki; it will be deprecated and removed once acto...
Pre-librarized class name for IPUtils.
Shortcut to construct an includable special page.
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.
UserNameUtils $userNameUtils
ContribsPager null $pager
ILoadBalancer $loadBalancer
__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)
NamespaceInfo $namespaceInfo
LinkBatchFactory $linkBatchFactory
ActorMigration $actorMigration
UserOptionsLookup $userOptionsLookup
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
RevisionStore $revisionStore
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 newFromName( $name, $validate='valid')
Special page to allow managing user group membership.