37 parent::__construct(
'Contributions' );
45 $out->addModuleStyles( [
46 'jquery.makeCollapsible.styles',
47 'mediawiki.interface.helpers.styles',
49 'mediawiki.special.changeslist',
52 'mediawiki.special.recentchanges',
54 'mediawiki.page.ready'
61 $target = $par ?? $request->getVal(
'target' );
63 $this->opts[
'deletedOnly'] = $request->getBool(
'deletedOnly' );
65 if ( !strlen( $target ) ) {
67 $out->addHTML( $this->
getForm( $this->opts ) );
75 $this->opts[
'limit'] = $request->getInt(
'limit', $user->getOption(
'rclimit' ) );
76 $this->opts[
'target'] = $target;
77 $this->opts[
'topOnly'] = $request->getBool(
'topOnly' );
78 $this->opts[
'newOnly'] = $request->getBool(
'newOnly' );
79 $this->opts[
'hideMinor'] = $request->getBool(
'hideMinor' );
82 if ( ExternalUserNames::isExternal( $target ) ) {
85 $out->addHTML( $this->
getForm( $this->opts ) );
90 $out->setHTMLTitle( $this->
msg(
92 $this->
msg(
'contributions-title', $target )->plain()
93 )->inContentLanguage() );
95 $nt = Title::makeTitleSafe(
NS_USER, $target );
97 $out->addHTML( $this->
getForm( $this->opts ) );
102 $out->addHTML( $this->
getForm( $this->opts ) );
105 $id = $userObj->getId();
107 $target = $nt->getText();
109 $out->setHTMLTitle( $this->
msg(
111 $this->
msg(
'contributions-title', $target )->plain()
112 )->inContentLanguage() );
114 # For IP ranges, we want the contributionsSub, but not the skin-dependent
115 # links under 'Tools', which may include irrelevant links like 'Logs'.
116 if ( !IPUtils::isValidRange( $target ) &&
117 (
User::isIP( $target ) || $userObj->isRegistered() )
125 $this->
getSkin()->setRelevantUser( $userObj );
129 $ns = $request->getVal(
'namespace',
null );
130 if ( $ns !==
null && $ns !==
'' && $ns !==
'all' ) {
131 $this->opts[
'namespace'] = intval( $ns );
133 $this->opts[
'namespace'] =
'';
139 $this->opts[
'associated'] = $request->getBool(
'associated' );
140 $this->opts[
'nsInvert'] = (bool)$request->getVal(
'nsInvert' );
141 $nsFilters = $request->getArray(
'wpfilters',
null );
142 if ( $nsFilters !==
null ) {
143 $this->opts[
'associated'] = in_array(
'associated', $nsFilters );
144 $this->opts[
'nsInvert'] = in_array(
'nsInvert', $nsFilters );
147 $this->opts[
'tagfilter'] = (string)$request->getVal(
'tagfilter' );
151 if ( MediaWikiServices::getInstance()
152 ->getPermissionManager()
153 ->userHasRight( $user,
'markbotedits' ) && $request->getBool(
'bot' )
155 $this->opts[
'bot'] =
'1';
158 $skip = $request->getText(
'offset' ) || $request->getText(
'dir' ) ==
'prev';
159 # Offset overrides year/month selection
161 $this->opts[
'year'] = $request->getVal(
'year' );
162 $this->opts[
'month'] = $request->getVal(
'month' );
164 $this->opts[
'start'] = $request->getVal(
'start' );
165 $this->opts[
'end'] = $request->getVal(
'end' );
167 $this->opts = ContribsPager::processDateFilter( $this->opts );
169 if ( $this->opts[
'namespace'] !==
'' && $this->opts[
'namespace'] <
NS_MAIN ) {
171 "<div class=\"mw-negative-namespace-not-supported error\">\n\$1\n</div>",
172 [
'negative-namespace-not-supported' ]
174 $out->addHTML( $this->
getForm( $this->opts ) );
178 $feedType = $request->getVal(
'feed' );
181 'action' =>
'feedcontributions',
184 if ( $this->opts[
'topOnly'] ) {
185 $feedParams[
'toponly'] =
true;
187 if ( $this->opts[
'newOnly'] ) {
188 $feedParams[
'newonly'] =
true;
190 if ( $this->opts[
'hideMinor'] ) {
191 $feedParams[
'hideminor'] =
true;
193 if ( $this->opts[
'deletedOnly'] ) {
194 $feedParams[
'deletedonly'] =
true;
196 if ( $this->opts[
'tagfilter'] !==
'' ) {
197 $feedParams[
'tagfilter'] = $this->opts[
'tagfilter'];
199 if ( $this->opts[
'namespace'] !==
'' ) {
200 $feedParams[
'namespace'] = $this->opts[
'namespace'];
204 if ( $feedType && $this->opts[
'year'] !==
null ) {
205 $feedParams[
'year'] = $this->opts[
'year'];
207 if ( $feedType && $this->opts[
'month'] !==
null ) {
208 $feedParams[
'month'] = $this->opts[
'month'];
214 $feedParams[
'feedformat'] = $feedType;
217 $out->redirect( $url,
'301' );
225 if ( $this->
getHookRunner()->onSpecialContributionsBeforeMainOutput(
226 $id, $userObj, $this )
230 'namespace' => $this->opts[
'namespace'],
231 'tagfilter' => $this->opts[
'tagfilter'],
232 'start' => $this->opts[
'start'],
233 'end' => $this->opts[
'end'],
234 'deletedOnly' => $this->opts[
'deletedOnly'],
235 'topOnly' => $this->opts[
'topOnly'],
236 'newOnly' => $this->opts[
'newOnly'],
237 'hideMinor' => $this->opts[
'hideMinor'],
238 'nsInvert' => $this->opts[
'nsInvert'],
239 'associated' => $this->opts[
'associated'],
242 $out->addHTML( $this->
getForm( $this->opts ) );
245 if ( IPUtils::isValidRange( $target ) && !$pager->isQueryableRange( $target ) ) {
247 $limits = $this->
getConfig()->get(
'RangeContributionsCIDRLimit' );
248 $limit = $limits[ IPUtils::isIPv4( $target ) ?
'IPv4' :
'IPv6' ];
249 $out->addWikiMsg(
'sp-contributions-outofrange', $limit );
253 $poolKey = WikiMap::getCurrentWikiDbDomain() .
':SpecialContributions:';
254 if ( $this->
getUser()->isAnon() ) {
255 $poolKey .=
'a:' . $this->
getUser()->getName();
257 $poolKey .=
'u:' . $this->
getUser()->getId();
260 'doWork' =>
function () use ( $pager, $out, $target ) {
261 if ( !$pager->getNumRows() ) {
262 $out->addWikiMsg(
'nocontribs', $target );
264 # Show a message about replica DB lag, if applicable
265 $lag = $pager->getDatabase()->getSessionLagStatus()[
'lag'];
267 $out->showLagWarning( $lag );
270 $output = $pager->getBody();
272 $output = $pager->getNavigationBar() .
274 $pager->getNavigationBar();
276 $out->addHTML( $output );
279 'error' =>
function () use ( $out ) {
280 $msg = $this->
getUser()->isAnon()
281 ?
'sp-contributions-concurrency-ip'
282 :
'sp-contributions-concurrency-user';
283 $out->wrapWikiMsg(
"<div class='errorbox'>\n$1\n</div>", $msg );
289 $out->preventClickjacking( $pager->getPreventClickjacking() );
291 # Show the appropriate "footer" message - WHOIS tools, etc.
292 if ( IPUtils::isValidRange( $target ) ) {
293 $message =
'sp-contributions-footer-anon-range';
294 } elseif ( IPUtils::isIPAddress( $target ) ) {
295 $message =
'sp-contributions-footer-anon';
296 } elseif ( $userObj->isAnon() ) {
299 } elseif ( $userObj->isHidden() &&
300 !MediaWikiServices::getInstance()->getPermissionManager()
301 ->userHasRight( $this->getUser(),
'hideuser' )
308 $message =
'sp-contributions-footer';
311 if ( $message && !$this->
including() && !$this->
msg( $message, $target )->isDisabled() ) {
313 "<div class='mw-contributions-footer'>\n$1\n</div>",
314 [ $message, $target ] );
327 $isAnon = $userObj->isAnon();
328 if ( !$isAnon && $userObj->isHidden() &&
329 !MediaWikiServices::getInstance()->getPermissionManager()
330 ->userHasRight( $this->getUser(),
'hideuser' )
342 if ( !
User::isIP( $userObj->getName() ) && !$userObj->isIPRange() ) {
343 $this->getOutput()->wrapWikiMsg(
344 "<div class=\"mw-userpage-userdoesnotexist error\">\n\$1\n</div>",
346 'contributions-userdoesnotexist',
350 if ( !$this->including() ) {
351 $this->getOutput()->setStatusCode( 404 );
354 $user = htmlspecialchars( $userObj->getName() );
356 $user = $this->getLinkRenderer()->makeLink( $userObj->getUserPage(), $userObj->getName() );
358 $nt = $userObj->getUserPage();
359 $talk = $userObj->getTalkPage();
362 $tools = self::getUserLinks( $this, $userObj );
363 $links = Html::openElement(
'span', [
'class' =>
'mw-changeslist-links' ] );
364 foreach ( $tools as $tool ) {
365 $links .= Html::rawElement(
'span', [], $tool ) .
' ';
367 $links = trim( $links ) . Html::closeElement(
'span' );
372 if ( !$this->including() ) {
375 if ( $userObj->isIPRange() ) {
376 $block = DatabaseBlock::newFromTarget( $userObj->getName(), $userObj->getName() );
378 $block = DatabaseBlock::newFromTarget( $userObj, $userObj );
381 if ( $block !==
null && $block->getType() != DatabaseBlock::TYPE_AUTO ) {
382 if ( $block->getType() == DatabaseBlock::TYPE_RANGE ) {
383 $nt = MediaWikiServices::getInstance()->getNamespaceInfo()->
384 getCanonicalName(
NS_USER ) .
':' . $block->getTarget();
387 $out = $this->getOutput();
388 if ( $userObj->isAnon() ) {
389 $msgKey = $block->isSitewide() ?
390 'sp-contributions-blocked-notice-anon' :
391 'sp-contributions-blocked-notice-anon-partial';
393 $msgKey = $block->isSitewide() ?
394 'sp-contributions-blocked-notice' :
395 'sp-contributions-blocked-notice-partial';
398 $class = $block->isSitewide() ?
399 'mw-contributions-blocked-notice' :
400 'mw-contributions-blocked-notice-partial';
401 LogEventsList::showLogExtract(
408 'showIfEmpty' =>
false,
411 $userObj->getName() # Support GENDER in
'sp-contributions-blocked-notice'
413 'offset' =>
'', # don
't use WebRequest parameter offset
414 'wrap
' => Html::rawElement(
416 [ 'class' => $class ],
425 return Html::rawElement( 'div
', [ 'class' => 'mw-contributions-user-tools
' ],
426 $this->msg( 'contributions-subtitle
' )->rawParams( $user )->params( $userObj->getName() )
439 public static function getUserLinks( SpecialPage $sp, User $target ) {
440 $id = $target->getId();
441 $username = $target->getName();
442 $userpage = $target->getUserPage();
443 $talkpage = $target->getTalkPage();
444 $isIP = IPUtils::isValid( $username );
445 $isRange = IPUtils::isValidRange( $username );
447 $linkRenderer = $sp->getLinkRenderer();
450 # No talk pages for IP ranges.
452 $tools['user-talk
'] = $linkRenderer->makeLink(
454 $sp->msg( 'sp-contributions-talk
' )->text()
458 # Block / Change block / Unblock links
459 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
460 if ( $permissionManager->userHasRight( $sp->getUser(), 'block
' ) ) {
461 if ( $target->getBlock() && $target->getBlock()->getType() != DatabaseBlock::TYPE_AUTO ) {
462 $tools['block
'] = $linkRenderer->makeKnownLink( # Change block link
463 SpecialPage::getTitleFor( 'Block
', $username ),
464 $sp->msg( 'change-blocklink
' )->text()
466 $tools['unblock
'] = $linkRenderer->makeKnownLink( # Unblock link
467 SpecialPage::getTitleFor( 'Unblock
', $username ),
468 $sp->msg( 'unblocklink
' )->text()
470 } else { # User is not blocked
471 $tools['block
'] = $linkRenderer->makeKnownLink( # Block link
472 SpecialPage::getTitleFor( 'Block
', $username ),
473 $sp->msg( 'blocklink
' )->text()
479 $tools['log-block
'] = $linkRenderer->makeKnownLink(
480 SpecialPage::getTitleFor( 'Log
', 'block
' ),
481 $sp->msg( 'sp-contributions-blocklog
' )->text(),
483 [ 'page
' => $userpage->getPrefixedText() ]
486 # Suppression log link (T61120)
487 if ( $permissionManager->userHasRight( $sp->getUser(), 'suppressionlog
' ) ) {
488 $tools['log-suppression
'] = $linkRenderer->makeKnownLink(
489 SpecialPage::getTitleFor( 'Log
', 'suppress
' ),
490 $sp->msg( 'sp-contributions-suppresslog
', $username )->text(),
492 [ 'offender
' => $username ]
496 # Don't show some links
for IP ranges
498 # Uploads: hide if IPs cannot upload (T220674)
499 if ( !$isIP || $permissionManager->userHasRight( $target,
'upload' ) ) {
500 $tools[
'uploads'] = $linkRenderer->makeKnownLink(
501 SpecialPage::getTitleFor(
'Listfiles', $username ),
502 $sp->msg(
'sp-contributions-uploads' )->text()
508 $tools[
'logs'] = $linkRenderer->makeKnownLink(
510 $sp->msg(
'sp-contributions-logs' )->text()
513 # Add link to deleted user contributions for priviledged users
515 if ( $permissionManager->userHasRight( $sp->getUser(),
'deletedhistory' ) ) {
516 $tools[
'deletedcontribs'] = $linkRenderer->makeKnownLink(
518 $sp->msg(
'sp-contributions-deleted', $username )->text()
523 # Add a link to change user rights for privileged users
525 $userrightsPage->setContext( $sp->getContext() );
526 if ( $userrightsPage->userCanChangeRights( $target ) ) {
527 $tools[
'userrights'] = $linkRenderer->makeKnownLink(
529 $sp->msg(
'sp-contributions-userrights', $username )->text()
533 Hooks::runner()->onContributionsToolLinks( $id, $userpage, $tools, $sp );
544 protected function getForm( array $pagerOptions ) {
545 $this->opts[
'title'] = $this->getPageTitle()->getPrefixedText();
547 $this->getOutput()->addModules( [
548 'mediawiki.special.contributions',
550 $this->getOutput()->addModuleStyles(
'mediawiki.widgets.DateInputWidget.styles' );
551 $this->getOutput()->enableOOUI();
554 # Add hidden params for tracking except for parameters in $skipParameters
571 foreach ( $this->opts as $name => $value ) {
572 if ( in_array( $name, $skipParameters ) ) {
583 $target = $this->opts[
'target'] ??
null;
584 $fields[
'target'] = [
586 'default' => $target ?
587 str_replace(
'_',
' ', $target ) :
'' ,
588 'label' => $this->msg(
'sp-contributions-username' )->text(),
590 'id' =>
'mw-target-user-or-ip',
592 'autofocus' => !$target,
593 'section' =>
'contribs-top',
596 $ns = $this->opts[
'namespace'] ??
null;
597 $fields[
'namespace'] = [
598 'type' =>
'namespaceselect',
599 'label' => $this->msg(
'namespace' )->text(),
600 'name' =>
'namespace',
601 'cssclass' =>
'namespaceselector',
604 'section' =>
'contribs-top',
606 $request = $this->getRequest();
607 $nsFilters = $request->getArray(
'wpfilters' );
608 $fields[
'nsFilters'] = [
609 'class' =>
'HTMLMultiSelectField',
611 'name' =>
'wpfilters',
614 'cssclass' => $ns ===
'' ?
615 'contribs-ns-filters mw-input-with-label mw-input-hidden' :
616 'contribs-ns-filters mw-input-with-label',
620 'options-messages' => [
621 'invert' =>
'nsInvert',
622 'namespace_association' =>
'associated',
624 'default' => $nsFilters,
625 'section' =>
'contribs-top',
627 $fields[
'tagfilter'] = [
628 'type' =>
'tagfilter',
629 'cssclass' =>
'mw-tagfilter-input',
631 'label-message' => [
'tag-filter',
'parse' ],
632 'name' =>
'tagfilter',
634 'section' =>
'contribs-top',
637 if ( MediaWikiServices::getInstance()
639 ->userHasRight( $this->
getUser(),
'deletedhistory' )
641 $fields[
'deletedOnly'] = [
643 'id' =>
'mw-show-deleted-only',
644 'label' => $this->msg(
'history-show-deleted' )->text(),
645 'name' =>
'deletedOnly',
646 'section' =>
'contribs-top',
650 $fields[
'topOnly'] = [
652 'id' =>
'mw-show-top-only',
653 'label' => $this->msg(
'sp-contributions-toponly' )->text(),
655 'section' =>
'contribs-top',
657 $fields[
'newOnly'] = [
659 'id' =>
'mw-show-new-only',
660 'label' => $this->msg(
'sp-contributions-newonly' )->text(),
662 'section' =>
'contribs-top',
664 $fields[
'hideMinor'] = [
666 'cssclass' =>
'mw-hide-minor-edits',
667 'id' =>
'mw-show-new-only',
668 'label' => $this->msg(
'sp-contributions-hideminor' )->text(),
669 'name' =>
'hideMinor',
670 'section' =>
'contribs-top',
675 $this->getHookRunner()->onSpecialContributions__getForm__filters(
676 $this, $rawFilters );
677 foreach ( $rawFilters as $filter ) {
679 if ( is_string( $filter ) ) {
682 'default' => $filter,
684 'section' =>
'contribs-top',
687 'A SpecialContributions::getForm::filters hook handler returned ' .
688 'an array of strings, this is deprecated since MediaWiki 1.33',
700 'id' =>
'mw-date-start',
701 'label' => $this->msg(
'date-range-from' )->text(),
703 'section' =>
'contribs-date',
708 'id' =>
'mw-date-end',
709 'label' => $this->msg(
'date-range-to' )->text(),
711 'section' =>
'contribs-date',
714 $htmlForm = HTMLForm::factory(
'ooui', $fields, $this->
getContext() );
720 ->setCollapsibleOptions(
721 ( $pagerOptions[
'target'] ??
null ) ||
722 ( $pagerOptions[
'start'] ??
null ) ||
723 ( $pagerOptions[
'end'] ??
null )
726 ->setSubmitText( $this->msg(
'sp-contributions-submit' )->text() )
727 ->setWrapperLegend( $this->msg(
'sp-contributions-search' )->text() );
729 $explain = $this->msg(
'sp-contributions-explain' );
730 if ( !$explain->isBlank() ) {
731 $htmlForm->addFooterText(
"<p id='mw-sp-contributions-explain'>{$explain->parse()}</p>" );
734 $htmlForm->loadData();
736 return $htmlForm->getHTML(
false );
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 $function is deprecated.
if(ini_get('mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Pre-librarized class name for IPUtils.
Shortcut to construct an includable special page.
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.
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.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
contributionsSub( $userObj)
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.
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 search( $audience, $search, $limit, $offset=0)
Do a prefix search of user names and return a list of matching user names.
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
static isIP( $name)
Does the string match an anonymous IP address?
Special page to allow managing user group membership.