49 private $linkBatchFactory;
52 private $permissionManager;
55 private $loadBalancer;
58 private $actorMigration;
61 private $revisionStore;
64 private $namespaceInfo;
67 private $userNameUtils;
70 private $userNamePrefixSearch;
73 private $userOptionsLookup;
76 private $commentFormatter;
82 private $pager =
null;
110 parent::__construct(
'Contributions' );
112 $services = MediaWikiServices::getInstance();
113 $this->linkBatchFactory = $linkBatchFactory ?? $services->getLinkBatchFactory();
114 $this->permissionManager = $permissionManager ?? $services->getPermissionManager();
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();
120 $this->userNamePrefixSearch = $userNamePrefixSearch ?? $services->getUserNamePrefixSearch();
121 $this->userOptionsLookup = $userOptionsLookup ?? $services->getUserOptionsLookup();
122 $this->commentFormatter = $commentFormatter ?? $services->getCommentFormatter();
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' );
209 if ( ExternalUserNames::isExternal( $target ) ) {
210 $userObj = $this->userFactory->newFromName( $target, UserRigorOptions::RIGOR_NONE );
212 $out->addHTML( $this->
getForm( $this->opts ) );
217 $out->setPageTitle( $this->
msg(
'contributions-title', $target )->escaped() );
219 $nt = Title::makeTitleSafe(
NS_USER, $target );
221 $out->addHTML( $this->
getForm( $this->opts ) );
224 $target = $nt->getText();
225 if ( IPUtils::isValidRange( $target ) ) {
226 $target = IPUtils::sanitizeRange( $target );
228 $userObj = $this->userFactory->newFromName( $target, UserRigorOptions::RIGOR_NONE );
230 $out->addHTML( $this->
getForm( $this->opts ) );
233 $id = $userObj->getId();
236 $out->setPageTitle( $this->
msg(
'contributions-title', $target )->escaped() );
238 # For IP ranges, we want the contributionsSub, but not the skin-dependent
239 # links under 'Tools', which may include irrelevant links like 'Logs'.
240 if ( !IPUtils::isValidRange( $target ) &&
241 ( $this->userNameUtils->isIP( $target ) || $userObj->isRegistered() )
249 $this->
getSkin()->setRelevantUser( $userObj );
253 $this->opts = ContribsPager::processDateFilter( $this->opts );
255 if ( $this->opts[
'namespace'] !==
'' && $this->opts[
'namespace'] <
NS_MAIN ) {
257 "<div class=\"mw-negative-namespace-not-supported error\">\n\$1\n</div>",
258 [
'negative-namespace-not-supported' ]
260 $out->addHTML( $this->
getForm( $this->opts ) );
264 $feedType = $request->getVal(
'feed' );
267 'action' =>
'feedcontributions',
270 if ( $this->opts[
'topOnly'] ) {
271 $feedParams[
'toponly'] =
true;
273 if ( $this->opts[
'newOnly'] ) {
274 $feedParams[
'newonly'] =
true;
276 if ( $this->opts[
'hideMinor'] ) {
277 $feedParams[
'hideminor'] =
true;
279 if ( $this->opts[
'deletedOnly'] ) {
280 $feedParams[
'deletedonly'] =
true;
283 if ( $this->opts[
'tagfilter'] !== [] ) {
284 $feedParams[
'tagfilter'] = $this->opts[
'tagfilter'];
286 if ( $this->opts[
'namespace'] !==
'' ) {
287 $feedParams[
'namespace'] = $this->opts[
'namespace'];
291 if ( $feedType && isset( $this->opts[
'year'] ) ) {
292 $feedParams[
'year'] = $this->opts[
'year'];
294 if ( $feedType && isset( $this->opts[
'month'] ) ) {
295 $feedParams[
'month'] = $this->opts[
'month'];
301 $feedParams[
'feedformat'] = $feedType;
304 $out->redirect( $url,
'301' );
312 if ( $this->
getHookRunner()->onSpecialContributionsBeforeMainOutput(
313 $id, $userObj, $this )
316 $out->addHTML( $this->
getForm( $this->opts ) );
318 $pager = $this->getPager( $userObj );
319 if ( IPUtils::isValidRange( $target ) && !$pager->
isQueryableRange( $target ) ) {
321 $limits = $this->
getConfig()->get( MainConfigNames::RangeContributionsCIDRLimit );
322 $limit = $limits[ IPUtils::isIPv4( $target ) ?
'IPv4' :
'IPv6' ];
323 $out->addWikiMsg(
'sp-contributions-outofrange', $limit );
327 $poolKey = $this->loadBalancer->getLocalDomainID() .
':SpecialContributions:';
328 if ( $this->
getUser()->isAnon() ) {
329 $poolKey .=
'a:' . $this->
getUser()->getName();
331 $poolKey .=
'u:' . $this->
getUser()->getId();
334 'doWork' =>
function () use ( $pager, $out, $target ) {
336 $out->addWikiMsg(
'nocontribs', $target );
338 # Show a message about replica DB lag, if applicable
339 $lag = $pager->getDatabase()->getSessionLagStatus()[
'lag'];
341 $out->showLagWarning( $lag );
350 $out->addHTML( $output );
353 'error' =>
function () use ( $out ) {
354 $msg = $this->
getUser()->isAnon()
355 ?
'sp-contributions-concurrency-ip'
356 :
'sp-contributions-concurrency-user';
359 $out->msg( $msg )->parse()
369 # Show the appropriate "footer" message - WHOIS tools, etc.
370 if ( IPUtils::isValidRange( $target ) && $pager->
isQueryableRange( $target ) ) {
371 $message =
'sp-contributions-footer-anon-range';
372 } elseif ( IPUtils::isIPAddress( $target ) ) {
373 $message =
'sp-contributions-footer-anon';
374 } elseif ( $userObj->isAnon() ) {
377 } elseif ( $userObj->isHidden() &&
378 !$this->permissionManager->userHasRight( $this->getUser(),
'hideuser' )
385 $message =
'sp-contributions-footer';
388 if ( $message && !$this->
including() && !$this->
msg( $message, $target )->isDisabled() ) {
390 "<div class='mw-contributions-footer'>\n$1\n</div>",
391 [ $message, $target ] );
406 $isAnon = $userObj->isAnon();
407 if ( !$isAnon && $userObj->isHidden() &&
408 !$this->permissionManager->userHasRight( $this->getUser(),
'hideuser' )
420 if ( !$this->userNameUtils->isIP( $userObj->getName() )
421 && !IPUtils::isValidRange( $userObj->getName() )
423 $this->getOutput()->addHtml( Html::warningBox(
424 $this->getOutput()->msg(
'contributions-userdoesnotexist',
426 'mw-userpage-userdoesnotexist'
428 if ( !$this->including() ) {
429 $this->getOutput()->setStatusCode( 404 );
432 $user = htmlspecialchars( $userObj->getName() );
434 $user = $this->getLinkRenderer()->makeLink( $userObj->getUserPage(), $userObj->getName() );
436 $nt = $userObj->getUserPage();
437 $talk = $userObj->getTalkPage();
441 $showForIp = IPUtils::isValid( $userObj ) ||
442 ( IPUtils::isValidRange( $userObj ) && $this->getPager( $userObj )->isQueryableRange( $userObj ) );
445 $registeredAndVisible = $userObj->isRegistered() && ( !$userObj->isHidden()
446 || $this->permissionManager->userHasRight( $this->
getUser(),
'hideuser' ) );
448 if ( $talk && ( $registeredAndVisible || $showForIp ) ) {
449 $tools = self::getUserLinks(
452 $this->permissionManager,
453 $this->getHookRunner()
455 $links = Html::openElement(
'span', [
'class' =>
'mw-changeslist-links' ] );
456 foreach ( $tools as $tool ) {
457 $links .= Html::rawElement(
'span', [], $tool ) .
' ';
459 $links = trim( $links ) . Html::closeElement(
'span' );
464 if ( !$this->including() ) {
467 if ( IPUtils::isValidRange( $userObj->getName() ) ) {
468 $block = DatabaseBlock::newFromTarget( $userObj->getName(), $userObj->getName() );
470 $block = DatabaseBlock::newFromTarget( $userObj, $userObj );
473 if ( $block !==
null && $block->getType() != DatabaseBlock::TYPE_AUTO ) {
474 if ( $block->getType() == DatabaseBlock::TYPE_RANGE ) {
475 $nt = $this->namespaceInfo->getCanonicalName(
NS_USER )
476 .
':' . $block->getTargetName();
479 $out = $this->getOutput();
480 if ( $userObj->isAnon() ) {
481 $msgKey = $block->isSitewide() ?
482 'sp-contributions-blocked-notice-anon' :
483 'sp-contributions-blocked-notice-anon-partial';
485 $msgKey = $block->isSitewide() ?
486 'sp-contributions-blocked-notice' :
487 'sp-contributions-blocked-notice-partial';
490 $class = $block->isSitewide() ?
491 'mw-contributions-blocked-notice' :
492 'mw-contributions-blocked-notice-partial';
493 LogEventsList::showLogExtract(
500 'showIfEmpty' =>
false,
503 $userObj->getName() # Support GENDER in
'sp-contributions-blocked-notice'
505 'offset' =>
'', # don
't use WebRequest parameter offset
506 'wrap
' => Html::rawElement(
508 [ 'class' => $class ],
517 return Html::rawElement( 'div
', [ 'class' => 'mw-contributions-user-tools
' ],
518 $this->msg( 'contributions-subtitle
' )->rawParams( $user )->params( $userObj->getName() )
533 public static function getUserLinks(
536 PermissionManager $permissionManager = null,
537 HookRunner $hookRunner = null
539 // Fallback to global state, if not provided
540 $permissionManager = $permissionManager ?? MediaWikiServices::getInstance()->getPermissionManager();
541 $hookRunner = $hookRunner ?? Hooks::runner();
543 $id = $target->getId();
544 $username = $target->getName();
545 $userpage = $target->getUserPage();
546 $talkpage = $target->getTalkPage();
547 $isIP = IPUtils::isValid( $username );
548 $isRange = IPUtils::isValidRange( $username );
550 $linkRenderer = $sp->getLinkRenderer();
553 # No talk pages for IP ranges.
555 $tools['user-talk
'] = $linkRenderer->makeLink(
557 $sp->msg( 'sp-contributions-talk
' )->text(),
558 [ 'class' => 'mw-contributions-link-talk
' ]
562 # Block / Change block / Unblock links
563 if ( $permissionManager->userHasRight( $sp->getUser(), 'block
' ) ) {
564 if ( $target->getBlock() && $target->getBlock()->getType() != DatabaseBlock::TYPE_AUTO ) {
565 $tools['block
'] = $linkRenderer->makeKnownLink( # Change block link
566 SpecialPage::getTitleFor( 'Block
', $username ),
567 $sp->msg( 'change-blocklink
' )->text(),
568 [ 'class' => 'mw-contributions-link-change-block
' ]
570 $tools['unblock
'] = $linkRenderer->makeKnownLink( # Unblock link
571 SpecialPage::getTitleFor( 'Unblock
', $username ),
572 $sp->msg( 'unblocklink
' )->text(),
573 [ 'class' => 'mw-contributions-link-unblock
' ]
575 } else { # User is not blocked
576 $tools['block
'] = $linkRenderer->makeKnownLink( # Block link
577 SpecialPage::getTitleFor( 'Block
', $username ),
578 $sp->msg( 'blocklink
' )->text(),
579 [ 'class' => 'mw-contributions-link-block
' ]
585 $tools['log-block
'] = $linkRenderer->makeKnownLink(
586 SpecialPage::getTitleFor( 'Log
', 'block
' ),
587 $sp->msg( 'sp-contributions-blocklog
' )->text(),
588 [ 'class' => 'mw-contributions-link-block-log
' ],
589 [ 'page
' => $userpage->getPrefixedText() ]
592 # Suppression log link (T61120)
593 if ( $permissionManager->userHasRight( $sp->getUser(), 'suppressionlog
' ) ) {
594 $tools['log-suppression
'] = $linkRenderer->makeKnownLink(
595 SpecialPage::getTitleFor( 'Log
', 'suppress
' ),
596 $sp->msg( 'sp-contributions-suppresslog
', $username )->text(),
597 [ 'class' => 'mw-contributions-link-suppress-log
' ],
598 [ 'offender
' => $username ]
602 # Don't show some links
for IP ranges
604 # Uploads: hide if IPs cannot upload (T220674)
605 if ( !$isIP || $permissionManager->userHasRight( $target,
'upload' ) ) {
606 $tools[
'uploads'] = $linkRenderer->makeKnownLink(
607 SpecialPage::getTitleFor(
'Listfiles', $username ),
608 $sp->msg(
'sp-contributions-uploads' )->text(),
609 [
'class' =>
'mw-contributions-link-uploads' ]
615 $tools[
'logs'] = $linkRenderer->makeKnownLink(
617 $sp->msg(
'sp-contributions-logs' )->text(),
618 [
'class' =>
'mw-contributions-link-logs' ]
621 # Add link to deleted user contributions for privileged users
623 if ( $permissionManager->userHasRight( $sp->getUser(),
'deletedhistory' ) ) {
624 $tools[
'deletedcontribs'] = $linkRenderer->makeKnownLink(
626 $sp->msg(
'sp-contributions-deleted', $username )->text(),
627 [
'class' =>
'mw-contributions-link-deleted-contribs' ]
632 # Add a link to change user rights for privileged users
634 $userrightsPage->setContext( $sp->getContext() );
635 if ( $userrightsPage->userCanChangeRights( $target ) ) {
636 $tools[
'userrights'] = $linkRenderer->makeKnownLink(
638 $sp->msg(
'sp-contributions-userrights', $username )->text(),
639 [
'class' =>
'mw-contributions-link-user-rights' ]
643 $hookRunner->onContributionsToolLinks( $id, $userpage, $tools, $sp );
654 protected function getForm( array $pagerOptions ) {
656 $this->getOutput()->addModules( [
657 'mediawiki.special.contributions',
659 $this->getOutput()->addModuleStyles(
'mediawiki.widgets.DateInputWidget.styles' );
660 $this->getOutput()->enableOOUI();
663 # Add hidden params for tracking except for parameters in $skipParameters
681 foreach ( $this->opts as $name => $value ) {
682 if ( in_array( $name, $skipParameters ) ) {
693 $target = $this->opts[
'target'] ??
null;
694 $fields[
'target'] = [
696 'default' => $target ?
697 str_replace(
'_',
' ', $target ) :
'' ,
698 'label' => $this->msg(
'sp-contributions-username' )->text(),
700 'id' =>
'mw-target-user-or-ip',
702 'autofocus' => !$target,
703 'section' =>
'contribs-top',
708 $ns = $this->opts[
'namespace'] ??
null;
709 $fields[
'namespace'] = [
710 'type' =>
'namespaceselect',
711 'label' => $this->msg(
'namespace' )->text(),
712 'name' =>
'namespace',
713 'cssclass' =>
'namespaceselector',
716 'section' =>
'contribs-top',
718 $fields[
'nsFilters'] = [
719 'class' => HTMLMultiSelectField::class,
721 'name' =>
'wpfilters',
724 'hide-if' => [
'===',
'namespace',
'all' ],
725 'options-messages' => [
726 'invert' =>
'nsInvert',
727 'namespace_association' =>
'associated',
729 'section' =>
'contribs-top',
731 $fields[
'tagfilter'] = [
732 'type' =>
'tagfilter',
733 'cssclass' =>
'mw-tagfilter-input',
735 'label-message' => [
'tag-filter',
'parse' ],
736 'name' =>
'tagfilter',
738 'section' =>
'contribs-top',
741 if ( $this->permissionManager->userHasRight( $this->getUser(),
'deletedhistory' ) ) {
742 $fields[
'deletedOnly'] = [
744 'id' =>
'mw-show-deleted-only',
745 'label' => $this->msg(
'history-show-deleted' )->text(),
746 'name' =>
'deletedOnly',
747 'section' =>
'contribs-top',
751 $fields[
'topOnly'] = [
753 'id' =>
'mw-show-top-only',
754 'label' => $this->msg(
'sp-contributions-toponly' )->text(),
756 'section' =>
'contribs-top',
758 $fields[
'newOnly'] = [
760 'id' =>
'mw-show-new-only',
761 'label' => $this->msg(
'sp-contributions-newonly' )->text(),
763 'section' =>
'contribs-top',
765 $fields[
'hideMinor'] = [
767 'cssclass' =>
'mw-hide-minor-edits',
768 'id' =>
'mw-show-new-only',
769 'label' => $this->msg(
'sp-contributions-hideminor' )->text(),
770 'name' =>
'hideMinor',
771 'section' =>
'contribs-top',
776 $this->getHookRunner()->onSpecialContributions__getForm__filters(
777 $this, $rawFilters );
778 foreach ( $rawFilters as $filter ) {
780 if ( is_string( $filter ) ) {
783 'default' => $filter,
785 'section' =>
'contribs-top',
788 'A SpecialContributions::getForm::filters hook handler returned ' .
789 'an array of strings, this is deprecated since MediaWiki 1.33',
801 'id' =>
'mw-date-start',
802 'label' => $this->msg(
'date-range-from' )->text(),
804 'section' =>
'contribs-date',
809 'id' =>
'mw-date-end',
810 'label' => $this->msg(
'date-range-to' )->text(),
812 'section' =>
'contribs-date',
815 $htmlForm = HTMLForm::factory(
'ooui', $fields, $this->
getContext() );
818 ->setTitle( $this->getPageTitle() )
822 ->setCollapsibleOptions(
823 ( $pagerOptions[
'target'] ??
null ) ||
824 ( $pagerOptions[
'start'] ??
null ) ||
825 ( $pagerOptions[
'end'] ??
null )
828 ->setSubmitTextMsg(
'sp-contributions-submit' )
829 ->setWrapperLegendMsg(
'sp-contributions-search' );
831 $explain = $this->msg(
'sp-contributions-explain' );
832 if ( !$explain->isBlank() ) {
833 $htmlForm->addFooterText(
"<p id='mw-sp-contributions-explain'>{$explain->parse()}</p>" );
836 $htmlForm->prepareForm();
839 $htmlForm->setSubmitCallback(
static function () {
842 $result = $htmlForm->tryAuthorizedSubmit();
843 if ( !( $result ===
true || ( $result instanceof
Status && $result->
isGood() ) ) ) {
845 $htmlForm->setCollapsibleOptions(
false );
848 return $htmlForm->getHTML( $result );
860 $search = $this->userNameUtils->getCanonical( $search );
866 return $this->userNamePrefixSearch
867 ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset );
874 private function getPager( $targetUser ) {
875 if ( $this->pager ===
null ) {
877 'namespace' => $this->opts[
'namespace'],
878 'tagfilter' => $this->opts[
'tagfilter'],
879 'start' => $this->opts[
'start'] ??
'',
880 'end' => $this->opts[
'end'] ??
'',
881 'deletedOnly' => $this->opts[
'deletedOnly'],
882 'topOnly' => $this->opts[
'topOnly'],
883 'newOnly' => $this->opts[
'newOnly'],
884 'hideMinor' => $this->opts[
'hideMinor'],
885 'nsInvert' => $this->opts[
'nsInvert'],
886 'associated' => $this->opts[
'associated'],
892 $this->getLinkRenderer(),
893 $this->linkBatchFactory,
894 $this->getHookContainer(),
896 $this->actorMigration,
897 $this->revisionStore,
898 $this->namespaceInfo,
900 $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...
Shortcut to construct an includable special page.
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.
__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.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
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.
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.
isGood()
Returns whether the operation completed and didn't have any error or warnings.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Special page to allow managing user group membership.