MediaWiki master
ContributionsSpecialPage.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\SpecialPage;
25
52use Wikimedia\IPUtils;
54
71 protected $opts = [];
72 protected $formErrors = false;
73
83
97 public function __construct(
107 $name,
108 $restriction = ''
109 ) {
110 parent::__construct( $name, $restriction );
111 $this->permissionManager = $permissionManager;
112 $this->dbProvider = $dbProvider;
113 $this->namespaceInfo = $namespaceInfo;
114 $this->userNameUtils = $userNameUtils;
115 $this->userNamePrefixSearch = $userNamePrefixSearch;
116 $this->userOptionsLookup = $userOptionsLookup;
117 $this->userFactory = $userFactory;
118 $this->userIdentityLookup = $userIdentityLookup;
119 $this->blockStore = $blockStore;
120 }
121
125 public function execute( $par ) {
126 $this->setHeaders();
127 $this->outputHeader();
128 $out = $this->getOutput();
129 // Modules required for viewing the list of contributions (also when included on other pages)
130 $out->addModuleStyles( [
131 'jquery.makeCollapsible.styles',
132 'mediawiki.interface.helpers.styles',
133 'mediawiki.special',
134 'mediawiki.special.changeslist',
135 ] );
136 $out->addBodyClasses( 'mw-special-ContributionsSpecialPage' );
137 $out->addModules( [
138 // Certain skins e.g. Minerva might have disabled this module.
139 'mediawiki.page.ready'
140 ] );
141 $this->addHelpLink( 'Help:User contributions' );
142
143 $request = $this->getRequest();
144
145 $target = $par ?? $request->getVal( 'target', '' );
146 '@phan-var string $target'; // getVal does not return null here
147
148 $this->opts['deletedOnly'] = $request->getBool( 'deletedOnly' );
149
150 if ( !strlen( $target ) ) {
151 $out->addHTML( $this->getForm( $this->opts ) );
152
153 return;
154 }
155
156 $user = $this->getUser();
157
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' );
163
164 $ns = $request->getVal( 'namespace', null );
165 if ( $ns !== null && $ns !== '' && $ns !== 'all' ) {
166 $this->opts['namespace'] = intval( $ns );
167 } else {
168 $this->opts['namespace'] = '';
169 }
170
171 // Backwards compatibility: Before using OOUI form the old HTML form had
172 // fields for nsInvert and associated. These have now been replaced with the
173 // wpFilters query string parameters. These are retained to keep old URIs working.
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 );
180 }
181
182 $this->opts['tagfilter'] = array_filter( explode(
183 '|',
184 (string)$request->getVal( 'tagfilter' )
185 ), static function ( $el ) {
186 return $el !== '';
187 } );
188 $this->opts['tagInvert'] = $request->getBool( 'tagInvert' );
189
190 // Allows reverts to have the bot flag in recent changes. It is just here to
191 // be passed in the form at the top of the page
192 if ( $this->permissionManager->userHasRight( $user, 'markbotedits' ) && $request->getBool( 'bot' ) ) {
193 $this->opts['bot'] = '1';
194 }
195
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' );
200
201 $notExternal = !ExternalUserNames::isExternal( $target );
202 if ( $notExternal ) {
203 $nt = Title::makeTitleSafe( NS_USER, $target );
204 if ( !$nt ) {
205 $out->addHTML( $this->getForm( $this->opts ) );
206 return;
207 }
208 $target = $nt->getText();
209 if ( IPUtils::isValidRange( $target ) ) {
210 $target = IPUtils::sanitizeRange( $target );
211 }
212 }
213
214 $userObj = $this->userFactory->newFromName( $target, UserRigorOptions::RIGOR_NONE );
215 if ( !$userObj ) {
216 $out->addHTML( $this->getForm( $this->opts ) );
217 return;
218 }
219 $out->addSubtitle( $this->contributionsSub( $userObj, $target ) );
220 $out->setPageTitleMsg( $this->msg( $this->getResultsPageTitleMessageKey(), $target ) );
221
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() )
226 ) {
227 // Don't add non-existent users, because hidden users
228 // that we add here will be removed later to pretend
229 // that they don't exist, and if users that actually don't
230 // exist are added here and then not removed, it exposes
231 // which users exist and are hidden vs. which actually don't
232 // exist. But, do set the relevant user for single IPs.
233 $this->getSkin()->setRelevantUser( $userObj );
234 }
235
236 $this->opts = ContribsPager::processDateFilter( $this->opts );
237
238 if ( $this->opts['namespace'] !== '' && $this->opts['namespace'] < NS_MAIN ) {
239 $this->getOutput()->wrapWikiMsg(
240 "<div class=\"mw-negative-namespace-not-supported error\">\n\$1\n</div>",
241 [ 'negative-namespace-not-supported' ]
242 );
243 $out->addHTML( $this->getForm( $this->opts ) );
244 return;
245 }
246
247 if ( $this->providesFeeds() ) {
248 $feedType = $request->getVal( 'feed' );
249
250 $feedParams = [
251 'action' => 'feedcontributions',
252 'user' => $target,
253 ];
254 if ( $this->opts['topOnly'] ) {
255 $feedParams['toponly'] = true;
256 }
257 if ( $this->opts['newOnly'] ) {
258 $feedParams['newonly'] = true;
259 }
260 if ( $this->opts['hideMinor'] ) {
261 $feedParams['hideminor'] = true;
262 }
263 if ( $this->opts['deletedOnly'] ) {
264 $feedParams['deletedonly'] = true;
265 }
266
267 if ( $this->opts['tagfilter'] !== [] ) {
268 $feedParams['tagfilter'] = $this->opts['tagfilter'];
269 }
270 if ( $this->opts['namespace'] !== '' ) {
271 $feedParams['namespace'] = $this->opts['namespace'];
272 }
273 // Don't use year and month for the feed URL, but pass them on if
274 // we redirect to API (if $feedType is specified)
275 if ( $feedType && isset( $this->opts['year'] ) ) {
276 $feedParams['year'] = $this->opts['year'];
277 }
278 if ( $feedType && isset( $this->opts['month'] ) ) {
279 $feedParams['month'] = $this->opts['month'];
280 }
281
282 if ( $feedType ) {
283 // Maintain some level of backwards compatibility
284 // If people request feeds using the old parameters, redirect to API
285 $feedParams['feedformat'] = $feedType;
286 $url = wfAppendQuery( wfScript( 'api' ), $feedParams );
287
288 $out->redirect( $url, '301' );
289
290 return;
291 }
292
293 // Add RSS/atom links
294 $this->addFeedLinks( $feedParams );
295 }
296
297 if ( $this->getHookRunner()->onSpecialContributionsBeforeMainOutput(
298 $notExternal ? $userObj->getId() : 0, $userObj, $this )
299 ) {
300 $out->addHTML( $this->getForm( $this->opts ) );
301 if ( $this->formErrors ) {
302 return;
303 }
304 // We want a pure UserIdentity for imported actors, so the first letter
305 // of them is in lowercase and queryable.
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() )
311 ) {
312 // Valid range, but outside CIDR limit.
314 $limit = $limits[ IPUtils::isIPv4( $target ) ? 'IPv4' : 'IPv6' ];
315 $out->addWikiMsg( 'sp-contributions-outofrange', $limit );
316 } else {
317 // @todo We just want a wiki ID here, not a "DB domain", but
318 // current status of MediaWiki conflates the two. See T235955.
319 $poolKey = $this->dbProvider->getReplicaDatabase()->getDomainID() . ':Special' . $this->mName . ':';
320 if ( $this->getUser()->isAnon() ) {
321 $poolKey .= 'a:' . $this->getUser()->getName();
322 } else {
323 $poolKey .= 'u:' . $this->getUser()->getId();
324 }
325 $work = new PoolCounterWorkViaCallback( 'Special' . $this->mName, $poolKey, [
326 'doWork' => function () use ( $pager, $out, $target ) {
327 if ( !$pager->getNumRows() ) {
328 $out->addWikiMsg( 'nocontribs', $target );
329 } else {
330 # Show a message about replica DB lag, if applicable
331 $lag = $pager->getDatabase()->getSessionLagStatus()['lag'];
332 if ( $lag > 0 ) {
333 $out->showLagWarning( $lag );
334 }
335
336 $output = $pager->getBody();
337 if ( !$this->including() ) {
338 $output = $pager->getNavigationBar() .
339 $output .
340 $pager->getNavigationBar();
341 }
342 $out->addHTML( $output );
343 }
344 },
345 'error' => function () use ( $out ) {
346 $msg = $this->getUser()->isAnon()
347 ? 'sp-contributions-concurrency-ip'
348 : 'sp-contributions-concurrency-user';
349 $out->addHTML(
350 Html::errorBox(
351 $out->msg( $msg )->parse()
352 )
353 );
354 }
355 ] );
356 $work->execute();
357 }
358
359 $out->setPreventClickjacking( $pager->getPreventClickjacking() );
360
361 # Show the appropriate "footer" message - WHOIS tools, etc.
362 if ( IPUtils::isValidRange( $target ) &&
363 ContribsPager::isQueryableRange( $target, $this->getConfig() )
364 ) {
365 $message = 'sp-contributions-footer-anon-range';
366 } elseif ( IPUtils::isIPAddress( $target ) ) {
367 $message = 'sp-contributions-footer-anon';
368 } elseif ( $userObj->isAnon() ) {
369 // No message for non-existing users
370 $message = '';
371 } elseif ( $userObj->isHidden() &&
372 !$this->permissionManager->userHasRight( $this->getUser(), 'hideuser' )
373 ) {
374 // User is registered, but make sure that the viewer can't see them, to avoid
375 // having different behavior for missing and hidden users; see T120883
376 $message = '';
377 } else {
378 // Not hidden, or hidden but the viewer can still see it
379 $message = 'sp-contributions-footer';
380 }
381
382 if ( $message && !$this->including() && !$this->msg( $message, $target )->isDisabled() ) {
383 $out->wrapWikiMsg(
384 "<div class='mw-contributions-footer'>\n$1\n</div>",
385 [ $message, $target ] );
386 }
387 }
388 }
389
399 protected function contributionsSub( $userObj, $targetName ) {
400 $isAnon = $userObj->isAnon();
401 if ( !$isAnon && $userObj->isHidden() &&
402 !$this->permissionManager->userHasRight( $this->getUser(), 'hideuser' )
403 ) {
404 // T120883 if the user is hidden and the viewer cannot see hidden
405 // users, pretend like it does not exist at all.
406 $isAnon = true;
407 }
408
409 if ( $isAnon ) {
410 // Show a warning message that the user being searched for doesn't exist.
411 // UserNameUtils::isIP returns true for IP address and usemod IPs like '123.123.123.xxx',
412 // but returns false for IP ranges. We don't want to suggest either of these are
413 // valid usernames which we would with the 'contributions-userdoesnotexist' message.
414 if ( !$this->userNameUtils->isIP( $userObj->getName() )
415 && !IPUtils::isValidRange( $userObj->getName() )
416 ) {
417 $this->getOutput()->addHTML( Html::warningBox(
418 $this->getOutput()->msg( 'contributions-userdoesnotexist',
419 wfEscapeWikiText( $userObj->getName() ) )->parse(),
420 'mw-userpage-userdoesnotexist'
421 ) );
422 if ( !$this->including() ) {
423 $this->getOutput()->setStatusCode( 404 );
424 }
425 }
426 $user = htmlspecialchars( $userObj->getName() );
427 } else {
428 $user = $this->getLinkRenderer()->makeLink( $userObj->getUserPage(), $userObj->getName() );
429 }
430 $nt = $userObj->getUserPage();
431 $talk = $userObj->getTalkPage();
432 $links = '';
433
434 // T211910. Don't show action links if a range is outside block limit
435 $showForIp = IPUtils::isValid( $userObj ) ||
436 ( IPUtils::isValidRange( $userObj ) && ContribsPager::isQueryableRange( $userObj, $this->getConfig() ) );
437
438 // T276306. if the user is hidden and the viewer cannot see hidden, pretend that it does not exist
439 $registeredAndVisible = $userObj->isRegistered() && ( !$userObj->isHidden()
440 || $this->permissionManager->userHasRight( $this->getUser(), 'hideuser' ) );
441
442 $shouldShowLinks = $talk && ( $registeredAndVisible || $showForIp );
443 if ( $shouldShowLinks ) {
444 $tools = self::getUserLinks(
445 $this,
446 $userObj,
447 $this->permissionManager,
448 $this->getHookRunner()
449 );
450 $links = Html::openElement( 'span', [ 'class' => 'mw-changeslist-links' ] );
451 foreach ( $tools as $tool ) {
452 $links .= Html::rawElement( 'span', [], $tool ) . ' ';
453 }
454 $links = trim( $links ) . Html::closeElement( 'span' );
455
456 // Show a note if the user is blocked and display the last block log entry.
457 // Do not expose the autoblocks, since that may lead to a leak of accounts' IPs,
458 // and also this will display a totally irrelevant log entry as a current block.
459 $shouldShowBlocks = !$this->including();
460 if ( $shouldShowBlocks ) {
461 // For IP ranges you must give DatabaseBlock::newFromTarget the CIDR string
462 // and not a user object.
463 if ( IPUtils::isValidRange( $userObj->getName() ) ) {
464 $block = $this->blockStore
465 ->newFromTarget( $userObj->getName(), $userObj->getName() );
466 } else {
467 $block = $this->blockStore->newFromTarget( $userObj, $userObj );
468 }
469
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();
474 }
475
476 $out = $this->getOutput(); // showLogExtract() wants first parameter by reference
477 if ( $userObj->isAnon() ) {
478 $msgKey = $block->isSitewide() ?
479 'sp-contributions-blocked-notice-anon' :
480 'sp-contributions-blocked-notice-anon-partial';
481 } else {
482 $msgKey = $block->isSitewide() ?
483 'sp-contributions-blocked-notice' :
484 'sp-contributions-blocked-notice-partial';
485 }
486 // Allow local styling overrides for different types of block
487 $class = $block->isSitewide() ?
488 'mw-contributions-blocked-notice' :
489 'mw-contributions-blocked-notice-partial';
490 LogEventsList::showLogExtract(
491 $out,
492 'block',
493 $nt,
494 '',
495 [
496 'lim' => 1,
497 'showIfEmpty' => false,
498 'msgKey' => [
499 $msgKey,
500 $userObj->getName() # Support GENDER in 'sp-contributions-blocked-notice'
501 ],
502 'offset' => '', # don't use WebRequest parameter offset
503 'wrap' => Html::rawElement(
504 'div',
505 [ 'class' => $class ],
506 '$1'
507 ),
508 ]
509 );
510 }
511 }
512 }
513
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 )
518 . ' ' . $links
519 );
520
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 )
527 ->escaped();
528
529 $accountCreationDate = $userObj->getRegistration();
530 if ( $accountCreationDate ) {
531 $date = $this->getLanguage()->date( $accountCreationDate, true );
532 $userInfo .= $this->msg( 'word-separator' )
533 ->escaped();
534 $userInfo .= $this->msg( 'contributions-account-creation-date' )
535 ->plaintextParams( $date )
536 ->escaped();
537 }
538
539 $subHeadingsHtml .= Html::rawElement(
540 'div',
541 [ 'class' => 'mw-contributions-editor-info' ],
542 $userInfo
543 );
544 }
545
546 return $subHeadingsHtml;
547 }
548
559 public static function getUserLinks(
560 SpecialPage $sp,
561 User $target,
562 PermissionManager $permissionManager = null,
563 HookRunner $hookRunner = null
564 ) {
565 // Fallback to global state, if not provided
566 $permissionManager ??= MediaWikiServices::getInstance()->getPermissionManager();
567 $hookRunner ??= new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
568
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 );
575
576 $linkRenderer = $sp->getLinkRenderer();
577
578 $tools = [];
579 # No talk pages for IP ranges.
580 if ( !$isRange ) {
581 $tools['user-talk'] = $linkRenderer->makeLink(
582 $talkpage,
583 $sp->msg( 'sp-contributions-talk' )->text(),
584 [ 'class' => 'mw-contributions-link-talk' ]
585 );
586 }
587
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' ]
595 );
596 $tools['unblock'] = $linkRenderer->makeKnownLink( # Unblock link
597 SpecialPage::getTitleFor( 'Unblock', $username ),
598 $sp->msg( 'unblocklink' )->text(),
599 [ 'class' => 'mw-contributions-link-unblock' ]
600 );
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' ]
606 );
607 }
608 }
609
610 # Block log link
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() ]
616 );
617
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 ]
625 );
626 }
627
628 # Don't show some links for IP ranges
629 if ( !$isRange ) {
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' ]
636 );
637 }
638
639 # Other logs link
640 # Todo: T146628
641 $tools['logs'] = $linkRenderer->makeKnownLink(
642 SpecialPage::getTitleFor( 'Log', $username ),
643 $sp->msg( 'sp-contributions-logs' )->text(),
644 [ 'class' => 'mw-contributions-link-logs' ]
645 );
646
647 # Add link to deleted user contributions for privileged users
648 # Todo: T183457
649 if ( $permissionManager->userHasRight( $sp->getUser(), 'deletedhistory' ) ) {
650 $tools['deletedcontribs'] = $linkRenderer->makeKnownLink(
651 SpecialPage::getTitleFor( 'DeletedContributions', $username ),
652 $sp->msg( 'sp-contributions-deleted', $username )->text(),
653 [ 'class' => 'mw-contributions-link-deleted-contribs' ]
654 );
655 }
656 }
657
658 # Add a link to change user rights for privileged users
659 $userrightsPage = new SpecialUserRights();
660 $userrightsPage->setContext( $sp->getContext() );
661 if ( $userrightsPage->userCanChangeRights( $target ) ) {
662 $tools['userrights'] = $linkRenderer->makeKnownLink(
663 SpecialPage::getTitleFor( 'Userrights', $username ),
664 $sp->msg( 'sp-contributions-userrights', $username )->text(),
665 [ 'class' => 'mw-contributions-link-user-rights' ]
666 );
667 }
668
669 # Add a link to rename the user
670 if ( $id && $permissionManager->userHasRight( $sp->getUser(), 'renameuser' ) && !$target->isTemp() ) {
671 $tools['renameuser'] = $sp->getLinkRenderer()->makeKnownLink(
672 SpecialPage::getTitleFor( 'Renameuser' ),
673 $sp->msg( 'renameuser-linkoncontribs', $userpage->getText() )->text(),
674 [ 'title' => $sp->msg( 'renameuser-linkoncontribs-text', $userpage->getText() )->parse() ],
675 [ 'oldusername' => $userpage->getText() ]
676 );
677 }
678
679 $hookRunner->onContributionsToolLinks( $id, $userpage, $tools, $sp );
680
681 return $tools;
682 }
683
690 protected function getTargetField( $target ) {
691 return [
692 'type' => 'user',
693 'default' => str_replace( '_', ' ', $target ),
694 'label' => $this->msg( 'sp-contributions-username' )->text(),
695 'name' => 'target',
696 'id' => 'mw-target-user-or-ip',
697 'size' => 40,
698 'autofocus' => $target === '',
699 'section' => 'contribs-top',
700 'ipallowed' => true,
701 'iprange' => true,
702 'external' => true,
703 'required' => true,
704 ];
705 }
706
713 protected function getForm( array $pagerOptions ) {
714 if ( $this->including() ) {
715 // Do not show a form when special page is included in wikitext
716 return '';
717 }
718
719 // Modules required only for the form
720 $this->getOutput()->addModules( [
721 'mediawiki.special.contributions',
722 ] );
723 $this->getOutput()->enableOOUI();
724 $fields = [];
725
726 # Add hidden params for tracking except for parameters in $skipParameters
727 $skipParameters = [
728 'namespace',
729 'nsInvert',
730 'deletedOnly',
731 'target',
732 'year',
733 'month',
734 'start',
735 'end',
736 'topOnly',
737 'newOnly',
738 'hideMinor',
739 'associated',
740 'tagfilter',
741 'tagInvert',
742 'title',
743 ];
744
745 foreach ( $this->opts as $name => $value ) {
746 if ( in_array( $name, $skipParameters ) ) {
747 continue;
748 }
749
750 $fields[$name] = [
751 'name' => $name,
752 'type' => 'hidden',
753 'default' => $value,
754 ];
755 }
756
757 $target = $this->opts['target'] ?? '';
758 $fields['target'] = $this->getTargetField( $target );
759
760 $ns = $this->opts['namespace'] ?? null;
761 $fields['namespace'] = [
762 'type' => 'namespaceselect',
763 'label' => $this->msg( 'namespace' )->text(),
764 'name' => 'namespace',
765 'cssclass' => 'namespaceselector',
766 'default' => $ns,
767 'id' => 'namespace',
768 'section' => 'contribs-top',
769 ];
770 $fields['nsFilters'] = [
771 'class' => HTMLMultiSelectField::class,
772 'label' => '',
773 'name' => 'wpfilters',
774 'flatlist' => true,
775 // Only shown when namespaces are selected.
776 'hide-if' => [ '===', 'namespace', 'all' ],
777 'options-messages' => [
778 'invert' => 'nsInvert',
779 'namespace_association' => 'associated',
780 ],
781 'section' => 'contribs-top',
782 ];
783 $fields['tagfilter'] = [
784 'type' => 'tagfilter',
785 'cssclass' => 'mw-tagfilter-input',
786 'id' => 'tagfilter',
787 'label-message' => [ 'tag-filter', 'parse' ],
788 'name' => 'tagfilter',
789 'size' => 20,
790 'section' => 'contribs-top',
791 ];
792 $fields['tagInvert'] = [
793 'type' => 'check',
794 'id' => 'tagInvert',
795 'label' => $this->msg( 'invert' ),
796 'name' => 'tagInvert',
797 'hide-if' => [ '===', 'tagfilter', '' ],
798 'section' => 'contribs-top',
799 ];
800
801 if ( $this->permissionManager->userHasRight( $this->getUser(), 'deletedhistory' ) ) {
802 $fields['deletedOnly'] = [
803 'type' => 'check',
804 'id' => 'mw-show-deleted-only',
805 'label' => $this->msg( 'history-show-deleted' )->text(),
806 'name' => 'deletedOnly',
807 'section' => 'contribs-top',
808 ];
809 }
810
811 $fields['topOnly'] = [
812 'type' => 'check',
813 'id' => 'mw-show-top-only',
814 'label' => $this->msg( 'sp-contributions-toponly' )->text(),
815 'name' => 'topOnly',
816 'section' => 'contribs-top',
817 ];
818 $fields['newOnly'] = [
819 'type' => 'check',
820 'id' => 'mw-show-new-only',
821 'label' => $this->msg( 'sp-contributions-newonly' )->text(),
822 'name' => 'newOnly',
823 'section' => 'contribs-top',
824 ];
825 $fields['hideMinor'] = [
826 'type' => 'check',
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',
832 ];
833
834 // Allow additions at this point to the filters.
835 $rawFilters = [];
836 $this->getHookRunner()->onSpecialContributions__getForm__filters(
837 $this, $rawFilters );
838 foreach ( $rawFilters as $filter ) {
839 // Backwards compatibility support for previous hook function signature.
840 if ( is_string( $filter ) ) {
841 $fields[] = [
842 'type' => 'info',
843 'default' => $filter,
844 'raw' => true,
845 'section' => 'contribs-top',
846 ];
848 'A SpecialContributions::getForm::filters hook handler returned ' .
849 'an array of strings, this is deprecated since MediaWiki 1.33',
850 '1.33', false, false
851 );
852 } else {
853 // Preferred append method.
854 $fields[] = $filter;
855 }
856 }
857
858 $fields['start'] = [
859 'type' => 'date',
860 'default' => '',
861 'id' => 'mw-date-start',
862 'label' => $this->msg( 'date-range-from' )->text(),
863 'name' => 'start',
864 'section' => 'contribs-date',
865 ];
866 $fields['end'] = [
867 'type' => 'date',
868 'default' => '',
869 'id' => 'mw-date-end',
870 'label' => $this->msg( 'date-range-to' )->text(),
871 'name' => 'end',
872 'section' => 'contribs-date',
873 ];
874
875 $htmlForm = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
876 $htmlForm
877 ->setMethod( 'get' )
878 ->setTitle( $this->getPageTitle() )
879 // When offset is defined, the user is paging through results
880 // so we hide the form by default to allow users to focus on browsing
881 // rather than defining search parameters
882 ->setCollapsibleOptions(
883 ( $pagerOptions['target'] ?? null ) ||
884 ( $pagerOptions['start'] ?? null ) ||
885 ( $pagerOptions['end'] ?? null )
886 )
887 ->setAction( wfScript() )
888 ->setSubmitTextMsg( 'sp-contributions-submit' )
889 ->setWrapperLegendMsg( $this->getFormWrapperLegendMessageKey() );
890
891 $htmlForm->prepareForm();
892
893 // Submission is handled elsewhere, but do this to check for and display errors
894 $htmlForm->setSubmitCallback( static function () {
895 return true;
896 } );
897 $result = $htmlForm->tryAuthorizedSubmit();
898 if ( !( $result === true || ( $result instanceof Status && $result->isGood() ) ) ) {
899 // Uncollapse if there are errors
900 $htmlForm->setCollapsibleOptions( false );
901 $this->formErrors = true;
902 }
903
904 return $htmlForm->getHTML( $result );
905 }
906
915 public function prefixSearchSubpages( $search, $limit, $offset ) {
916 $search = $this->userNameUtils->getCanonical( $search );
917 if ( !$search ) {
918 // No prefix suggestion for invalid user
919 return [];
920 }
921 // Autocomplete subpage as user list - public to allow caching
922 return $this->userNamePrefixSearch
923 ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset );
924 }
925
929 protected function providesFeeds() {
930 return true;
931 }
932
937 protected function getPager( $targetUser ) {
938 // TODO: This class and the classes it extends should be abstract, and this
939 // method should be abstract.
940 throw new \LogicException( __METHOD__ . " must be overridden" );
941 }
942
946 protected function getGroupName() {
947 return 'users';
948 }
949
953 protected function getFormWrapperLegendMessageKey() {
954 return 'sp-contributions-search';
955 }
956
960 protected function getResultsPageTitleMessageKey() {
961 return 'contributions-title';
962 }
963}
getUser()
const NS_USER
Definition Defines.php:67
const NS_MAIN
Definition Defines.php:65
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.
getContext()
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:81
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:208
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
A class containing constants representing the names of configuration variables.
const RangeContributionsCIDRLimit
Name constant for the RangeContributionsCIDRLimit setting, for use with Config::get()
Service locator for MediaWiki core services.
Pager for Special:Contributions.
Pager for Special:Contributions.
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
Convenience class for dealing with PoolCounter using callbacks.
execute( $skipcache=false)
Get the result of the work (whatever it is), or the result of the error() function.
__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.
getTargetField( $target)
Get the target field for the form.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
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.
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.
Special page to allow managing user group membership.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Represents a title within MediaWiki.
Definition Title.php:79
Class to parse and build external user names.
Provides access to user options.
Creates User objects.
Handles searching prefixes of user names.
UserNameUtils service.
internal since 1.36
Definition User.php:93
isGood()
Returns whether the operation completed and didn't have any error or warnings.
Represents a block that may prevent users from performing specific operations.
Definition Block.php:45
Interface for objects representing user identity.
Shared interface for rigor levels when dealing with User methods.
Provide primary and replica IDatabase connections.