MediaWiki REL1_39
SpecialContributions.php
Go to the documentation of this file.
1<?php
37use Wikimedia\IPUtils;
39
46 protected $opts;
47
49 private $linkBatchFactory;
50
52 private $permissionManager;
53
55 private $loadBalancer;
56
58 private $actorMigration;
59
61 private $revisionStore;
62
64 private $namespaceInfo;
65
67 private $userNameUtils;
68
70 private $userNamePrefixSearch;
71
73 private $userOptionsLookup;
74
76 private $commentFormatter;
77
79 private $userFactory;
80
82 private $pager = null;
83
97 public function __construct(
98 LinkBatchFactory $linkBatchFactory = null,
99 PermissionManager $permissionManager = null,
100 ILoadBalancer $loadBalancer = null,
101 ActorMigration $actorMigration = null,
102 RevisionStore $revisionStore = null,
103 NamespaceInfo $namespaceInfo = null,
104 UserNameUtils $userNameUtils = null,
105 UserNamePrefixSearch $userNamePrefixSearch = null,
106 UserOptionsLookup $userOptionsLookup = null,
107 CommentFormatter $commentFormatter = null,
108 UserFactory $userFactory = null
109 ) {
110 parent::__construct( 'Contributions' );
111 // This class is extended and therefore falls back to global state - T269521
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();
124 }
125
126 public function execute( $par ) {
127 $this->setHeaders();
128 $this->outputHeader();
129 $out = $this->getOutput();
130 // Modules required for viewing the list of contributions (also when included on other pages)
131 $out->addModuleStyles( [
132 'jquery.makeCollapsible.styles',
133 'mediawiki.interface.helpers.styles',
134 'mediawiki.special',
135 'mediawiki.special.changeslist',
136 ] );
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 $this->opts = [];
144 $request = $this->getRequest();
145
146 $target = $par ?? $request->getVal( 'target', '' );
147 '@phan-var string $target'; // getVal does not return null here
148
149 $this->opts['deletedOnly'] = $request->getBool( 'deletedOnly' );
150
151 if ( !strlen( $target ) ) {
152 if ( !$this->including() ) {
153 $out->addHTML( $this->getForm( $this->opts ) );
154 }
155
156 return;
157 }
158
159 $user = $this->getUser();
160
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' );
166
167 $ns = $request->getVal( 'namespace', null );
168 if ( $ns !== null && $ns !== '' && $ns !== 'all' ) {
169 $this->opts['namespace'] = intval( $ns );
170 } else {
171 $this->opts['namespace'] = '';
172 }
173
174 // Backwards compatibility: Before using OOUI form the old HTML form had
175 // fields for nsInvert and associated. These have now been replaced with the
176 // wpFilters query string parameters. These are retained to keep old URIs working.
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 );
183 }
184
185 $this->opts['tagfilter'] = array_filter( explode(
186 '|',
187 (string)$request->getVal( 'tagfilter' )
188 ), static function ( $el ) {
189 return $el !== '';
190 } );
191
192 // Allows reverts to have the bot flag in recent changes. It is just here to
193 // be passed in the form at the top of the page
194 if ( $this->permissionManager->userHasRight( $user, 'markbotedits' ) && $request->getBool( 'bot' ) ) {
195 $this->opts['bot'] = '1';
196 }
197
198 $skip = $request->getText( 'offset' ) || $request->getText( 'dir' ) == 'prev';
199 # Offset overrides year/month selection
200 if ( !$skip ) {
201 $this->opts['year'] = $request->getIntOrNull( 'year' );
202 $this->opts['month'] = $request->getIntOrNull( 'month' );
203
204 $this->opts['start'] = $request->getVal( 'start' );
205 $this->opts['end'] = $request->getVal( 'end' );
206 }
207
208 $id = 0;
209 if ( ExternalUserNames::isExternal( $target ) ) {
210 $userObj = $this->userFactory->newFromName( $target, UserRigorOptions::RIGOR_NONE );
211 if ( !$userObj ) {
212 $out->addHTML( $this->getForm( $this->opts ) );
213 return;
214 }
215
216 $out->addSubtitle( $this->contributionsSub( $userObj, $target ) );
217 $out->setPageTitle( $this->msg( 'contributions-title', $target )->escaped() );
218 } else {
219 $nt = Title::makeTitleSafe( NS_USER, $target );
220 if ( !$nt ) {
221 $out->addHTML( $this->getForm( $this->opts ) );
222 return;
223 }
224 $target = $nt->getText();
225 if ( IPUtils::isValidRange( $target ) ) {
226 $target = IPUtils::sanitizeRange( $target );
227 }
228 $userObj = $this->userFactory->newFromName( $target, UserRigorOptions::RIGOR_NONE );
229 if ( !$userObj ) {
230 $out->addHTML( $this->getForm( $this->opts ) );
231 return;
232 }
233 $id = $userObj->getId();
234
235 $out->addSubtitle( $this->contributionsSub( $userObj, $target ) );
236 $out->setPageTitle( $this->msg( 'contributions-title', $target )->escaped() );
237
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() )
242 ) {
243 // Don't add non-existent users, because hidden users
244 // that we add here will be removed later to pretend
245 // that they don't exist, and if users that actually don't
246 // exist are added here and then not removed, it exposes
247 // which users exist and are hidden vs. which actually don't
248 // exist. But, do set the relevant user for single IPs.
249 $this->getSkin()->setRelevantUser( $userObj );
250 }
251 }
252
253 $this->opts = ContribsPager::processDateFilter( $this->opts );
254
255 if ( $this->opts['namespace'] !== '' && $this->opts['namespace'] < NS_MAIN ) {
256 $this->getOutput()->wrapWikiMsg(
257 "<div class=\"mw-negative-namespace-not-supported error\">\n\$1\n</div>",
258 [ 'negative-namespace-not-supported' ]
259 );
260 $out->addHTML( $this->getForm( $this->opts ) );
261 return;
262 }
263
264 $feedType = $request->getVal( 'feed' );
265
266 $feedParams = [
267 'action' => 'feedcontributions',
268 'user' => $target,
269 ];
270 if ( $this->opts['topOnly'] ) {
271 $feedParams['toponly'] = true;
272 }
273 if ( $this->opts['newOnly'] ) {
274 $feedParams['newonly'] = true;
275 }
276 if ( $this->opts['hideMinor'] ) {
277 $feedParams['hideminor'] = true;
278 }
279 if ( $this->opts['deletedOnly'] ) {
280 $feedParams['deletedonly'] = true;
281 }
282
283 if ( $this->opts['tagfilter'] !== [] ) {
284 $feedParams['tagfilter'] = $this->opts['tagfilter'];
285 }
286 if ( $this->opts['namespace'] !== '' ) {
287 $feedParams['namespace'] = $this->opts['namespace'];
288 }
289 // Don't use year and month for the feed URL, but pass them on if
290 // we redirect to API (if $feedType is specified)
291 if ( $feedType && isset( $this->opts['year'] ) ) {
292 $feedParams['year'] = $this->opts['year'];
293 }
294 if ( $feedType && isset( $this->opts['month'] ) ) {
295 $feedParams['month'] = $this->opts['month'];
296 }
297
298 if ( $feedType ) {
299 // Maintain some level of backwards compatibility
300 // If people request feeds using the old parameters, redirect to API
301 $feedParams['feedformat'] = $feedType;
302 $url = wfAppendQuery( wfScript( 'api' ), $feedParams );
303
304 $out->redirect( $url, '301' );
305
306 return;
307 }
308
309 // Add RSS/atom links
310 $this->addFeedLinks( $feedParams );
311
312 if ( $this->getHookRunner()->onSpecialContributionsBeforeMainOutput(
313 $id, $userObj, $this )
314 ) {
315 if ( !$this->including() ) {
316 $out->addHTML( $this->getForm( $this->opts ) );
317 }
318 $pager = $this->getPager( $userObj );
319 if ( IPUtils::isValidRange( $target ) && !$pager->isQueryableRange( $target ) ) {
320 // Valid range, but outside CIDR limit.
321 $limits = $this->getConfig()->get( MainConfigNames::RangeContributionsCIDRLimit );
322 $limit = $limits[ IPUtils::isIPv4( $target ) ? 'IPv4' : 'IPv6' ];
323 $out->addWikiMsg( 'sp-contributions-outofrange', $limit );
324 } else {
325 // @todo We just want a wiki ID here, not a "DB domain", but
326 // current status of MediaWiki conflates the two. See T235955.
327 $poolKey = $this->loadBalancer->getLocalDomainID() . ':SpecialContributions:';
328 if ( $this->getUser()->isAnon() ) {
329 $poolKey .= 'a:' . $this->getUser()->getName();
330 } else {
331 $poolKey .= 'u:' . $this->getUser()->getId();
332 }
333 $work = new PoolCounterWorkViaCallback( 'SpecialContributions', $poolKey, [
334 'doWork' => function () use ( $pager, $out, $target ) {
335 if ( !$pager->getNumRows() ) {
336 $out->addWikiMsg( 'nocontribs', $target );
337 } else {
338 # Show a message about replica DB lag, if applicable
339 $lag = $pager->getDatabase()->getSessionLagStatus()['lag'];
340 if ( $lag > 0 ) {
341 $out->showLagWarning( $lag );
342 }
343
344 $output = $pager->getBody();
345 if ( !$this->including() ) {
346 $output = $pager->getNavigationBar() .
347 $output .
348 $pager->getNavigationBar();
349 }
350 $out->addHTML( $output );
351 }
352 },
353 'error' => function () use ( $out ) {
354 $msg = $this->getUser()->isAnon()
355 ? 'sp-contributions-concurrency-ip'
356 : 'sp-contributions-concurrency-user';
357 $out->addHTML(
358 Html::errorBox(
359 $out->msg( $msg )->parse()
360 )
361 );
362 }
363 ] );
364 $work->execute();
365 }
366
367 $out->setPreventClickjacking( $pager->getPreventClickjacking() );
368
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() ) {
375 // No message for non-existing users
376 $message = '';
377 } elseif ( $userObj->isHidden() &&
378 !$this->permissionManager->userHasRight( $this->getUser(), 'hideuser' )
379 ) {
380 // User is registered, but make sure that the viewer can see them, to avoid
381 // having different behavior for missing and hidden users; see T120883
382 $message = '';
383 } else {
384 // Not hidden, or hidden but the viewer can still see it
385 $message = 'sp-contributions-footer';
386 }
387
388 if ( $message && !$this->including() && !$this->msg( $message, $target )->isDisabled() ) {
389 $out->wrapWikiMsg(
390 "<div class='mw-contributions-footer'>\n$1\n</div>",
391 [ $message, $target ] );
392 }
393 }
394 }
395
405 protected function contributionsSub( $userObj, $targetName ) {
406 $isAnon = $userObj->isAnon();
407 if ( !$isAnon && $userObj->isHidden() &&
408 !$this->permissionManager->userHasRight( $this->getUser(), 'hideuser' )
409 ) {
410 // T120883 if the user is hidden and the viewer cannot see hidden
411 // users, pretend like it does not exist at all.
412 $isAnon = true;
413 }
414
415 if ( $isAnon ) {
416 // Show a warning message that the user being searched for doesn't exist.
417 // UserNameUtils::isIP returns true for IP address and usemod IPs like '123.123.123.xxx',
418 // but returns false for IP ranges. We don't want to suggest either of these are
419 // valid usernames which we would with the 'contributions-userdoesnotexist' message.
420 if ( !$this->userNameUtils->isIP( $userObj->getName() )
421 && !IPUtils::isValidRange( $userObj->getName() )
422 ) {
423 $this->getOutput()->addHtml( Html::warningBox(
424 $this->getOutput()->msg( 'contributions-userdoesnotexist',
425 wfEscapeWikiText( $userObj->getName() ) )->parse(),
426 'mw-userpage-userdoesnotexist'
427 ) );
428 if ( !$this->including() ) {
429 $this->getOutput()->setStatusCode( 404 );
430 }
431 }
432 $user = htmlspecialchars( $userObj->getName() );
433 } else {
434 $user = $this->getLinkRenderer()->makeLink( $userObj->getUserPage(), $userObj->getName() );
435 }
436 $nt = $userObj->getUserPage();
437 $talk = $userObj->getTalkPage();
438 $links = '';
439
440 // T211910. Don't show action links if a range is outside block limit
441 $showForIp = IPUtils::isValid( $userObj ) ||
442 ( IPUtils::isValidRange( $userObj ) && $this->getPager( $userObj )->isQueryableRange( $userObj ) );
443
444 // T276306. if the user is hidden and the viewer cannot see hidden, pretend that it does not exist
445 $registeredAndVisible = $userObj->isRegistered() && ( !$userObj->isHidden()
446 || $this->permissionManager->userHasRight( $this->getUser(), 'hideuser' ) );
447
448 if ( $talk && ( $registeredAndVisible || $showForIp ) ) {
449 $tools = self::getUserLinks(
450 $this,
451 $userObj,
452 $this->permissionManager,
453 $this->getHookRunner()
454 );
455 $links = Html::openElement( 'span', [ 'class' => 'mw-changeslist-links' ] );
456 foreach ( $tools as $tool ) {
457 $links .= Html::rawElement( 'span', [], $tool ) . ' ';
458 }
459 $links = trim( $links ) . Html::closeElement( 'span' );
460
461 // Show a note if the user is blocked and display the last block log entry.
462 // Do not expose the autoblocks, since that may lead to a leak of accounts' IPs,
463 // and also this will display a totally irrelevant log entry as a current block.
464 if ( !$this->including() ) {
465 // For IP ranges you must give DatabaseBlock::newFromTarget the CIDR string
466 // and not a user object.
467 if ( IPUtils::isValidRange( $userObj->getName() ) ) {
468 $block = DatabaseBlock::newFromTarget( $userObj->getName(), $userObj->getName() );
469 } else {
470 $block = DatabaseBlock::newFromTarget( $userObj, $userObj );
471 }
472
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();
477 }
478
479 $out = $this->getOutput(); // showLogExtract() wants first parameter by reference
480 if ( $userObj->isAnon() ) {
481 $msgKey = $block->isSitewide() ?
482 'sp-contributions-blocked-notice-anon' :
483 'sp-contributions-blocked-notice-anon-partial';
484 } else {
485 $msgKey = $block->isSitewide() ?
486 'sp-contributions-blocked-notice' :
487 'sp-contributions-blocked-notice-partial';
488 }
489 // Allow local styling overrides for different types of block
490 $class = $block->isSitewide() ?
491 'mw-contributions-blocked-notice' :
492 'mw-contributions-blocked-notice-partial';
493 LogEventsList::showLogExtract(
494 $out,
495 'block',
496 $nt,
497 '',
498 [
499 'lim' => 1,
500 'showIfEmpty' => false,
501 'msgKey' => [
502 $msgKey,
503 $userObj->getName() # Support GENDER in 'sp-contributions-blocked-notice'
504 ],
505 'offset' => '', # don't use WebRequest parameter offset
506 'wrap' => Html::rawElement(
507 'div',
508 [ 'class' => $class ],
509 '$1'
510 ),
511 ]
512 );
513 }
514 }
515 }
516
517 return Html::rawElement( 'div', [ 'class' => 'mw-contributions-user-tools' ],
518 $this->msg( 'contributions-subtitle' )->rawParams( $user )->params( $userObj->getName() )
519 . ' ' . $links
520 );
521 }
522
533 public static function getUserLinks(
534 SpecialPage $sp,
535 User $target,
536 PermissionManager $permissionManager = null,
537 HookRunner $hookRunner = null
538 ) {
539 // Fallback to global state, if not provided
540 $permissionManager = $permissionManager ?? MediaWikiServices::getInstance()->getPermissionManager();
541 $hookRunner = $hookRunner ?? Hooks::runner();
542
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 );
549
550 $linkRenderer = $sp->getLinkRenderer();
551
552 $tools = [];
553 # No talk pages for IP ranges.
554 if ( !$isRange ) {
555 $tools['user-talk'] = $linkRenderer->makeLink(
556 $talkpage,
557 $sp->msg( 'sp-contributions-talk' )->text(),
558 [ 'class' => 'mw-contributions-link-talk' ]
559 );
560 }
561
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' ]
569 );
570 $tools['unblock'] = $linkRenderer->makeKnownLink( # Unblock link
571 SpecialPage::getTitleFor( 'Unblock', $username ),
572 $sp->msg( 'unblocklink' )->text(),
573 [ 'class' => 'mw-contributions-link-unblock' ]
574 );
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' ]
580 );
581 }
582 }
583
584 # Block log link
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() ]
590 );
591
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 ]
599 );
600 }
601
602 # Don't show some links for IP ranges
603 if ( !$isRange ) {
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' ]
610 );
611 }
612
613 # Other logs link
614 # Todo: T146628
615 $tools['logs'] = $linkRenderer->makeKnownLink(
616 SpecialPage::getTitleFor( 'Log', $username ),
617 $sp->msg( 'sp-contributions-logs' )->text(),
618 [ 'class' => 'mw-contributions-link-logs' ]
619 );
620
621 # Add link to deleted user contributions for privileged users
622 # Todo: T183457
623 if ( $permissionManager->userHasRight( $sp->getUser(), 'deletedhistory' ) ) {
624 $tools['deletedcontribs'] = $linkRenderer->makeKnownLink(
625 SpecialPage::getTitleFor( 'DeletedContributions', $username ),
626 $sp->msg( 'sp-contributions-deleted', $username )->text(),
627 [ 'class' => 'mw-contributions-link-deleted-contribs' ]
628 );
629 }
630 }
631
632 # Add a link to change user rights for privileged users
633 $userrightsPage = new UserrightsPage();
634 $userrightsPage->setContext( $sp->getContext() );
635 if ( $userrightsPage->userCanChangeRights( $target ) ) {
636 $tools['userrights'] = $linkRenderer->makeKnownLink(
637 SpecialPage::getTitleFor( 'Userrights', $username ),
638 $sp->msg( 'sp-contributions-userrights', $username )->text(),
639 [ 'class' => 'mw-contributions-link-user-rights' ]
640 );
641 }
642
643 $hookRunner->onContributionsToolLinks( $id, $userpage, $tools, $sp );
644
645 return $tools;
646 }
647
654 protected function getForm( array $pagerOptions ) {
655 // Modules required only for the form
656 $this->getOutput()->addModules( [
657 'mediawiki.special.contributions',
658 ] );
659 $this->getOutput()->addModuleStyles( 'mediawiki.widgets.DateInputWidget.styles' );
660 $this->getOutput()->enableOOUI();
661 $fields = [];
662
663 # Add hidden params for tracking except for parameters in $skipParameters
664 $skipParameters = [
665 'namespace',
666 'nsInvert',
667 'deletedOnly',
668 'target',
669 'year',
670 'month',
671 'start',
672 'end',
673 'topOnly',
674 'newOnly',
675 'hideMinor',
676 'associated',
677 'tagfilter',
678 'title',
679 ];
680
681 foreach ( $this->opts as $name => $value ) {
682 if ( in_array( $name, $skipParameters ) ) {
683 continue;
684 }
685
686 $fields[$name] = [
687 'name' => $name,
688 'type' => 'hidden',
689 'default' => $value,
690 ];
691 }
692
693 $target = $this->opts['target'] ?? null;
694 $fields['target'] = [
695 'type' => 'user',
696 'default' => $target ?
697 str_replace( '_', ' ', $target ) : '' ,
698 'label' => $this->msg( 'sp-contributions-username' )->text(),
699 'name' => 'target',
700 'id' => 'mw-target-user-or-ip',
701 'size' => 40,
702 'autofocus' => !$target,
703 'section' => 'contribs-top',
704 'ipallowed' => true,
705 'iprange' => true,
706 ];
707
708 $ns = $this->opts['namespace'] ?? null;
709 $fields['namespace'] = [
710 'type' => 'namespaceselect',
711 'label' => $this->msg( 'namespace' )->text(),
712 'name' => 'namespace',
713 'cssclass' => 'namespaceselector',
714 'default' => $ns,
715 'id' => 'namespace',
716 'section' => 'contribs-top',
717 ];
718 $fields['nsFilters'] = [
719 'class' => HTMLMultiSelectField::class,
720 'label' => '',
721 'name' => 'wpfilters',
722 'flatlist' => true,
723 // Only shown when namespaces are selected.
724 'hide-if' => [ '===', 'namespace', 'all' ],
725 'options-messages' => [
726 'invert' => 'nsInvert',
727 'namespace_association' => 'associated',
728 ],
729 'section' => 'contribs-top',
730 ];
731 $fields['tagfilter'] = [
732 'type' => 'tagfilter',
733 'cssclass' => 'mw-tagfilter-input',
734 'id' => 'tagfilter',
735 'label-message' => [ 'tag-filter', 'parse' ],
736 'name' => 'tagfilter',
737 'size' => 20,
738 'section' => 'contribs-top',
739 ];
740
741 if ( $this->permissionManager->userHasRight( $this->getUser(), 'deletedhistory' ) ) {
742 $fields['deletedOnly'] = [
743 'type' => 'check',
744 'id' => 'mw-show-deleted-only',
745 'label' => $this->msg( 'history-show-deleted' )->text(),
746 'name' => 'deletedOnly',
747 'section' => 'contribs-top',
748 ];
749 }
750
751 $fields['topOnly'] = [
752 'type' => 'check',
753 'id' => 'mw-show-top-only',
754 'label' => $this->msg( 'sp-contributions-toponly' )->text(),
755 'name' => 'topOnly',
756 'section' => 'contribs-top',
757 ];
758 $fields['newOnly'] = [
759 'type' => 'check',
760 'id' => 'mw-show-new-only',
761 'label' => $this->msg( 'sp-contributions-newonly' )->text(),
762 'name' => 'newOnly',
763 'section' => 'contribs-top',
764 ];
765 $fields['hideMinor'] = [
766 'type' => 'check',
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',
772 ];
773
774 // Allow additions at this point to the filters.
775 $rawFilters = [];
776 $this->getHookRunner()->onSpecialContributions__getForm__filters(
777 $this, $rawFilters );
778 foreach ( $rawFilters as $filter ) {
779 // Backwards compatibility support for previous hook function signature.
780 if ( is_string( $filter ) ) {
781 $fields[] = [
782 'type' => 'info',
783 'default' => $filter,
784 'raw' => true,
785 'section' => 'contribs-top',
786 ];
788 'A SpecialContributions::getForm::filters hook handler returned ' .
789 'an array of strings, this is deprecated since MediaWiki 1.33',
790 '1.33', false, false
791 );
792 } else {
793 // Preferred append method.
794 $fields[] = $filter;
795 }
796 }
797
798 $fields['start'] = [
799 'type' => 'date',
800 'default' => '',
801 'id' => 'mw-date-start',
802 'label' => $this->msg( 'date-range-from' )->text(),
803 'name' => 'start',
804 'section' => 'contribs-date',
805 ];
806 $fields['end'] = [
807 'type' => 'date',
808 'default' => '',
809 'id' => 'mw-date-end',
810 'label' => $this->msg( 'date-range-to' )->text(),
811 'name' => 'end',
812 'section' => 'contribs-date',
813 ];
814
815 $htmlForm = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
816 $htmlForm
817 ->setMethod( 'get' )
818 ->setTitle( $this->getPageTitle() )
819 // When offset is defined, the user is paging through results
820 // so we hide the form by default to allow users to focus on browsing
821 // rather than defining search parameters
822 ->setCollapsibleOptions(
823 ( $pagerOptions['target'] ?? null ) ||
824 ( $pagerOptions['start'] ?? null ) ||
825 ( $pagerOptions['end'] ?? null )
826 )
827 ->setAction( wfScript() )
828 ->setSubmitTextMsg( 'sp-contributions-submit' )
829 ->setWrapperLegendMsg( 'sp-contributions-search' );
830
831 $explain = $this->msg( 'sp-contributions-explain' );
832 if ( !$explain->isBlank() ) {
833 $htmlForm->addFooterText( "<p id='mw-sp-contributions-explain'>{$explain->parse()}</p>" );
834 }
835
836 $htmlForm->prepareForm();
837
838 // Submission is handled elsewhere, but do this to check for and display errors
839 $htmlForm->setSubmitCallback( static function () {
840 return true;
841 } );
842 $result = $htmlForm->tryAuthorizedSubmit();
843 if ( !( $result === true || ( $result instanceof Status && $result->isGood() ) ) ) {
844 // Uncollapse if there are errors
845 $htmlForm->setCollapsibleOptions( false );
846 }
847
848 return $htmlForm->getHTML( $result );
849 }
850
859 public function prefixSearchSubpages( $search, $limit, $offset ) {
860 $search = $this->userNameUtils->getCanonical( $search );
861 if ( !$search ) {
862 // No prefix suggestion for invalid user
863 return [];
864 }
865 // Autocomplete subpage as user list - public to allow caching
866 return $this->userNamePrefixSearch
867 ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset );
868 }
869
874 private function getPager( $targetUser ) {
875 if ( $this->pager === null ) {
876 $options = [
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'],
887 ];
888
889 $this->pager = new ContribsPager(
890 $this->getContext(),
891 $options,
892 $this->getLinkRenderer(),
893 $this->linkBatchFactory,
894 $this->getHookContainer(),
895 $this->loadBalancer,
896 $this->actorMigration,
897 $this->revisionStore,
898 $this->namespaceInfo,
899 $targetUser,
900 $this->commentFormatter
901 );
902 }
903
904 return $this->pager;
905 }
906
907 protected function getGroupName() {
908 return 'users';
909 }
910}
getUser()
const NS_USER
Definition Defines.php:66
const NS_MAIN
Definition Defines.php:64
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,...
getContext()
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Definition WebStart.php:82
This is not intended to be a long-term part of MediaWiki; it will be deprecated and removed once acto...
Pager for Special:Contributions.
isQueryableRange( $ipRange)
Is the given IP a range and within the CIDR limit?
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 is the main service interface for converting single-line comments from various DB comment fields...
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
Service for looking up page revisions.
Creates User objects.
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.
__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.
Definition Status.php:44
Special page to allow managing user group membership.
Shared interface for rigor levels when dealing with User methods.
Create and track the database connections and transactions for a given database cluster.