37 use Wikimedia\IPUtils;
110 parent::__construct(
'Contributions' );
112 $services = MediaWikiServices::getInstance();
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();
123 $this->userFactory =
$userFactory ?? $services->getUserFactory();
131 $out->addModuleStyles( [
132 'jquery.makeCollapsible.styles',
133 'mediawiki.interface.helpers.styles',
135 'mediawiki.special.changeslist',
139 'mediawiki.page.ready'
146 $target = $par ?? $request->getVal(
'target',
'' );
147 '@phan-var string $target';
149 $this->opts[
'deletedOnly'] = $request->getBool(
'deletedOnly' );
151 if ( !strlen( $target ) ) {
153 $out->addHTML( $this->
getForm( $this->opts ) );
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' );
167 $ns = $request->getVal(
'namespace',
null );
168 if ( $ns !==
null && $ns !==
'' && $ns !==
'all' ) {
169 $this->opts[
'namespace'] = intval( $ns );
171 $this->opts[
'namespace'] =
'';
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 );
185 $this->opts[
'tagfilter'] = array_filter( explode(
187 (
string)$request->getVal(
'tagfilter' )
188 ),
static function ( $el ) {
194 if ( $this->permissionManager->userHasRight( $user,
'markbotedits' ) && $request->getBool(
'bot' ) ) {
195 $this->opts[
'bot'] =
'1';
198 $skip = $request->getText(
'offset' ) || $request->getText(
'dir' ) ==
'prev';
199 # Offset overrides year/month selection
201 $this->opts[
'year'] = $request->getIntOrNull(
'year' );
202 $this->opts[
'month'] = $request->getIntOrNull(
'month' );
204 $this->opts[
'start'] = $request->getVal(
'start' );
205 $this->opts[
'end'] = $request->getVal(
'end' );
210 $userObj = $this->userFactory->newFromName( $target, UserRigorOptions::RIGOR_NONE );
212 $out->addHTML( $this->
getForm( $this->opts ) );
217 $out->setPageTitle( $this->
msg(
'contributions-title', $target ) );
221 $out->addHTML( $this->
getForm( $this->opts ) );
224 $userObj = $this->userFactory->newFromName(
225 $nt->getText(), UserRigorOptions::RIGOR_NONE );
227 $out->addHTML( $this->
getForm( $this->opts ) );
230 $id = $userObj->getId();
232 $target = $nt->getText();
234 $out->setPageTitle( $this->
msg(
'contributions-title', $target ) );
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() )
247 $this->
getSkin()->setRelevantUser( $userObj );
253 if ( $this->opts[
'namespace'] !==
'' && $this->opts[
'namespace'] <
NS_MAIN ) {
255 "<div class=\"mw-negative-namespace-not-supported error\">\n\$1\n</div>",
256 [
'negative-namespace-not-supported' ]
258 $out->addHTML( $this->
getForm( $this->opts ) );
262 $feedType = $request->getVal(
'feed' );
265 'action' =>
'feedcontributions',
268 if ( $this->opts[
'topOnly'] ) {
269 $feedParams[
'toponly'] =
true;
271 if ( $this->opts[
'newOnly'] ) {
272 $feedParams[
'newonly'] =
true;
274 if ( $this->opts[
'hideMinor'] ) {
275 $feedParams[
'hideminor'] =
true;
277 if ( $this->opts[
'deletedOnly'] ) {
278 $feedParams[
'deletedonly'] =
true;
281 if ( $this->opts[
'tagfilter'] !== [] ) {
282 $feedParams[
'tagfilter'] = $this->opts[
'tagfilter'];
284 if ( $this->opts[
'namespace'] !==
'' ) {
285 $feedParams[
'namespace'] = $this->opts[
'namespace'];
289 if ( $feedType && isset( $this->opts[
'year'] ) ) {
290 $feedParams[
'year'] = $this->opts[
'year'];
292 if ( $feedType && isset( $this->opts[
'month'] ) ) {
293 $feedParams[
'month'] = $this->opts[
'month'];
299 $feedParams[
'feedformat'] = $feedType;
302 $out->redirect( $url,
'301' );
310 if ( $this->
getHookRunner()->onSpecialContributionsBeforeMainOutput(
311 $id, $userObj, $this )
314 $out->addHTML( $this->
getForm( $this->opts ) );
319 $limits = $this->
getConfig()->get( MainConfigNames::RangeContributionsCIDRLimit );
320 $limit = $limits[ IPUtils::isIPv4( $target ) ?
'IPv4' :
'IPv6' ];
321 $out->addWikiMsg(
'sp-contributions-outofrange', $limit );
325 $poolKey = $this->loadBalancer->getLocalDomainID() .
':SpecialContributions:';
326 if ( $this->
getUser()->isAnon() ) {
327 $poolKey .=
'a:' . $this->
getUser()->getName();
329 $poolKey .=
'u:' . $this->
getUser()->getId();
332 'doWork' =>
function () use (
$pager, $out, $target ) {
334 $out->addWikiMsg(
'nocontribs', $target );
336 # Show a message about replica DB lag, if applicable
337 $lag = $pager->getDatabase()->getSessionLagStatus()[
'lag'];
339 $out->showLagWarning( $lag );
348 $out->addHTML( $output );
351 'error' =>
function () use ( $out ) {
352 $msg = $this->
getUser()->isAnon()
353 ?
'sp-contributions-concurrency-ip'
354 :
'sp-contributions-concurrency-user';
357 $out->msg( $msg )->parse()
367 # Show the appropriate "footer" message - WHOIS tools, etc.
369 $message =
'sp-contributions-footer-anon-range';
370 } elseif ( IPUtils::isIPAddress( $target ) ) {
371 $message =
'sp-contributions-footer-anon';
372 } elseif ( $userObj->isAnon() ) {
375 } elseif ( $userObj->isHidden() &&
376 !$this->permissionManager->userHasRight( $this->getUser(),
'hideuser' )
383 $message =
'sp-contributions-footer';
386 if ( $message && !$this->
including() && !$this->
msg( $message, $target )->isDisabled() ) {
388 "<div class='mw-contributions-footer'>\n$1\n</div>",
389 [ $message, $target ] );
404 $isAnon = $userObj->isAnon();
405 if ( !$isAnon && $userObj->isHidden() &&
406 !$this->permissionManager->userHasRight( $this->getUser(),
'hideuser' )
418 if ( !$this->userNameUtils->isIP( $userObj->getName() )
419 && !IPUtils::isValidRange( $userObj->getName() )
422 $this->getOutput()->msg(
'contributions-userdoesnotexist',
424 'mw-userpage-userdoesnotexist'
426 if ( !$this->including() ) {
427 $this->getOutput()->setStatusCode( 404 );
430 $user = htmlspecialchars( $userObj->getName() );
432 $user = $this->getLinkRenderer()->makeLink( $userObj->getUserPage(), $userObj->getName() );
434 $nt = $userObj->getUserPage();
435 $talk = $userObj->getTalkPage();
439 $showForIp = IPUtils::isValid( $userObj ) ||
440 ( IPUtils::isValidRange( $userObj ) && $this->getPager( $userObj )->isQueryableRange( $userObj ) );
443 $registeredAndVisible = $userObj->isRegistered() && ( !$userObj->isHidden()
444 || $this->permissionManager->userHasRight( $this->
getUser(),
'hideuser' ) );
446 if ( $talk && ( $registeredAndVisible || $showForIp ) ) {
447 $tools = self::getUserLinks(
450 $this->permissionManager,
451 $this->getHookRunner()
454 foreach ( $tools as $tool ) {
462 if ( !$this->including() ) {
465 if ( IPUtils::isValidRange( $userObj->getName() ) ) {
466 $block = DatabaseBlock::newFromTarget( $userObj->getName(), $userObj->getName() );
468 $block = DatabaseBlock::newFromTarget( $userObj, $userObj );
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();
477 $out = $this->getOutput();
478 if ( $userObj->isAnon() ) {
479 $msgKey = $block->isSitewide() ?
480 'sp-contributions-blocked-notice-anon' :
481 'sp-contributions-blocked-notice-anon-partial';
483 $msgKey = $block->isSitewide() ?
484 'sp-contributions-blocked-notice' :
485 'sp-contributions-blocked-notice-partial';
488 $class = $block->isSitewide() ?
489 'mw-contributions-blocked-notice' :
490 'mw-contributions-blocked-notice-partial';
498 'showIfEmpty' =>
false,
501 $userObj->getName() # Support GENDER in
'sp-contributions-blocked-notice'
503 'offset' =>
'', # don
't use WebRequest parameter offset
504 'wrap
' => Html::rawElement(
506 [ 'class' => $class ],
515 return Html::rawElement( 'div
', [ 'class' => 'mw-contributions-user-tools
' ],
516 $this->msg( 'contributions-subtitle
' )->rawParams( $user )->params( $userObj->getName() )
531 public static function getUserLinks(
534 PermissionManager $permissionManager = null,
535 HookRunner $hookRunner = null
537 // Fallback to global state, if not provided
538 $permissionManager = $permissionManager ?? MediaWikiServices::getInstance()->getPermissionManager();
539 $hookRunner = $hookRunner ?? Hooks::runner();
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 );
548 $linkRenderer = $sp->getLinkRenderer();
551 # No talk pages for IP ranges.
553 $tools['user-talk
'] = $linkRenderer->makeLink(
555 $sp->msg( 'sp-contributions-talk
' )->text(),
556 [ 'class' => 'mw-contributions-link-talk
' ]
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
' ]
568 $tools['unblock
'] = $linkRenderer->makeKnownLink( # Unblock link
569 SpecialPage::getTitleFor( 'Unblock
', $username ),
570 $sp->msg( 'unblocklink
' )->text(),
571 [ 'class' => 'mw-contributions-link-unblock
' ]
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
' ]
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() ]
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 ]
600 # Don't show some links
for IP ranges
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' ]
613 $tools[
'logs'] = $linkRenderer->makeKnownLink(
615 $sp->msg(
'sp-contributions-logs' )->text(),
616 [
'class' =>
'mw-contributions-link-logs' ]
619 # Add link to deleted user contributions
for privileged users
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' ]
630 # Add a link to change user rights for privileged users
632 $userrightsPage->setContext( $sp->getContext() );
633 if ( $userrightsPage->userCanChangeRights( $target ) ) {
634 $tools[
'userrights'] = $linkRenderer->makeKnownLink(
636 $sp->msg(
'sp-contributions-userrights', $username )->text(),
637 [
'class' =>
'mw-contributions-link-user-rights' ]
641 $hookRunner->onContributionsToolLinks( $id, $userpage, $tools, $sp );
652 protected function getForm( array $pagerOptions ) {
654 $this->getOutput()->addModules( [
655 'mediawiki.special.contributions',
657 $this->getOutput()->addModuleStyles(
'mediawiki.widgets.DateInputWidget.styles' );
658 $this->getOutput()->enableOOUI();
661 # Add hidden params for tracking except for parameters in $skipParameters
679 foreach ( $this->opts as $name => $value ) {
680 if ( in_array( $name, $skipParameters ) ) {
691 $target = $this->opts[
'target'] ??
null;
692 $fields[
'target'] = [
694 'default' => $target ?
695 str_replace(
'_',
' ', $target ) :
'' ,
696 'label' => $this->msg(
'sp-contributions-username' )->text(),
698 'id' =>
'mw-target-user-or-ip',
700 'autofocus' => !$target,
701 'section' =>
'contribs-top',
704 $ns = $this->opts[
'namespace'] ??
null;
705 $fields[
'namespace'] = [
706 'type' =>
'namespaceselect',
707 'label' => $this->msg(
'namespace' )->text(),
708 'name' =>
'namespace',
709 'cssclass' =>
'namespaceselector',
712 'section' =>
'contribs-top',
714 $fields[
'nsFilters'] = [
715 'class' => HTMLMultiSelectField::class,
717 'name' =>
'wpfilters',
720 'hide-if' => [
'===',
'namespace',
'all' ],
721 'options-messages' => [
722 'invert' =>
'nsInvert',
723 'namespace_association' =>
'associated',
725 'section' =>
'contribs-top',
727 $fields[
'tagfilter'] = [
728 'type' =>
'tagfilter',
729 'cssclass' =>
'mw-tagfilter-input',
731 'label-message' => [
'tag-filter',
'parse' ],
732 'name' =>
'tagfilter',
734 'section' =>
'contribs-top',
737 if ( $this->permissionManager->userHasRight( $this->getUser(),
'deletedhistory' ) ) {
738 $fields[
'deletedOnly'] = [
740 'id' =>
'mw-show-deleted-only',
741 'label' => $this->msg(
'history-show-deleted' )->text(),
742 'name' =>
'deletedOnly',
743 'section' =>
'contribs-top',
747 $fields[
'topOnly'] = [
749 'id' =>
'mw-show-top-only',
750 'label' => $this->msg(
'sp-contributions-toponly' )->text(),
752 'section' =>
'contribs-top',
754 $fields[
'newOnly'] = [
756 'id' =>
'mw-show-new-only',
757 'label' => $this->msg(
'sp-contributions-newonly' )->text(),
759 'section' =>
'contribs-top',
761 $fields[
'hideMinor'] = [
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',
772 $this->getHookRunner()->onSpecialContributions__getForm__filters(
773 $this, $rawFilters );
774 foreach ( $rawFilters as $filter ) {
776 if ( is_string( $filter ) ) {
779 'default' => $filter,
781 'section' =>
'contribs-top',
784 'A SpecialContributions::getForm::filters hook handler returned ' .
785 'an array of strings, this is deprecated since MediaWiki 1.33',
797 'id' =>
'mw-date-start',
798 'label' => $this->msg(
'date-range-from' )->text(),
800 'section' =>
'contribs-date',
805 'id' =>
'mw-date-end',
806 'label' => $this->msg(
'date-range-to' )->text(),
808 'section' =>
'contribs-date',
814 ->setTitle( $this->getPageTitle() )
818 ->setCollapsibleOptions(
819 ( $pagerOptions[
'target'] ??
null ) ||
820 ( $pagerOptions[
'start'] ??
null ) ||
821 ( $pagerOptions[
'end'] ??
null )
824 ->setSubmitTextMsg(
'sp-contributions-submit' )
825 ->setWrapperLegendMsg(
'sp-contributions-search' );
827 $explain = $this->msg(
'sp-contributions-explain' );
828 if ( !$explain->isBlank() ) {
829 $htmlForm->addFooterText(
"<p id='mw-sp-contributions-explain'>{$explain->parse()}</p>" );
832 $htmlForm->prepareForm();
834 return $htmlForm->getHTML(
false );
846 $search = $this->userNameUtils->getCanonical( $search );
852 return $this->userNamePrefixSearch
853 ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset );
861 if ( $this->pager ===
null ) {
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'],
878 $this->getLinkRenderer(),
879 $this->linkBatchFactory,
880 $this->getHookContainer(),
882 $this->actorMigration,
883 $this->revisionStore,
884 $this->namespaceInfo,
886 $this->commentFormatter
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,...
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
This is not intended to be a long-term part of MediaWiki; it will be deprecated and removed once acto...
static isExternal( $username)
Tells whether the username is external or not.
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
static warningBox( $html, $className='')
Return a warning box.
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing '/'...
static errorBox( $html, $heading='', $className='')
Return an error box.
static closeElement( $element)
Returns "</$element>".
Shortcut to construct an includable special page.
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
A class containing constants representing the names of configuration variables.
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
NamespaceInfo $namespaceInfo
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
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 makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Special page to allow managing user group membership.