36 parent::__construct(
'Contributions' );
44 $out->addModuleStyles( [
45 'jquery.makeCollapsible.styles',
46 'mediawiki.interface.helpers.styles',
48 'mediawiki.special.changeslist',
51 'mediawiki.special.recentchanges',
53 'mediawiki.page.ready'
60 $target = $par ?? $request->getVal(
'target' );
62 $this->opts[
'deletedOnly'] = $request->getBool(
'deletedOnly' );
64 if ( !strlen( $target ) ) {
66 $out->addHTML( $this->
getForm( $this->opts ) );
74 $this->opts[
'limit'] = $request->getInt(
'limit', $user->getOption(
'rclimit' ) );
75 $this->opts[
'target'] = $target;
76 $this->opts[
'topOnly'] = $request->getBool(
'topOnly' );
77 $this->opts[
'newOnly'] = $request->getBool(
'newOnly' );
78 $this->opts[
'hideMinor'] = $request->getBool(
'hideMinor' );
84 $out->addHTML( $this->
getForm( $this->opts ) );
89 $out->setHTMLTitle( $this->
msg(
91 $this->
msg(
'contributions-title', $target )->plain()
92 )->inContentLanguage() );
94 $nt = Title::makeTitleSafe(
NS_USER, $target );
96 $out->addHTML( $this->
getForm( $this->opts ) );
101 $out->addHTML( $this->
getForm( $this->opts ) );
104 $id = $userObj->getId();
106 $target = $nt->getText();
108 $out->setHTMLTitle( $this->
msg(
110 $this->
msg(
'contributions-title', $target )->plain()
111 )->inContentLanguage() );
113 # For IP ranges, we want the contributionsSub, but not the skin-dependent
114 # links under 'Tools', which may include irrelevant links like 'Logs'.
115 if ( !IP::isValidRange( $target ) ) {
116 $this->
getSkin()->setRelevantUser( $userObj );
120 $ns = $request->getVal(
'namespace',
null );
121 if ( $ns !==
null && $ns !==
'' && $ns !==
'all' ) {
122 $this->opts[
'namespace'] = intval( $ns );
124 $this->opts[
'namespace'] =
'';
130 $this->opts[
'associated'] = $request->getBool(
'associated' );
131 $this->opts[
'nsInvert'] = (bool)$request->getVal(
'nsInvert' );
132 $nsFilters = $request->getArray(
'wpfilters',
null );
133 if ( $nsFilters !==
null ) {
134 $this->opts[
'associated'] = in_array(
'associated', $nsFilters );
135 $this->opts[
'nsInvert'] = in_array(
'nsInvert', $nsFilters );
138 $this->opts[
'tagfilter'] = (string)$request->getVal(
'tagfilter' );
142 if ( MediaWikiServices::getInstance()
143 ->getPermissionManager()
144 ->userHasRight( $user,
'markbotedits' ) && $request->getBool(
'bot' )
146 $this->opts[
'bot'] =
'1';
149 $skip = $request->getText(
'offset' ) || $request->getText(
'dir' ) ==
'prev';
150 # Offset overrides year/month selection
152 $this->opts[
'year'] = $request->getVal(
'year' );
153 $this->opts[
'month'] = $request->getVal(
'month' );
155 $this->opts[
'start'] = $request->getVal(
'start' );
156 $this->opts[
'end'] = $request->getVal(
'end' );
160 if ( $this->opts[
'namespace'] <
NS_MAIN ) {
162 "<div class=\"mw-negative-namespace-not-supported error\">\n\$1\n</div>",
163 [
'negative-namespace-not-supported' ]
165 $out->addHTML( $this->
getForm( $this->opts ) );
169 $feedType = $request->getVal(
'feed' );
172 'action' =>
'feedcontributions',
175 if ( $this->opts[
'topOnly'] ) {
176 $feedParams[
'toponly'] =
true;
178 if ( $this->opts[
'newOnly'] ) {
179 $feedParams[
'newonly'] =
true;
181 if ( $this->opts[
'hideMinor'] ) {
182 $feedParams[
'hideminor'] =
true;
184 if ( $this->opts[
'deletedOnly'] ) {
185 $feedParams[
'deletedonly'] =
true;
187 if ( $this->opts[
'tagfilter'] !==
'' ) {
188 $feedParams[
'tagfilter'] = $this->opts[
'tagfilter'];
190 if ( $this->opts[
'namespace'] !==
'' ) {
191 $feedParams[
'namespace'] = $this->opts[
'namespace'];
195 if ( $feedType && $this->opts[
'year'] !==
null ) {
196 $feedParams[
'year'] = $this->opts[
'year'];
198 if ( $feedType && $this->opts[
'month'] !==
null ) {
199 $feedParams[
'month'] = $this->opts[
'month'];
205 $feedParams[
'feedformat'] = $feedType;
208 $out->redirect( $url,
'301' );
216 if ( Hooks::run(
'SpecialContributionsBeforeMainOutput', [ $id, $userObj, $this ] ) ) {
219 'namespace' => $this->opts[
'namespace'],
220 'tagfilter' => $this->opts[
'tagfilter'],
221 'start' => $this->opts[
'start'],
222 'end' => $this->opts[
'end'],
223 'deletedOnly' => $this->opts[
'deletedOnly'],
224 'topOnly' => $this->opts[
'topOnly'],
225 'newOnly' => $this->opts[
'newOnly'],
226 'hideMinor' => $this->opts[
'hideMinor'],
227 'nsInvert' => $this->opts[
'nsInvert'],
228 'associated' => $this->opts[
'associated'],
231 $out->addHTML( $this->
getForm( $this->opts ) );
234 if ( IP::isValidRange( $target ) && !$pager->isQueryableRange( $target ) ) {
236 $limits = $this->
getConfig()->get(
'RangeContributionsCIDRLimit' );
237 $limit = $limits[ IP::isIPv4( $target ) ?
'IPv4' :
'IPv6' ];
238 $out->addWikiMsg(
'sp-contributions-outofrange', $limit );
239 } elseif ( !$pager->getNumRows() ) {
240 $out->addWikiMsg(
'nocontribs', $target );
244 $poolKey = WikiMap::getCurrentWikiDbDomain() .
':SpecialContributions:';
245 if ( $this->
getUser()->isAnon() ) {
246 $poolKey .=
'a:' . $this->
getUser()->getName();
248 $poolKey .=
'u:' . $this->
getUser()->getId();
251 'doWork' =>
function () use ( $pager, $out ) {
252 # Show a message about replica DB lag, if applicable
253 $lag = $pager->getDatabase()->getSessionLagStatus()[
'lag'];
255 $out->showLagWarning( $lag );
258 $output = $pager->getBody();
260 $output = $pager->getNavigationBar() .
262 $pager->getNavigationBar();
264 $out->addHTML( $output );
266 'error' =>
function () use ( $out ) {
267 $msg = $this->
getUser()->isAnon()
268 ?
'sp-contributions-concurrency-ip'
269 :
'sp-contributions-concurrency-user';
270 $out->wrapWikiMsg(
"<div class='errorbox'>\n$1\n</div>", $msg );
276 $out->preventClickjacking( $pager->getPreventClickjacking() );
278 # Show the appropriate "footer" message - WHOIS tools, etc.
279 if ( IP::isValidRange( $target ) ) {
280 $message =
'sp-contributions-footer-anon-range';
281 } elseif ( IP::isIPAddress( $target ) ) {
282 $message =
'sp-contributions-footer-anon';
283 } elseif ( $userObj->isAnon() ) {
287 $message =
'sp-contributions-footer';
290 if ( $message && !$this->
including() && !$this->
msg( $message, $target )->isDisabled() ) {
292 "<div class='mw-contributions-footer'>\n$1\n</div>",
293 [ $message, $target ] );
306 if ( $userObj->isAnon() ) {
311 if ( !
User::isIP( $userObj->getName() ) && !$userObj->isIPRange() ) {
313 "<div class=\"mw-userpage-userdoesnotexist error\">\n\$1\n</div>",
315 'contributions-userdoesnotexist',
320 $this->
getOutput()->setStatusCode( 404 );
323 $user = htmlspecialchars( $userObj->getName() );
325 $user = $this->
getLinkRenderer()->makeLink( $userObj->getUserPage(), $userObj->getName() );
327 $nt = $userObj->getUserPage();
328 $talk = $userObj->getTalkPage();
332 $links = Html::openElement(
'span', [
'class' =>
'mw-changeslist-links' ] );
333 foreach ( $tools as $tool ) {
334 $links .= Html::rawElement(
'span', [], $tool ) .
' ';
336 $links = trim( $links ) . Html::closeElement(
'span' );
344 if ( $userObj->isIPRange() ) {
345 $block = DatabaseBlock::newFromTarget( $userObj->getName(), $userObj->getName() );
347 $block = DatabaseBlock::newFromTarget( $userObj, $userObj );
350 if ( !is_null( $block ) && $block->getType() != DatabaseBlock::TYPE_AUTO ) {
351 if ( $block->getType() == DatabaseBlock::TYPE_RANGE ) {
352 $nt = MediaWikiServices::getInstance()->getNamespaceInfo()->
353 getCanonicalName(
NS_USER ) .
':' . $block->getTarget();
364 'showIfEmpty' =>
false,
367 'sp-contributions-blocked-notice-anon' :
368 'sp-contributions-blocked-notice',
369 $userObj->getName() # Support GENDER in
'sp-contributions-blocked-notice'
371 'offset' =>
'' # don
't use WebRequest parameter offset
378 return Html::rawElement( 'div
', [ 'class' => 'mw-contributions-user-tools
' ],
379 $this->msg( 'contributions-subtitle
' )->rawParams( $user )->params( $userObj->getName() )
392 public static function getUserLinks( SpecialPage $sp, User $target ) {
393 $id = $target->getId();
394 $username = $target->getName();
395 $userpage = $target->getUserPage();
396 $talkpage = $target->getTalkPage();
397 $isIP = IP::isValid( $username );
398 $isRange = IP::isValidRange( $username );
400 $linkRenderer = $sp->getLinkRenderer();
403 # No talk pages for IP ranges.
405 $tools['user-talk
'] = $linkRenderer->makeLink(
407 $sp->msg( 'sp-contributions-talk
' )->text()
411 # Block / Change block / Unblock links
412 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
413 if ( $permissionManager->userHasRight( $sp->getUser(), 'block
' ) ) {
414 if ( $target->getBlock() && $target->getBlock()->getType() != DatabaseBlock::TYPE_AUTO ) {
415 $tools['block
'] = $linkRenderer->makeKnownLink( # Change block link
416 SpecialPage::getTitleFor( 'Block
', $username ),
417 $sp->msg( 'change-blocklink
' )->text()
419 $tools['unblock
'] = $linkRenderer->makeKnownLink( # Unblock link
420 SpecialPage::getTitleFor( 'Unblock
', $username ),
421 $sp->msg( 'unblocklink
' )->text()
423 } else { # User is not blocked
424 $tools['block
'] = $linkRenderer->makeKnownLink( # Block link
425 SpecialPage::getTitleFor( 'Block
', $username ),
426 $sp->msg( 'blocklink
' )->text()
432 $tools['log-block
'] = $linkRenderer->makeKnownLink(
433 SpecialPage::getTitleFor( 'Log
', 'block
' ),
434 $sp->msg( 'sp-contributions-blocklog
' )->text(),
436 [ 'page
' => $userpage->getPrefixedText() ]
439 # Suppression log link (T61120)
440 if ( $permissionManager->userHasRight( $sp->getUser(), 'suppressionlog
' ) ) {
441 $tools['log-suppression
'] = $linkRenderer->makeKnownLink(
442 SpecialPage::getTitleFor( 'Log
', 'suppress
' ),
443 $sp->msg( 'sp-contributions-suppresslog
', $username )->text(),
445 [ 'offender
' => $username ]
449 # Don't show some links
for IP ranges
451 # Uploads: hide if IPs cannot upload (T220674)
452 if ( !$isIP || $permissionManager->userHasRight( $target,
'upload' ) ) {
455 $sp->msg(
'sp-contributions-uploads' )->text()
463 $sp->msg(
'sp-contributions-logs' )->text()
466 # Add link to deleted user contributions for priviledged users
468 if ( $permissionManager->userHasRight( $sp->getUser(),
'deletedhistory' ) ) {
471 $sp->msg(
'sp-contributions-deleted', $username )->text()
476 # Add a link to change user rights for privileged users
478 $userrightsPage->setContext( $sp->getContext() );
479 if ( $userrightsPage->userCanChangeRights( $target ) ) {
481 SpecialPage::getTitleFor(
'Userrights', $username ),
482 $sp->msg(
'sp-contributions-userrights', $username )->text()
486 Hooks::run(
'ContributionsToolLinks', [ $id, $userpage, &$tools, $sp ] );
497 protected function getForm( array $pagerOptions ) {
498 $this->opts[
'title'] = $this->
getPageTitle()->getPrefixedText();
501 'mediawiki.userSuggest',
502 'mediawiki.special.contributions',
504 $this->
getOutput()->addModuleStyles(
'mediawiki.widgets.DateInputWidget.styles' );
508 # Add hidden params for tracking except for parameters in $skipParameters
525 foreach ( $this->opts as $name => $value ) {
526 if ( in_array( $name, $skipParameters ) ) {
537 $target = $this->opts[
'target'] ??
null;
538 $fields[
'target'] = [
540 'cssclass' =>
'mw-autocomplete-user mw-ui-input-inline mw-input',
541 'default' => $target ?
542 str_replace(
'_',
' ', $target ) :
'' ,
543 'label' => $this->
msg(
'sp-contributions-username' )->text(),
545 'id' =>
'mw-target-user-or-ip',
547 'autofocus' => !$target,
548 'section' =>
'contribs-top',
551 $ns = $this->opts[
'namespace'] ??
null;
552 $fields[
'namespace'] = [
553 'type' =>
'namespaceselect',
554 'label' => $this->
msg(
'namespace' )->text(),
555 'name' =>
'namespace',
556 'cssclass' =>
'namespaceselector',
559 'section' =>
'contribs-top',
562 $nsFilters = $request->getArray(
'wpfilters' );
563 $fields[
'nsFilters'] = [
564 'class' =>
'HTMLMultiSelectField',
566 'name' =>
'wpfilters',
569 'cssclass' => $ns ===
'' ?
570 'contribs-ns-filters mw-input-with-label mw-input-hidden' :
571 'contribs-ns-filters mw-input-with-label',
575 'options-messages' => [
576 'invert' =>
'nsInvert',
577 'namespace_association' =>
'associated',
579 'default' => $nsFilters,
580 'section' =>
'contribs-top',
582 $fields[
'tagfilter'] = [
583 'type' =>
'tagfilter',
584 'cssclass' =>
'mw-tagfilter-input',
586 'label-message' => [
'tag-filter',
'parse' ],
587 'name' =>
'tagfilter',
589 'section' =>
'contribs-top',
592 if ( MediaWikiServices::getInstance()
594 ->userHasRight( $this->
getUser(),
'deletedhistory' )
596 $fields[
'deletedOnly'] = [
598 'id' =>
'mw-show-deleted-only',
599 'label' => $this->
msg(
'history-show-deleted' )->text(),
600 'name' =>
'deletedOnly',
601 'section' =>
'contribs-top',
605 $fields[
'topOnly'] = [
607 'id' =>
'mw-show-top-only',
608 'label' => $this->
msg(
'sp-contributions-toponly' )->text(),
610 'section' =>
'contribs-top',
612 $fields[
'newOnly'] = [
614 'id' =>
'mw-show-new-only',
615 'label' => $this->
msg(
'sp-contributions-newonly' )->text(),
617 'section' =>
'contribs-top',
619 $fields[
'hideMinor'] = [
621 'cssclass' =>
'mw-hide-minor-edits',
622 'id' =>
'mw-show-new-only',
623 'label' => $this->
msg(
'sp-contributions-hideminor' )->text(),
624 'name' =>
'hideMinor',
625 'section' =>
'contribs-top',
631 'SpecialContributions::getForm::filters',
632 [ $this, &$rawFilters ]
634 foreach ( $rawFilters as
$filter ) {
641 'section' =>
'contribs-top',
645 ' returning string[]',
657 'id' =>
'mw-date-start',
658 'label' => $this->
msg(
'date-range-from' )->text(),
660 'section' =>
'contribs-date',
665 'id' =>
'mw-date-end',
666 'label' => $this->
msg(
'date-range-to' )->text(),
668 'section' =>
'contribs-date',
671 $htmlForm = HTMLForm::factory(
'ooui', $fields, $this->
getContext() );
677 ->setCollapsibleOptions(
678 ( $pagerOptions[
'target'] ??
null ) ||
679 ( $pagerOptions[
'start'] ??
null ) ||
680 ( $pagerOptions[
'end'] ??
null )
683 ->setSubmitText( $this->
msg(
'sp-contributions-submit' )->text() )
684 ->setWrapperLegend( $this->
msg(
'sp-contributions-search' )->text() );
686 $explain = $this->
msg(
'sp-contributions-explain' );
687 if ( !$explain->isBlank() ) {
688 $htmlForm->addFooterText(
"<p id='mw-sp-contributions-explain'>{$explain->parse()}</p>" );
691 $htmlForm->loadData();
693 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)
Throws a warning that $function is deprecated.
static isExternal( $username)
Tells whether the username is external or not.
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
A collection of public static functions to play with IP address and IP ranges.
Shortcut to construct an includable special page.
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
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.
static getUserLinks(SpecialPage $sp, User $target)
Links to different places.
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.
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.
getContext()
Gets the context this SpecialPage is executed in.
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.
getPageTitle( $subpage=false)
Get a self-referential title object.
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.
MediaWiki Linker LinkRenderer null $linkRenderer
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.