MediaWiki REL1_37
SpecialContributions.php
Go to the documentation of this file.
1<?php
33use Wikimedia\IPUtils;
35
42 protected $opts;
43
46
49
52
55
58
61
64
67
70
72 private $pager = null;
73
85 public function __construct(
86 LinkBatchFactory $linkBatchFactory = null,
87 PermissionManager $permissionManager = null,
88 ILoadBalancer $loadBalancer = null,
89 ActorMigration $actorMigration = null,
90 RevisionStore $revisionStore = null,
91 NamespaceInfo $namespaceInfo = null,
92 UserNameUtils $userNameUtils = null,
93 UserNamePrefixSearch $userNamePrefixSearch = null,
95 ) {
96 parent::__construct( 'Contributions' );
97 // This class is extended and therefore falls back to global state - T269521
98 $services = MediaWikiServices::getInstance();
99 $this->linkBatchFactory = $linkBatchFactory ?? $services->getLinkBatchFactory();
100 $this->permissionManager = $permissionManager ?? $services->getPermissionManager();
101 $this->loadBalancer = $loadBalancer ?? $services->getDBLoadBalancer();
102 $this->actorMigration = $actorMigration ?? $services->getActorMigration();
103 $this->revisionStore = $revisionStore ?? $services->getRevisionStore();
104 $this->namespaceInfo = $namespaceInfo ?? $services->getNamespaceInfo();
105 $this->userNameUtils = $userNameUtils ?? $services->getUserNameUtils();
106 $this->userNamePrefixSearch = $userNamePrefixSearch ?? $services->getUserNamePrefixSearch();
107 $this->userOptionsLookup = $userOptionsLookup ?? $services->getUserOptionsLookup();
108 }
109
110 public function execute( $par ) {
111 $this->setHeaders();
112 $this->outputHeader();
113 $out = $this->getOutput();
114 // Modules required for viewing the list of contributions (also when included on other pages)
115 $out->addModuleStyles( [
116 'jquery.makeCollapsible.styles',
117 'mediawiki.interface.helpers.styles',
118 'mediawiki.special',
119 'mediawiki.special.changeslist',
120 ] );
121 $out->addModules( [
122 'mediawiki.special.recentchanges',
123 // Certain skins e.g. Minerva might have disabled this module.
124 'mediawiki.page.ready'
125 ] );
126 $this->addHelpLink( 'Help:User contributions' );
127
128 $this->opts = [];
129 $request = $this->getRequest();
130
131 $target = $par ?? $request->getVal( 'target' );
132
133 $this->opts['deletedOnly'] = $request->getBool( 'deletedOnly' );
134
135 if ( !strlen( $target ) ) {
136 if ( !$this->including() ) {
137 $out->addHTML( $this->getForm( $this->opts ) );
138 }
139
140 return;
141 }
142
143 $user = $this->getUser();
144
145 $this->opts['limit'] = $request->getInt( 'limit', $this->userOptionsLookup->getIntOption( $user, 'rclimit' ) );
146 $this->opts['target'] = $target;
147 $this->opts['topOnly'] = $request->getBool( 'topOnly' );
148 $this->opts['newOnly'] = $request->getBool( 'newOnly' );
149 $this->opts['hideMinor'] = $request->getBool( 'hideMinor' );
150
151 $ns = $request->getVal( 'namespace', null );
152 if ( $ns !== null && $ns !== '' && $ns !== 'all' ) {
153 $this->opts['namespace'] = intval( $ns );
154 } else {
155 $this->opts['namespace'] = '';
156 }
157
158 // Backwards compatibility: Before using OOUI form the old HTML form had
159 // fields for nsInvert and associated. These have now been replaced with the
160 // wpFilters query string parameters. These are retained to keep old URIs working.
161 $this->opts['associated'] = $request->getBool( 'associated' );
162 $this->opts['nsInvert'] = (bool)$request->getVal( 'nsInvert' );
163 $nsFilters = $request->getArray( 'wpfilters', null );
164 if ( $nsFilters !== null ) {
165 $this->opts['associated'] = in_array( 'associated', $nsFilters );
166 $this->opts['nsInvert'] = in_array( 'nsInvert', $nsFilters );
167 }
168
169 $this->opts['tagfilter'] = (string)$request->getVal( 'tagfilter' );
170
171 // Allows reverts to have the bot flag in recent changes. It is just here to
172 // be passed in the form at the top of the page
173 if ( $this->permissionManager->userHasRight( $user, 'markbotedits' ) && $request->getBool( 'bot' ) ) {
174 $this->opts['bot'] = '1';
175 }
176
177 $skip = $request->getText( 'offset' ) || $request->getText( 'dir' ) == 'prev';
178 # Offset overrides year/month selection
179 if ( !$skip ) {
180 $this->opts['year'] = $request->getVal( 'year' );
181 $this->opts['month'] = $request->getVal( 'month' );
182
183 $this->opts['start'] = $request->getVal( 'start' );
184 $this->opts['end'] = $request->getVal( 'end' );
185 }
186
187 $id = 0;
188 if ( ExternalUserNames::isExternal( $target ) ) {
189 $userObj = User::newFromName( $target, false );
190 if ( !$userObj ) {
191 $out->addHTML( $this->getForm( $this->opts ) );
192 return;
193 }
194
195 $out->addSubtitle( $this->contributionsSub( $userObj, $target ) );
196 $out->setPageTitle( $this->msg( 'contributions-title', $target )->escaped() );
197 } else {
198 $nt = Title::makeTitleSafe( NS_USER, $target );
199 if ( !$nt ) {
200 $out->addHTML( $this->getForm( $this->opts ) );
201 return;
202 }
203 $userObj = User::newFromName( $nt->getText(), false );
204 if ( !$userObj ) {
205 $out->addHTML( $this->getForm( $this->opts ) );
206 return;
207 }
208 $id = $userObj->getId();
209
210 $target = $nt->getText();
211 $out->addSubtitle( $this->contributionsSub( $userObj, $target ) );
212 $out->setPageTitle( $this->msg( 'contributions-title', $target )->escaped() );
213
214 # For IP ranges, we want the contributionsSub, but not the skin-dependent
215 # links under 'Tools', which may include irrelevant links like 'Logs'.
216 if ( !IPUtils::isValidRange( $target ) &&
217 ( $this->userNameUtils->isIP( $target ) || $userObj->isRegistered() )
218 ) {
219 // Don't add non-existent users, because hidden users
220 // that we add here will be removed later to pretend
221 // that they don't exist, and if users that actually don't
222 // exist are added here and then not removed, it exposes
223 // which users exist and are hidden vs. which actually don't
224 // exist. But, do set the relevant user for single IPs.
225 $this->getSkin()->setRelevantUser( $userObj );
226 }
227 }
228
229 $this->opts = ContribsPager::processDateFilter( $this->opts );
230
231 if ( $this->opts['namespace'] !== '' && $this->opts['namespace'] < NS_MAIN ) {
232 $this->getOutput()->wrapWikiMsg(
233 "<div class=\"mw-negative-namespace-not-supported error\">\n\$1\n</div>",
234 [ 'negative-namespace-not-supported' ]
235 );
236 $out->addHTML( $this->getForm( $this->opts ) );
237 return;
238 }
239
240 $feedType = $request->getVal( 'feed' );
241
242 $feedParams = [
243 'action' => 'feedcontributions',
244 'user' => $target,
245 ];
246 if ( $this->opts['topOnly'] ) {
247 $feedParams['toponly'] = true;
248 }
249 if ( $this->opts['newOnly'] ) {
250 $feedParams['newonly'] = true;
251 }
252 if ( $this->opts['hideMinor'] ) {
253 $feedParams['hideminor'] = true;
254 }
255 if ( $this->opts['deletedOnly'] ) {
256 $feedParams['deletedonly'] = true;
257 }
258 if ( $this->opts['tagfilter'] !== '' ) {
259 $feedParams['tagfilter'] = $this->opts['tagfilter'];
260 }
261 if ( $this->opts['namespace'] !== '' ) {
262 $feedParams['namespace'] = $this->opts['namespace'];
263 }
264 // Don't use year and month for the feed URL, but pass them on if
265 // we redirect to API (if $feedType is specified)
266 if ( $feedType && isset( $this->opts['year'] ) ) {
267 $feedParams['year'] = $this->opts['year'];
268 }
269 if ( $feedType && isset( $this->opts['month'] ) ) {
270 $feedParams['month'] = $this->opts['month'];
271 }
272
273 if ( $feedType ) {
274 // Maintain some level of backwards compatibility
275 // If people request feeds using the old parameters, redirect to API
276 $feedParams['feedformat'] = $feedType;
277 $url = wfAppendQuery( wfScript( 'api' ), $feedParams );
278
279 $out->redirect( $url, '301' );
280
281 return;
282 }
283
284 // Add RSS/atom links
285 $this->addFeedLinks( $feedParams );
286
287 if ( $this->getHookRunner()->onSpecialContributionsBeforeMainOutput(
288 $id, $userObj, $this )
289 ) {
290 if ( !$this->including() ) {
291 $out->addHTML( $this->getForm( $this->opts ) );
292 }
293 $pager = $this->getPager( $userObj );
294 if ( IPUtils::isValidRange( $target ) && !$pager->isQueryableRange( $target ) ) {
295 // Valid range, but outside CIDR limit.
296 $limits = $this->getConfig()->get( 'RangeContributionsCIDRLimit' );
297 $limit = $limits[ IPUtils::isIPv4( $target ) ? 'IPv4' : 'IPv6' ];
298 $out->addWikiMsg( 'sp-contributions-outofrange', $limit );
299 } else {
300 // @todo We just want a wiki ID here, not a "DB domain", but
301 // current status of MediaWiki conflates the two. See T235955.
302 $poolKey = $this->loadBalancer->getLocalDomainID() . ':SpecialContributions:';
303 if ( $this->getUser()->isAnon() ) {
304 $poolKey .= 'a:' . $this->getUser()->getName();
305 } else {
306 $poolKey .= 'u:' . $this->getUser()->getId();
307 }
308 $work = new PoolCounterWorkViaCallback( 'SpecialContributions', $poolKey, [
309 'doWork' => function () use ( $pager, $out, $target ) {
310 if ( !$pager->getNumRows() ) {
311 $out->addWikiMsg( 'nocontribs', $target );
312 } else {
313 # Show a message about replica DB lag, if applicable
314 $lag = $pager->getDatabase()->getSessionLagStatus()['lag'];
315 if ( $lag > 0 ) {
316 $out->showLagWarning( $lag );
317 }
318
319 $output = $pager->getBody();
320 if ( !$this->including() ) {
321 $output = $pager->getNavigationBar() .
322 $output .
324 }
325 $out->addHTML( $output );
326 }
327 },
328 'error' => function () use ( $out ) {
329 $msg = $this->getUser()->isAnon()
330 ? 'sp-contributions-concurrency-ip'
331 : 'sp-contributions-concurrency-user';
332 $out->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>", $msg );
333 }
334 ] );
335 $work->execute();
336 }
337
338 $out->preventClickjacking( $pager->getPreventClickjacking() );
339
340 # Show the appropriate "footer" message - WHOIS tools, etc.
341 if ( IPUtils::isValidRange( $target ) && $pager->isQueryableRange( $target ) ) {
342 $message = 'sp-contributions-footer-anon-range';
343 } elseif ( IPUtils::isIPAddress( $target ) ) {
344 $message = 'sp-contributions-footer-anon';
345 } elseif ( $userObj->isAnon() ) {
346 // No message for non-existing users
347 $message = '';
348 } elseif ( $userObj->isHidden() &&
349 !$this->permissionManager->userHasRight( $this->getUser(), 'hideuser' )
350 ) {
351 // User is registered, but make sure that the viewer can see them, to avoid
352 // having different behavior for missing and hidden users; see T120883
353 $message = '';
354 } else {
355 // Not hidden, or hidden but the viewer can still see it
356 $message = 'sp-contributions-footer';
357 }
358
359 if ( $message && !$this->including() && !$this->msg( $message, $target )->isDisabled() ) {
360 $out->wrapWikiMsg(
361 "<div class='mw-contributions-footer'>\n$1\n</div>",
362 [ $message, $target ] );
363 }
364 }
365 }
366
376 protected function contributionsSub( $userObj, $targetName ) {
377 $isAnon = $userObj->isAnon();
378 if ( !$isAnon && $userObj->isHidden() &&
379 !$this->permissionManager->userHasRight( $this->getUser(), 'hideuser' )
380 ) {
381 // T120883 if the user is hidden and the viewer cannot see hidden
382 // users, pretend like it does not exist at all.
383 $isAnon = true;
384 }
385
386 if ( $isAnon ) {
387 // Show a warning message that the user being searched for doesn't exist.
388 // UserNameUtils::isIP returns true for IP address and usemod IPs like '123.123.123.xxx',
389 // but returns false for IP ranges. We don't want to suggest either of these are
390 // valid usernames which we would with the 'contributions-userdoesnotexist' message.
391 if ( !$this->userNameUtils->isIP( $userObj->getName() )
392 && !IPUtils::isValidRange( $userObj->getName() )
393 ) {
394 $this->getOutput()->wrapWikiMsg(
395 "<div class=\"mw-userpage-userdoesnotexist error\">\n\$1\n</div>",
396 [
397 'contributions-userdoesnotexist',
398 wfEscapeWikiText( $userObj->getName() ),
399 ]
400 );
401 if ( !$this->including() ) {
402 $this->getOutput()->setStatusCode( 404 );
403 }
404 }
405 $user = htmlspecialchars( $userObj->getName() );
406 } else {
407 $user = $this->getLinkRenderer()->makeLink( $userObj->getUserPage(), $userObj->getName() );
408 }
409 $nt = $userObj->getUserPage();
410 $talk = $userObj->getTalkPage();
411 $links = '';
412
413 // T211910. Don't show action links if a range is outside block limit
414 $showForIp = IPUtils::isValid( $userObj ) ||
415 ( IPUtils::isValidRange( $userObj ) && $this->getPager( $userObj )->isQueryableRange( $userObj ) );
416
417 // T276306. if the user is hidden and the viewer cannot see hidden, pretend that it does not exist
418 $registeredAndVisible = $userObj->isRegistered() && ( !$userObj->isHidden()
419 || $this->permissionManager->userHasRight( $this->getUser(), 'hideuser' ) );
420
421 if ( $talk && ( $registeredAndVisible || $showForIp ) ) {
422 $tools = self::getUserLinks(
423 $this,
424 $userObj,
425 $this->permissionManager,
426 $this->getHookRunner()
427 );
428 $links = Html::openElement( 'span', [ 'class' => 'mw-changeslist-links' ] );
429 foreach ( $tools as $tool ) {
430 $links .= Html::rawElement( 'span', [], $tool ) . ' ';
431 }
432 $links = trim( $links ) . Html::closeElement( 'span' );
433
434 // Show a note if the user is blocked and display the last block log entry.
435 // Do not expose the autoblocks, since that may lead to a leak of accounts' IPs,
436 // and also this will display a totally irrelevant log entry as a current block.
437 if ( !$this->including() ) {
438 // For IP ranges you must give DatabaseBlock::newFromTarget the CIDR string
439 // and not a user object.
440 if ( IPUtils::isValidRange( $userObj->getName() ) ) {
441 $block = DatabaseBlock::newFromTarget( $userObj->getName(), $userObj->getName() );
442 } else {
443 $block = DatabaseBlock::newFromTarget( $userObj, $userObj );
444 }
445
446 if ( $block !== null && $block->getType() != DatabaseBlock::TYPE_AUTO ) {
447 if ( $block->getType() == DatabaseBlock::TYPE_RANGE ) {
448 $nt = $this->namespaceInfo->getCanonicalName( NS_USER )
449 . ':' . $block->getTargetName();
450 }
451
452 $out = $this->getOutput(); // showLogExtract() wants first parameter by reference
453 if ( $userObj->isAnon() ) {
454 $msgKey = $block->isSitewide() ?
455 'sp-contributions-blocked-notice-anon' :
456 'sp-contributions-blocked-notice-anon-partial';
457 } else {
458 $msgKey = $block->isSitewide() ?
459 'sp-contributions-blocked-notice' :
460 'sp-contributions-blocked-notice-partial';
461 }
462 // Allow local styling overrides for different types of block
463 $class = $block->isSitewide() ?
464 'mw-contributions-blocked-notice' :
465 'mw-contributions-blocked-notice-partial';
466 LogEventsList::showLogExtract(
467 $out,
468 'block',
469 $nt,
470 '',
471 [
472 'lim' => 1,
473 'showIfEmpty' => false,
474 'msgKey' => [
475 $msgKey,
476 $userObj->getName() # Support GENDER in 'sp-contributions-blocked-notice'
477 ],
478 'offset' => '', # don't use WebRequest parameter offset
479 'wrap' => Html::rawElement(
480 'div',
481 [ 'class' => $class ],
482 '$1'
483 ),
484 ]
485 );
486 }
487 }
488 }
489
490 return Html::rawElement( 'div', [ 'class' => 'mw-contributions-user-tools' ],
491 $this->msg( 'contributions-subtitle' )->rawParams( $user )->params( $userObj->getName() )
492 . ' ' . $links
493 );
494 }
495
506 public static function getUserLinks(
507 SpecialPage $sp,
508 User $target,
509 PermissionManager $permissionManager = null,
510 HookRunner $hookRunner = null
511 ) {
512 // Fallback to global state, if not provided
513 $permissionManager = $permissionManager ?? MediaWikiServices::getInstance()->getPermissionManager();
514 $hookRunner = $hookRunner ?? Hooks::runner();
515
516 $id = $target->getId();
517 $username = $target->getName();
518 $userpage = $target->getUserPage();
519 $talkpage = $target->getTalkPage();
520 $isIP = IPUtils::isValid( $username );
521 $isRange = IPUtils::isValidRange( $username );
522
523 $linkRenderer = $sp->getLinkRenderer();
524
525 $tools = [];
526 # No talk pages for IP ranges.
527 if ( !$isRange ) {
528 $tools['user-talk'] = $linkRenderer->makeLink(
529 $talkpage,
530 $sp->msg( 'sp-contributions-talk' )->text()
531 );
532 }
533
534 # Block / Change block / Unblock links
535 if ( $permissionManager->userHasRight( $sp->getUser(), 'block' ) ) {
536 if ( $target->getBlock() && $target->getBlock()->getType() != DatabaseBlock::TYPE_AUTO ) {
537 $tools['block'] = $linkRenderer->makeKnownLink( # Change block link
538 SpecialPage::getTitleFor( 'Block', $username ),
539 $sp->msg( 'change-blocklink' )->text()
540 );
541 $tools['unblock'] = $linkRenderer->makeKnownLink( # Unblock link
542 SpecialPage::getTitleFor( 'Unblock', $username ),
543 $sp->msg( 'unblocklink' )->text()
544 );
545 } else { # User is not blocked
546 $tools['block'] = $linkRenderer->makeKnownLink( # Block link
547 SpecialPage::getTitleFor( 'Block', $username ),
548 $sp->msg( 'blocklink' )->text()
549 );
550 }
551 }
552
553 # Block log link
554 $tools['log-block'] = $linkRenderer->makeKnownLink(
555 SpecialPage::getTitleFor( 'Log', 'block' ),
556 $sp->msg( 'sp-contributions-blocklog' )->text(),
557 [],
558 [ 'page' => $userpage->getPrefixedText() ]
559 );
560
561 # Suppression log link (T61120)
562 if ( $permissionManager->userHasRight( $sp->getUser(), 'suppressionlog' ) ) {
563 $tools['log-suppression'] = $linkRenderer->makeKnownLink(
564 SpecialPage::getTitleFor( 'Log', 'suppress' ),
565 $sp->msg( 'sp-contributions-suppresslog', $username )->text(),
566 [],
567 [ 'offender' => $username ]
568 );
569 }
570
571 # Don't show some links for IP ranges
572 if ( !$isRange ) {
573 # Uploads: hide if IPs cannot upload (T220674)
574 if ( !$isIP || $permissionManager->userHasRight( $target, 'upload' ) ) {
575 $tools['uploads'] = $linkRenderer->makeKnownLink(
576 SpecialPage::getTitleFor( 'Listfiles', $username ),
577 $sp->msg( 'sp-contributions-uploads' )->text()
578 );
579 }
580
581 # Other logs link
582 # Todo: T146628
583 $tools['logs'] = $linkRenderer->makeKnownLink(
584 SpecialPage::getTitleFor( 'Log', $username ),
585 $sp->msg( 'sp-contributions-logs' )->text()
586 );
587
588 # Add link to deleted user contributions for priviledged users
589 # Todo: T183457
590 if ( $permissionManager->userHasRight( $sp->getUser(), 'deletedhistory' ) ) {
591 $tools['deletedcontribs'] = $linkRenderer->makeKnownLink(
592 SpecialPage::getTitleFor( 'DeletedContributions', $username ),
593 $sp->msg( 'sp-contributions-deleted', $username )->text()
594 );
595 }
596 }
597
598 # Add a link to change user rights for privileged users
599 $userrightsPage = new UserrightsPage();
600 $userrightsPage->setContext( $sp->getContext() );
601 if ( $userrightsPage->userCanChangeRights( $target ) ) {
602 $tools['userrights'] = $linkRenderer->makeKnownLink(
603 SpecialPage::getTitleFor( 'Userrights', $username ),
604 $sp->msg( 'sp-contributions-userrights', $username )->text()
605 );
606 }
607
608 $hookRunner->onContributionsToolLinks( $id, $userpage, $tools, $sp );
609
610 return $tools;
611 }
612
619 protected function getForm( array $pagerOptions ) {
620 $this->opts['title'] = $this->getPageTitle()->getPrefixedText();
621 // Modules required only for the form
622 $this->getOutput()->addModules( [
623 'mediawiki.special.contributions',
624 ] );
625 $this->getOutput()->addModuleStyles( 'mediawiki.widgets.DateInputWidget.styles' );
626 $this->getOutput()->enableOOUI();
627 $fields = [];
628
629 # Add hidden params for tracking except for parameters in $skipParameters
630 $skipParameters = [
631 'namespace',
632 'nsInvert',
633 'deletedOnly',
634 'target',
635 'year',
636 'month',
637 'start',
638 'end',
639 'topOnly',
640 'newOnly',
641 'hideMinor',
642 'associated',
643 'tagfilter'
644 ];
645
646 foreach ( $this->opts as $name => $value ) {
647 if ( in_array( $name, $skipParameters ) ) {
648 continue;
649 }
650
651 $fields[$name] = [
652 'name' => $name,
653 'type' => 'hidden',
654 'default' => $value,
655 ];
656 }
657
658 $target = $this->opts['target'] ?? null;
659 $fields['target'] = [
660 'type' => 'user',
661 'default' => $target ?
662 str_replace( '_', ' ', $target ) : '' ,
663 'label' => $this->msg( 'sp-contributions-username' )->text(),
664 'name' => 'target',
665 'id' => 'mw-target-user-or-ip',
666 'size' => 40,
667 'autofocus' => !$target,
668 'section' => 'contribs-top',
669 ];
670
671 $ns = $this->opts['namespace'] ?? null;
672 $fields['namespace'] = [
673 'type' => 'namespaceselect',
674 'label' => $this->msg( 'namespace' )->text(),
675 'name' => 'namespace',
676 'cssclass' => 'namespaceselector',
677 'default' => $ns,
678 'id' => 'namespace',
679 'section' => 'contribs-top',
680 ];
681 $fields['nsFilters'] = [
682 'class' => HTMLMultiSelectField::class,
683 'label' => '',
684 'name' => 'wpfilters',
685 'flatlist' => true,
686 // Only shown when namespaces are selected.
687 'cssclass' => $ns === '' ?
688 'contribs-ns-filters mw-input-with-label mw-input-hidden' :
689 'contribs-ns-filters mw-input-with-label',
690 // `contribs-ns-filters` class allows these fields to be toggled on/off by JavaScript.
691 // See resources/src/mediawiki.special.recentchanges.js
692 'infusable' => true,
693 'options-messages' => [
694 'invert' => 'nsInvert',
695 'namespace_association' => 'associated',
696 ],
697 'section' => 'contribs-top',
698 ];
699 $fields['tagfilter'] = [
700 'type' => 'tagfilter',
701 'cssclass' => 'mw-tagfilter-input',
702 'id' => 'tagfilter',
703 'label-message' => [ 'tag-filter', 'parse' ],
704 'name' => 'tagfilter',
705 'size' => 20,
706 'section' => 'contribs-top',
707 ];
708
709 if ( $this->permissionManager->userHasRight( $this->getUser(), 'deletedhistory' ) ) {
710 $fields['deletedOnly'] = [
711 'type' => 'check',
712 'id' => 'mw-show-deleted-only',
713 'label' => $this->msg( 'history-show-deleted' )->text(),
714 'name' => 'deletedOnly',
715 'section' => 'contribs-top',
716 ];
717 }
718
719 $fields['topOnly'] = [
720 'type' => 'check',
721 'id' => 'mw-show-top-only',
722 'label' => $this->msg( 'sp-contributions-toponly' )->text(),
723 'name' => 'topOnly',
724 'section' => 'contribs-top',
725 ];
726 $fields['newOnly'] = [
727 'type' => 'check',
728 'id' => 'mw-show-new-only',
729 'label' => $this->msg( 'sp-contributions-newonly' )->text(),
730 'name' => 'newOnly',
731 'section' => 'contribs-top',
732 ];
733 $fields['hideMinor'] = [
734 'type' => 'check',
735 'cssclass' => 'mw-hide-minor-edits',
736 'id' => 'mw-show-new-only',
737 'label' => $this->msg( 'sp-contributions-hideminor' )->text(),
738 'name' => 'hideMinor',
739 'section' => 'contribs-top',
740 ];
741
742 // Allow additions at this point to the filters.
743 $rawFilters = [];
744 $this->getHookRunner()->onSpecialContributions__getForm__filters(
745 $this, $rawFilters );
746 foreach ( $rawFilters as $filter ) {
747 // Backwards compatibility support for previous hook function signature.
748 if ( is_string( $filter ) ) {
749 $fields[] = [
750 'type' => 'info',
751 'default' => $filter,
752 'raw' => true,
753 'section' => 'contribs-top',
754 ];
756 'A SpecialContributions::getForm::filters hook handler returned ' .
757 'an array of strings, this is deprecated since MediaWiki 1.33',
758 '1.33', false, false
759 );
760 } else {
761 // Preferred append method.
762 $fields[] = $filter;
763 }
764 }
765
766 $fields['start'] = [
767 'type' => 'date',
768 'default' => '',
769 'id' => 'mw-date-start',
770 'label' => $this->msg( 'date-range-from' )->text(),
771 'name' => 'start',
772 'section' => 'contribs-date',
773 ];
774 $fields['end'] = [
775 'type' => 'date',
776 'default' => '',
777 'id' => 'mw-date-end',
778 'label' => $this->msg( 'date-range-to' )->text(),
779 'name' => 'end',
780 'section' => 'contribs-date',
781 ];
782
783 $htmlForm = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
784 $htmlForm
785 ->setMethod( 'get' )
786 // When offset is defined, the user is paging through results
787 // so we hide the form by default to allow users to focus on browsing
788 // rather than defining search parameters
789 ->setCollapsibleOptions(
790 ( $pagerOptions['target'] ?? null ) ||
791 ( $pagerOptions['start'] ?? null ) ||
792 ( $pagerOptions['end'] ?? null )
793 )
794 ->setAction( wfScript() )
795 ->setSubmitTextMsg( 'sp-contributions-submit' )
796 ->setWrapperLegendMsg( 'sp-contributions-search' );
797
798 $explain = $this->msg( 'sp-contributions-explain' );
799 if ( !$explain->isBlank() ) {
800 $htmlForm->addFooterText( "<p id='mw-sp-contributions-explain'>{$explain->parse()}</p>" );
801 }
802
803 $htmlForm->loadData();
804
805 return $htmlForm->getHTML( false );
806 }
807
816 public function prefixSearchSubpages( $search, $limit, $offset ) {
817 $search = $this->userNameUtils->getCanonical( $search );
818 if ( !$search ) {
819 // No prefix suggestion for invalid user
820 return [];
821 }
822 // Autocomplete subpage as user list - public to allow caching
823 return $this->userNamePrefixSearch
824 ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset );
825 }
826
831 private function getPager( $targetUser ) {
832 if ( $this->pager === null ) {
833 $options = [
834 'namespace' => $this->opts['namespace'],
835 'tagfilter' => $this->opts['tagfilter'],
836 'start' => $this->opts['start'] ?? '',
837 'end' => $this->opts['end'] ?? '',
838 'deletedOnly' => $this->opts['deletedOnly'],
839 'topOnly' => $this->opts['topOnly'],
840 'newOnly' => $this->opts['newOnly'],
841 'hideMinor' => $this->opts['hideMinor'],
842 'nsInvert' => $this->opts['nsInvert'],
843 'associated' => $this->opts['associated'],
844 ];
845
846 $this->pager = new ContribsPager(
847 $this->getContext(),
848 $options,
849 $this->getLinkRenderer(),
850 $this->linkBatchFactory,
851 $this->getHookContainer(),
852 $this->loadBalancer,
853 $this->actorMigration,
854 $this->revisionStore,
855 $this->namespaceInfo,
856 $targetUser
857 );
858 }
859
860 return $this->pager;
861 }
862
863 protected function getGroupName() {
864 return 'users';
865 }
866}
UserOptionsLookup $userOptionsLookup
const NS_USER
Definition Defines.php:66
const NS_MAIN
Definition Defines.php:64
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
getContext()
if(ini_get('mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Definition Setup.php:88
This is not intended to be a long-term part of MediaWiki; it will be deprecated and removed once acto...
Pager for Special:Contributions.
getNavigationBar()
Wrap the navigation bar in a p element with identifying class.
isQueryableRange( $ipRange)
Is the given IP a range and within the CIDR limit?
Pre-librarized class name for IPUtils.
Definition IP.php:80
Shortcut to construct an includable special page.
getBody()
Get the formatted result list.
getNumRows()
Get the number of rows in the result set.
A DatabaseBlock (unlike a SystemBlock) is stored in the database, may give rise to autoblocks and may...
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
MediaWikiServices is the service locator for the application scope of MediaWiki.
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
Service for looking up page revisions.
Handles searching prefixes of user names.
UserNameUtils service.
Provides access to user options.
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.
ContribsPager null $pager
__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)
LinkBatchFactory $linkBatchFactory
UserOptionsLookup $userOptionsLookup
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.
PermissionManager $permissionManager
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
UserNamePrefixSearch $userNamePrefixSearch
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.
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.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getConfig()
Shortcut to get main config object.
getRequest()
Get the WebRequest being used for this instance.
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
including( $x=null)
Whether the special page is being evaluated via transclusion.
static newFromName( $name, $validate='valid')
Definition User.php:607
Special page to allow managing user group membership.
Database cluster connection, tracking, load balancing, and transaction manager interface.