110 parent::__construct( $name, $restriction );
130 $out->addModuleStyles( [
131 'jquery.makeCollapsible.styles',
132 'mediawiki.interface.helpers.styles',
134 'mediawiki.special.changeslist',
136 $out->addBodyClasses(
'mw-special-ContributionsSpecialPage' );
139 'mediawiki.page.ready'
145 $target = $par ?? $request->getVal(
'target',
'' );
146 '@phan-var string $target';
148 $this->opts[
'deletedOnly'] = $request->getBool(
'deletedOnly' );
150 if ( !strlen( $target ) ) {
151 $out->addHTML( $this->
getForm( $this->opts ) );
158 $this->opts[
'limit'] = $request->getInt(
'limit', $this->userOptionsLookup->getIntOption( $user,
'rclimit' ) );
159 $this->opts[
'target'] = $target;
160 $this->opts[
'topOnly'] = $request->getBool(
'topOnly' );
161 $this->opts[
'newOnly'] = $request->getBool(
'newOnly' );
162 $this->opts[
'hideMinor'] = $request->getBool(
'hideMinor' );
164 $ns = $request->getVal(
'namespace',
null );
165 if ( $ns !==
null && $ns !==
'' && $ns !==
'all' ) {
166 $this->opts[
'namespace'] = intval( $ns );
168 $this->opts[
'namespace'] =
'';
174 $this->opts[
'associated'] = $request->getBool(
'associated' );
175 $this->opts[
'nsInvert'] = (bool)$request->getVal(
'nsInvert' );
176 $nsFilters = $request->getArray(
'wpfilters',
null );
177 if ( $nsFilters !==
null ) {
178 $this->opts[
'associated'] = in_array(
'associated', $nsFilters );
179 $this->opts[
'nsInvert'] = in_array(
'nsInvert', $nsFilters );
182 $this->opts[
'tagfilter'] = array_filter( explode(
184 (
string)$request->getVal(
'tagfilter' )
185 ),
static function ( $el ) {
188 $this->opts[
'tagInvert'] = $request->getBool(
'tagInvert' );
192 if ( $this->permissionManager->userHasRight( $user,
'markbotedits' ) && $request->getBool(
'bot' ) ) {
193 $this->opts[
'bot'] =
'1';
196 $this->opts[
'year'] = $request->getIntOrNull(
'year' );
197 $this->opts[
'month'] = $request->getIntOrNull(
'month' );
198 $this->opts[
'start'] = $request->getVal(
'start' );
199 $this->opts[
'end'] = $request->getVal(
'end' );
201 $notExternal = !ExternalUserNames::isExternal( $target );
202 if ( $notExternal ) {
203 $nt = Title::makeTitleSafe(
NS_USER, $target );
205 $out->addHTML( $this->
getForm( $this->opts ) );
208 $target = $nt->getText();
209 if ( IPUtils::isValidRange( $target ) ) {
210 $target = IPUtils::sanitizeRange( $target );
214 $userObj = $this->userFactory->newFromName( $target, UserRigorOptions::RIGOR_NONE );
216 $out->addHTML( $this->
getForm( $this->opts ) );
222 # For IP ranges, we want the contributionsSub, but not the skin-dependent
223 # links under 'Tools', which may include irrelevant links like 'Logs'.
224 if ( $notExternal && !IPUtils::isValidRange( $target ) &&
225 ( $this->userNameUtils->isIP( $target ) || $userObj->isRegistered() )
233 $this->
getSkin()->setRelevantUser( $userObj );
236 $this->opts = ContribsPager::processDateFilter( $this->opts );
238 if ( $this->opts[
'namespace'] !==
'' && $this->opts[
'namespace'] <
NS_MAIN ) {
240 "<div class=\"mw-negative-namespace-not-supported error\">\n\$1\n</div>",
241 [
'negative-namespace-not-supported' ]
243 $out->addHTML( $this->
getForm( $this->opts ) );
248 $feedType = $request->getVal(
'feed' );
251 'action' =>
'feedcontributions',
254 if ( $this->opts[
'topOnly'] ) {
255 $feedParams[
'toponly'] =
true;
257 if ( $this->opts[
'newOnly'] ) {
258 $feedParams[
'newonly'] =
true;
260 if ( $this->opts[
'hideMinor'] ) {
261 $feedParams[
'hideminor'] =
true;
263 if ( $this->opts[
'deletedOnly'] ) {
264 $feedParams[
'deletedonly'] =
true;
267 if ( $this->opts[
'tagfilter'] !== [] ) {
268 $feedParams[
'tagfilter'] = $this->opts[
'tagfilter'];
270 if ( $this->opts[
'namespace'] !==
'' ) {
271 $feedParams[
'namespace'] = $this->opts[
'namespace'];
275 if ( $feedType && isset( $this->opts[
'year'] ) ) {
276 $feedParams[
'year'] = $this->opts[
'year'];
278 if ( $feedType && isset( $this->opts[
'month'] ) ) {
279 $feedParams[
'month'] = $this->opts[
'month'];
285 $feedParams[
'feedformat'] = $feedType;
288 $out->redirect(
$url,
'301' );
297 if ( $this->
getHookRunner()->onSpecialContributionsBeforeMainOutput(
298 $notExternal ? $userObj->getId() : 0, $userObj, $this )
300 $out->addHTML( $this->
getForm( $this->opts ) );
301 if ( $this->formErrors ) {
306 $userIdentity = $notExternal ? $userObj :
307 $this->userIdentityLookup->getUserIdentityByName( $target ) ?? $userObj;
308 $pager = $this->
getPager( $userIdentity );
309 if ( IPUtils::isValidRange( $target ) &&
310 !ContribsPager::isQueryableRange( $target, $this->
getConfig() )
314 $limit = $limits[ IPUtils::isIPv4( $target ) ?
'IPv4' :
'IPv6' ];
315 $out->addWikiMsg(
'sp-contributions-outofrange', $limit );
319 $poolKey = $this->dbProvider->getReplicaDatabase()->getDomainID() .
':Special' . $this->mName .
':';
320 if ( $this->
getUser()->isAnon() ) {
321 $poolKey .=
'a:' . $this->
getUser()->getName();
323 $poolKey .=
'u:' . $this->
getUser()->getId();
326 'doWork' =>
function () use ( $pager, $out, $target ) {
327 if ( !$pager->getNumRows() ) {
328 $out->addWikiMsg(
'nocontribs', $target );
330 # Show a message about replica DB lag, if applicable
331 $lag = $pager->getDatabase()->getSessionLagStatus()[
'lag'];
333 $out->showLagWarning( $lag );
336 $output = $pager->getBody();
338 $output = $pager->getNavigationBar() .
340 $pager->getNavigationBar();
342 $out->addHTML( $output );
345 'error' =>
function () use ( $out ) {
346 $msg = $this->
getUser()->isAnon()
347 ?
'sp-contributions-concurrency-ip'
348 :
'sp-contributions-concurrency-user';
351 $out->msg( $msg )->parse()
359 $out->setPreventClickjacking( $pager->getPreventClickjacking() );
361 # Show the appropriate "footer" message - WHOIS tools, etc.
362 if ( IPUtils::isValidRange( $target ) &&
363 ContribsPager::isQueryableRange( $target, $this->
getConfig() )
365 $message =
'sp-contributions-footer-anon-range';
366 } elseif ( IPUtils::isIPAddress( $target ) ) {
367 $message =
'sp-contributions-footer-anon';
368 } elseif ( $userObj->isAnon() ) {
371 } elseif ( $userObj->isHidden() &&
372 !$this->permissionManager->userHasRight( $this->getUser(),
'hideuser' )
379 $message =
'sp-contributions-footer';
382 if ( $message && !$this->
including() && !$this->
msg( $message, $target )->isDisabled() ) {
384 "<div class='mw-contributions-footer'>\n$1\n</div>",
385 [ $message, $target ] );
400 $isAnon = $userObj->isAnon();
401 if ( !$isAnon && $userObj->isHidden() &&
402 !$this->permissionManager->userHasRight( $this->getUser(),
'hideuser' )
414 if ( !$this->userNameUtils->isIP( $userObj->getName() )
415 && !IPUtils::isValidRange( $userObj->getName() )
417 $this->getOutput()->addHTML( Html::warningBox(
418 $this->getOutput()->msg(
'contributions-userdoesnotexist',
420 'mw-userpage-userdoesnotexist'
422 if ( !$this->including() ) {
423 $this->getOutput()->setStatusCode( 404 );
426 $user = htmlspecialchars( $userObj->getName() );
428 $user = $this->getLinkRenderer()->makeLink( $userObj->getUserPage(), $userObj->getName() );
430 $nt = $userObj->getUserPage();
431 $talk = $userObj->getTalkPage();
435 $showForIp = IPUtils::isValid( $userObj ) ||
436 ( IPUtils::isValidRange( $userObj ) && ContribsPager::isQueryableRange( $userObj, $this->getConfig() ) );
439 $registeredAndVisible = $userObj->isRegistered() && ( !$userObj->isHidden()
440 || $this->permissionManager->userHasRight( $this->
getUser(),
'hideuser' ) );
442 $shouldShowLinks = $talk && ( $registeredAndVisible || $showForIp );
443 if ( $shouldShowLinks ) {
444 $tools = self::getUserLinks(
447 $this->permissionManager,
448 $this->getHookRunner()
450 $links = Html::openElement(
'span', [
'class' =>
'mw-changeslist-links' ] );
451 foreach ( $tools as $tool ) {
452 $links .= Html::rawElement(
'span', [], $tool ) .
' ';
454 $links = trim( $links ) . Html::closeElement(
'span' );
459 $shouldShowBlocks = !$this->including();
460 if ( $shouldShowBlocks ) {
463 if ( IPUtils::isValidRange( $userObj->getName() ) ) {
464 $block = $this->blockStore
465 ->newFromTarget( $userObj->getName(), $userObj->getName() );
467 $block = $this->blockStore->newFromTarget( $userObj, $userObj );
470 if ( $block !==
null && $block->getType() != Block::TYPE_AUTO ) {
471 if ( $block->getType() == Block::TYPE_RANGE ) {
472 $nt = $this->namespaceInfo->getCanonicalName(
NS_USER )
473 .
':' . $block->getTargetName();
476 $out = $this->getOutput();
477 if ( $userObj->isAnon() ) {
478 $msgKey = $block->isSitewide() ?
479 'sp-contributions-blocked-notice-anon' :
480 'sp-contributions-blocked-notice-anon-partial';
482 $msgKey = $block->isSitewide() ?
483 'sp-contributions-blocked-notice' :
484 'sp-contributions-blocked-notice-partial';
487 $class = $block->isSitewide() ?
488 'mw-contributions-blocked-notice' :
489 'mw-contributions-blocked-notice-partial';
490 LogEventsList::showLogExtract(
497 'showIfEmpty' =>
false,
500 $userObj->getName() # Support GENDER in
'sp-contributions-blocked-notice'
502 'offset' =>
'', # don
't use WebRequest parameter offset
503 'wrap
' => Html::rawElement(
505 [ 'class' => $class ],
514 // First subheading. "For Username (talk | block log | logs | etc.)"
515 $userName = $userObj->getName();
516 $subHeadingsHtml = Html::rawElement( 'div
', [ 'class' => 'mw-contributions-user-tools
' ],
517 $this->msg( 'contributions-subtitle
' )->rawParams( $user )->params( $userName )
521 // Second subheading. "A user with 37,208 edits. Account created on 2008-09-17."
522 if ( $talk && $registeredAndVisible ) {
523 $editCount = $userObj->getEditCount();
524 $userInfo = $this->msg( 'contributions-edit-count
' )
525 ->params( $userName )
526 ->numParams( $editCount )
529 $accountCreationDate = $userObj->getRegistration();
530 if ( $accountCreationDate ) {
531 $date = $this->getLanguage()->date( $accountCreationDate, true );
532 $userInfo .= $this->msg( 'word-separator
' )
534 $userInfo .= $this->msg( 'contributions-account-creation-date
' )
535 ->plaintextParams( $date )
539 $subHeadingsHtml .= Html::rawElement(
541 [ 'class' => 'mw-contributions-editor-info
' ],
546 return $subHeadingsHtml;
559 public static function getUserLinks(
562 PermissionManager $permissionManager = null,
563 HookRunner $hookRunner = null
565 // Fallback to global state, if not provided
566 $permissionManager ??= MediaWikiServices::getInstance()->getPermissionManager();
567 $hookRunner ??= new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
569 $id = $target->getId();
570 $username = $target->getName();
571 $userpage = $target->getUserPage();
572 $talkpage = $target->getTalkPage();
573 $isIP = IPUtils::isValid( $username );
574 $isRange = IPUtils::isValidRange( $username );
576 $linkRenderer = $sp->getLinkRenderer();
579 # No talk pages for IP ranges.
581 $tools['user-talk
'] = $linkRenderer->makeLink(
583 $sp->msg( 'sp-contributions-talk
' )->text(),
584 [ 'class' => 'mw-contributions-link-talk
' ]
588 # Block / Change block / Unblock links
589 if ( $permissionManager->userHasRight( $sp->getUser(), 'block
' ) ) {
590 if ( $target->getBlock() && $target->getBlock()->getType() != Block::TYPE_AUTO ) {
591 $tools['block
'] = $linkRenderer->makeKnownLink( # Change block link
592 SpecialPage::getTitleFor( 'Block', $username ),
593 $sp->msg( 'change-blocklink
' )->text(),
594 [ 'class' => 'mw-contributions-link-change-block
' ]
596 $tools['unblock
'] = $linkRenderer->makeKnownLink( # Unblock link
597 SpecialPage::getTitleFor( 'Unblock
', $username ),
598 $sp->msg( 'unblocklink
' )->text(),
599 [ 'class' => 'mw-contributions-link-unblock
' ]
601 } else { # User is not blocked
602 $tools['block
'] = $linkRenderer->makeKnownLink( # Block link
603 SpecialPage::getTitleFor( 'Block', $username ),
604 $sp->msg( 'blocklink
' )->text(),
605 [ 'class' => 'mw-contributions-link-block
' ]
611 $tools['log-block
'] = $linkRenderer->makeKnownLink(
612 SpecialPage::getTitleFor( 'Log
', 'block
' ),
613 $sp->msg( 'sp-contributions-blocklog
' )->text(),
614 [ 'class' => 'mw-contributions-link-block-log
' ],
615 [ 'page
' => $userpage->getPrefixedText() ]
618 # Suppression log link (T61120)
619 if ( $permissionManager->userHasRight( $sp->getUser(), 'suppressionlog
' ) ) {
620 $tools['log-suppression
'] = $linkRenderer->makeKnownLink(
621 SpecialPage::getTitleFor( 'Log
', 'suppress
' ),
622 $sp->msg( 'sp-contributions-suppresslog
', $username )->text(),
623 [ 'class' => 'mw-contributions-link-suppress-log
' ],
624 [ 'offender
' => $username ]
628 # Don't show some links
for IP ranges
630 # Uploads: hide if IPs cannot upload (T220674)
631 if ( !$isIP || $permissionManager->userHasRight( $target,
'upload' ) ) {
632 $tools[
'uploads'] = $linkRenderer->makeKnownLink(
633 SpecialPage::getTitleFor(
'Listfiles', $username ),
634 $sp->msg(
'sp-contributions-uploads' )->text(),
635 [
'class' =>
'mw-contributions-link-uploads' ]
641 $tools[
'logs'] = $linkRenderer->makeKnownLink(
643 $sp->msg(
'sp-contributions-logs' )->text(),
644 [
'class' =>
'mw-contributions-link-logs' ]
647 # Add link to deleted user contributions for privileged users
649 if ( $permissionManager->userHasRight( $sp->getUser(),
'deletedhistory' ) ) {
650 $tools[
'deletedcontribs'] = $linkRenderer->makeKnownLink(
652 $sp->msg(
'sp-contributions-deleted', $username )->text(),
653 [
'class' =>
'mw-contributions-link-deleted-contribs' ]
658 # Add a link to change user rights for privileged users
660 $userrightsPage->setContext( $sp->getContext() );
661 if ( $userrightsPage->userCanChangeRights( $target ) ) {
662 $tools[
'userrights'] = $linkRenderer->makeKnownLink(
664 $sp->msg(
'sp-contributions-userrights', $username )->text(),
665 [
'class' =>
'mw-contributions-link-user-rights' ]
669 # Add a link to rename the user
670 if ( $id && $permissionManager->userHasRight( $sp->getUser(),
'renameuser' ) && !$target->isTemp() ) {
671 $tools[
'renameuser'] = $sp->getLinkRenderer()->makeKnownLink(
673 $sp->msg(
'renameuser-linkoncontribs', $userpage->getText() )->text(),
674 [
'title' => $sp->msg(
'renameuser-linkoncontribs-text', $userpage->getText() )->parse() ],
675 [
'oldusername' => $userpage->getText() ]
679 $hookRunner->onContributionsToolLinks( $id, $userpage, $tools, $sp );
693 'default' => str_replace(
'_',
' ', $target ),
694 'label' => $this->msg(
'sp-contributions-username' )->text(),
696 'id' =>
'mw-target-user-or-ip',
698 'autofocus' => $target ===
'',
699 'section' =>
'contribs-top',
713 protected function getForm( array $pagerOptions ) {
714 if ( $this->including() ) {
720 $this->getOutput()->addModules( [
721 'mediawiki.special.contributions',
723 $this->getOutput()->enableOOUI();
726 # Add hidden params for tracking except for parameters in $skipParameters
745 foreach ( $this->opts as $name => $value ) {
746 if ( in_array( $name, $skipParameters ) ) {
757 $target = $this->opts[
'target'] ??
'';
758 $fields[
'target'] = $this->getTargetField( $target );
760 $ns = $this->opts[
'namespace'] ??
null;
761 $fields[
'namespace'] = [
762 'type' =>
'namespaceselect',
763 'label' => $this->msg(
'namespace' )->text(),
764 'name' =>
'namespace',
765 'cssclass' =>
'namespaceselector',
768 'section' =>
'contribs-top',
770 $fields[
'nsFilters'] = [
771 'class' => HTMLMultiSelectField::class,
773 'name' =>
'wpfilters',
776 'hide-if' => [
'===',
'namespace',
'all' ],
777 'options-messages' => [
778 'invert' =>
'nsInvert',
779 'namespace_association' =>
'associated',
781 'section' =>
'contribs-top',
783 $fields[
'tagfilter'] = [
784 'type' =>
'tagfilter',
785 'cssclass' =>
'mw-tagfilter-input',
787 'label-message' => [
'tag-filter',
'parse' ],
788 'name' =>
'tagfilter',
790 'section' =>
'contribs-top',
792 $fields[
'tagInvert'] = [
795 'label' => $this->msg(
'invert' ),
796 'name' =>
'tagInvert',
797 'hide-if' => [
'===',
'tagfilter',
'' ],
798 'section' =>
'contribs-top',
801 if ( $this->permissionManager->userHasRight( $this->getUser(),
'deletedhistory' ) ) {
802 $fields[
'deletedOnly'] = [
804 'id' =>
'mw-show-deleted-only',
805 'label' => $this->msg(
'history-show-deleted' )->text(),
806 'name' =>
'deletedOnly',
807 'section' =>
'contribs-top',
811 $fields[
'topOnly'] = [
813 'id' =>
'mw-show-top-only',
814 'label' => $this->msg(
'sp-contributions-toponly' )->text(),
816 'section' =>
'contribs-top',
818 $fields[
'newOnly'] = [
820 'id' =>
'mw-show-new-only',
821 'label' => $this->msg(
'sp-contributions-newonly' )->text(),
823 'section' =>
'contribs-top',
825 $fields[
'hideMinor'] = [
827 'cssclass' =>
'mw-hide-minor-edits',
828 'id' =>
'mw-show-new-only',
829 'label' => $this->msg(
'sp-contributions-hideminor' )->text(),
830 'name' =>
'hideMinor',
831 'section' =>
'contribs-top',
836 $this->getHookRunner()->onSpecialContributions__getForm__filters(
837 $this, $rawFilters );
838 foreach ( $rawFilters as $filter ) {
840 if ( is_string( $filter ) ) {
843 'default' => $filter,
845 'section' =>
'contribs-top',
848 'A SpecialContributions::getForm::filters hook handler returned ' .
849 'an array of strings, this is deprecated since MediaWiki 1.33',
861 'id' =>
'mw-date-start',
862 'label' => $this->msg(
'date-range-from' )->text(),
864 'section' =>
'contribs-date',
869 'id' =>
'mw-date-end',
870 'label' => $this->msg(
'date-range-to' )->text(),
872 'section' =>
'contribs-date',
875 $htmlForm = HTMLForm::factory(
'ooui', $fields, $this->
getContext() );
878 ->setTitle( $this->getPageTitle() )
882 ->setCollapsibleOptions(
883 ( $pagerOptions[
'target'] ??
null ) ||
884 ( $pagerOptions[
'start'] ??
null ) ||
885 ( $pagerOptions[
'end'] ??
null )
888 ->setSubmitTextMsg(
'sp-contributions-submit' )
889 ->setWrapperLegendMsg( $this->getFormWrapperLegendMessageKey() );
891 $htmlForm->prepareForm();
894 $htmlForm->setSubmitCallback(
static function () {
897 $result = $htmlForm->tryAuthorizedSubmit();
898 if ( !( $result ===
true || ( $result instanceof
Status && $result->
isGood() ) ) ) {
900 $htmlForm->setCollapsibleOptions(
false );
901 $this->formErrors =
true;
904 return $htmlForm->getHTML( $result );
916 $search = $this->userNameUtils->getCanonical( $search );
922 return $this->userNamePrefixSearch
923 ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset );
940 throw new \LogicException( __METHOD__ .
" must be overridden" );
954 return 'sp-contributions-search';
961 return 'contributions-title';
wfEscapeWikiText( $input)
Escapes the given text so that it may be output using addWikiText() without any linking,...
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 URL path to a MediaWiki entry point.
if(!defined('MW_SETUP_CALLBACK'))
A class containing constants representing the names of configuration variables.
const RangeContributionsCIDRLimit
Name constant for the RangeContributionsCIDRLimit setting, for use with Config::get()
Show user contributions in a paged list.
__construct(PermissionManager $permissionManager, IConnectionProvider $dbProvider, NamespaceInfo $namespaceInfo, UserNameUtils $userNameUtils, UserNamePrefixSearch $userNamePrefixSearch, UserOptionsLookup $userOptionsLookup, UserFactory $userFactory, UserIdentityLookup $userIdentityLookup, DatabaseBlockStore $blockStore, $name, $restriction='')
contributionsSub( $userObj, $targetName)
Generates the subheading with links.
getFormWrapperLegendMessageKey()
getResultsPageTitleMessageKey()
UserIdentityLookup $userIdentityLookup
NamespaceInfo $namespaceInfo
PermissionManager $permissionManager
getTargetField( $target)
Get the target field for the form.
UserOptionsLookup $userOptionsLookup
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
UserNamePrefixSearch $userNamePrefixSearch
IConnectionProvider $dbProvider
UserNameUtils $userNameUtils
execute( $par)
Default execute method Checks user permissions.This must be overridden by subclasses; it will be made...
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.
DatabaseBlockStore $blockStore
Shortcut to construct an includable special page.
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
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,...
getUser()
Shortcut to get the User executing this instance.
addFeedLinks( $params)
Adds RSS/atom links.
getConfig()
Shortcut to get main config object.
getRequest()
Get the WebRequest being used for this instance.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getOutput()
Get the OutputPage being used for this instance.
including( $x=null)
Whether the special page is being evaluated via transclusion.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages By default the message key is the canonical name of...
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
isGood()
Returns whether the operation completed and didn't have any error or warnings.