]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/specials/SpecialContributions.php
MediaWiki 1.30.2 renames
[autoinstallsdev/mediawiki.git] / includes / specials / SpecialContributions.php
1 <?php
2 /**
3  * Implements Special:Contributions
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  * http://www.gnu.org/copyleft/gpl.html
19  *
20  * @file
21  * @ingroup SpecialPage
22  */
23
24 use MediaWiki\Widget\DateInputWidget;
25
26 /**
27  * Special:Contributions, show user contributions in a paged list
28  *
29  * @ingroup SpecialPage
30  */
31 class SpecialContributions extends IncludableSpecialPage {
32         protected $opts;
33
34         public function __construct() {
35                 parent::__construct( 'Contributions' );
36         }
37
38         public function execute( $par ) {
39                 $this->setHeaders();
40                 $this->outputHeader();
41                 $out = $this->getOutput();
42                 $out->addModuleStyles( [
43                         'mediawiki.special',
44                         'mediawiki.special.changeslist',
45                         'mediawiki.widgets.DateInputWidget.styles',
46                 ] );
47                 $out->addModules( 'mediawiki.special.contributions' );
48                 $this->addHelpLink( 'Help:User contributions' );
49                 $out->enableOOUI();
50
51                 $this->opts = [];
52                 $request = $this->getRequest();
53
54                 if ( $par !== null ) {
55                         $target = $par;
56                 } else {
57                         $target = $request->getVal( 'target' );
58                 }
59
60                 if ( $request->getVal( 'contribs' ) == 'newbie' || $par === 'newbies' ) {
61                         $target = 'newbies';
62                         $this->opts['contribs'] = 'newbie';
63                 } else {
64                         $this->opts['contribs'] = 'user';
65                 }
66
67                 $this->opts['deletedOnly'] = $request->getBool( 'deletedOnly' );
68
69                 if ( !strlen( $target ) ) {
70                         if ( !$this->including() ) {
71                                 $out->addHTML( $this->getForm() );
72                         }
73
74                         return;
75                 }
76
77                 $user = $this->getUser();
78
79                 $this->opts['limit'] = $request->getInt( 'limit', $user->getOption( 'rclimit' ) );
80                 $this->opts['target'] = $target;
81                 $this->opts['topOnly'] = $request->getBool( 'topOnly' );
82                 $this->opts['newOnly'] = $request->getBool( 'newOnly' );
83                 $this->opts['hideMinor'] = $request->getBool( 'hideMinor' );
84
85                 $nt = Title::makeTitleSafe( NS_USER, $target );
86                 if ( !$nt ) {
87                         $out->addHTML( $this->getForm() );
88
89                         return;
90                 }
91                 $userObj = User::newFromName( $nt->getText(), false );
92                 if ( !$userObj ) {
93                         $out->addHTML( $this->getForm() );
94
95                         return;
96                 }
97                 $id = $userObj->getId();
98
99                 if ( $this->opts['contribs'] != 'newbie' ) {
100                         $target = $nt->getText();
101                         $out->addSubtitle( $this->contributionsSub( $userObj ) );
102                         $out->setHTMLTitle( $this->msg(
103                                 'pagetitle',
104                                 $this->msg( 'contributions-title', $target )->plain()
105                         )->inContentLanguage() );
106
107                         # For IP ranges, we want the contributionsSub, but not the skin-dependent
108                         # links under 'Tools', which may include irrelevant links like 'Logs'.
109                         if ( !IP::isValidRange( $target ) ) {
110                                 $this->getSkin()->setRelevantUser( $userObj );
111                         }
112                 } else {
113                         $out->addSubtitle( $this->msg( 'sp-contributions-newbies-sub' ) );
114                         $out->setHTMLTitle( $this->msg(
115                                 'pagetitle',
116                                 $this->msg( 'sp-contributions-newbies-title' )->plain()
117                         )->inContentLanguage() );
118                 }
119
120                 $ns = $request->getVal( 'namespace', null );
121                 if ( $ns !== null && $ns !== '' ) {
122                         $this->opts['namespace'] = intval( $ns );
123                 } else {
124                         $this->opts['namespace'] = '';
125                 }
126
127                 $this->opts['associated'] = $request->getBool( 'associated' );
128                 $this->opts['nsInvert'] = (bool)$request->getVal( 'nsInvert' );
129                 $this->opts['tagfilter'] = (string)$request->getVal( 'tagfilter' );
130
131                 // Allows reverts to have the bot flag in recent changes. It is just here to
132                 // be passed in the form at the top of the page
133                 if ( $user->isAllowed( 'markbotedits' ) && $request->getBool( 'bot' ) ) {
134                         $this->opts['bot'] = '1';
135                 }
136
137                 $skip = $request->getText( 'offset' ) || $request->getText( 'dir' ) == 'prev';
138                 # Offset overrides year/month selection
139                 if ( !$skip ) {
140                         $this->opts['year'] = $request->getVal( 'year' );
141                         $this->opts['month'] = $request->getVal( 'month' );
142
143                         $this->opts['start'] = $request->getVal( 'start' );
144                         $this->opts['end'] = $request->getVal( 'end' );
145                 }
146                 $this->opts = ContribsPager::processDateFilter( $this->opts );
147
148                 $feedType = $request->getVal( 'feed' );
149
150                 $feedParams = [
151                         'action' => 'feedcontributions',
152                         'user' => $target,
153                 ];
154                 if ( $this->opts['topOnly'] ) {
155                         $feedParams['toponly'] = true;
156                 }
157                 if ( $this->opts['newOnly'] ) {
158                         $feedParams['newonly'] = true;
159                 }
160                 if ( $this->opts['hideMinor'] ) {
161                         $feedParams['hideminor'] = true;
162                 }
163                 if ( $this->opts['deletedOnly'] ) {
164                         $feedParams['deletedonly'] = true;
165                 }
166                 if ( $this->opts['tagfilter'] !== '' ) {
167                         $feedParams['tagfilter'] = $this->opts['tagfilter'];
168                 }
169                 if ( $this->opts['namespace'] !== '' ) {
170                         $feedParams['namespace'] = $this->opts['namespace'];
171                 }
172                 // Don't use year and month for the feed URL, but pass them on if
173                 // we redirect to API (if $feedType is specified)
174                 if ( $feedType && $this->opts['year'] !== null ) {
175                         $feedParams['year'] = $this->opts['year'];
176                 }
177                 if ( $feedType && $this->opts['month'] !== null ) {
178                         $feedParams['month'] = $this->opts['month'];
179                 }
180
181                 if ( $feedType ) {
182                         // Maintain some level of backwards compatibility
183                         // If people request feeds using the old parameters, redirect to API
184                         $feedParams['feedformat'] = $feedType;
185                         $url = wfAppendQuery( wfScript( 'api' ), $feedParams );
186
187                         $out->redirect( $url, '301' );
188
189                         return;
190                 }
191
192                 // Add RSS/atom links
193                 $this->addFeedLinks( $feedParams );
194
195                 if ( Hooks::run( 'SpecialContributionsBeforeMainOutput', [ $id, $userObj, $this ] ) ) {
196                         if ( !$this->including() ) {
197                                 $out->addHTML( $this->getForm() );
198                         }
199                         $pager = new ContribsPager( $this->getContext(), [
200                                 'target' => $target,
201                                 'contribs' => $this->opts['contribs'],
202                                 'namespace' => $this->opts['namespace'],
203                                 'tagfilter' => $this->opts['tagfilter'],
204                                 'start' => $this->opts['start'],
205                                 'end' => $this->opts['end'],
206                                 'deletedOnly' => $this->opts['deletedOnly'],
207                                 'topOnly' => $this->opts['topOnly'],
208                                 'newOnly' => $this->opts['newOnly'],
209                                 'hideMinor' => $this->opts['hideMinor'],
210                                 'nsInvert' => $this->opts['nsInvert'],
211                                 'associated' => $this->opts['associated'],
212                         ] );
213
214                         if ( IP::isValidRange( $target ) && !$pager->isQueryableRange( $target ) ) {
215                                 // Valid range, but outside CIDR limit.
216                                 $limits = $this->getConfig()->get( 'RangeContributionsCIDRLimit' );
217                                 $limit = $limits[ IP::isIPv4( $target ) ? 'IPv4' : 'IPv6' ];
218                                 $out->addWikiMsg( 'sp-contributions-outofrange', $limit );
219                         } elseif ( !$pager->getNumRows() ) {
220                                 $out->addWikiMsg( 'nocontribs', $target );
221                         } else {
222                                 # Show a message about replica DB lag, if applicable
223                                 $lag = wfGetLB()->safeGetLag( $pager->getDatabase() );
224                                 if ( $lag > 0 ) {
225                                         $out->showLagWarning( $lag );
226                                 }
227
228                                 $output = $pager->getBody();
229                                 if ( !$this->including() ) {
230                                         $output = '<p>' . $pager->getNavigationBar() . '</p>' .
231                                                 $output .
232                                                 '<p>' . $pager->getNavigationBar() . '</p>';
233                                 }
234                                 $out->addHTML( $output );
235                         }
236
237                         $out->preventClickjacking( $pager->getPreventClickjacking() );
238
239                         # Show the appropriate "footer" message - WHOIS tools, etc.
240                         if ( $this->opts['contribs'] == 'newbie' ) {
241                                 $message = 'sp-contributions-footer-newbies';
242                         } elseif ( IP::isValidRange( $target ) ) {
243                                 $message = 'sp-contributions-footer-anon-range';
244                         } elseif ( IP::isIPAddress( $target ) ) {
245                                 $message = 'sp-contributions-footer-anon';
246                         } elseif ( $userObj->isAnon() ) {
247                                 // No message for non-existing users
248                                 $message = '';
249                         } else {
250                                 $message = 'sp-contributions-footer';
251                         }
252
253                         if ( $message ) {
254                                 if ( !$this->including() ) {
255                                         if ( !$this->msg( $message, $target )->isDisabled() ) {
256                                                 $out->wrapWikiMsg(
257                                                         "<div class='mw-contributions-footer'>\n$1\n</div>",
258                                                         [ $message, $target ] );
259                                         }
260                                 }
261                         }
262                 }
263         }
264
265         /**
266          * Generates the subheading with links
267          * @param User $userObj User object for the target
268          * @return string Appropriately-escaped HTML to be output literally
269          * @todo FIXME: Almost the same as getSubTitle in SpecialDeletedContributions.php.
270          * Could be combined.
271          */
272         protected function contributionsSub( $userObj ) {
273                 if ( $userObj->isAnon() ) {
274                         // Show a warning message that the user being searched for doesn't exists.
275                         // User::isIP returns true for IP address and usemod IPs like '123.123.123.xxx',
276                         // but returns false for IP ranges. We don't want to suggest either of these are
277                         // valid usernames which we would with the 'contributions-userdoesnotexist' message.
278                         if ( !User::isIP( $userObj->getName() ) && !$userObj->isIPRange() ) {
279                                 $this->getOutput()->wrapWikiMsg(
280                                         "<div class=\"mw-userpage-userdoesnotexist error\">\n\$1\n</div>",
281                                         [
282                                                 'contributions-userdoesnotexist',
283                                                 wfEscapeWikiText( $userObj->getName() ),
284                                         ]
285                                 );
286                                 if ( !$this->including() ) {
287                                         $this->getOutput()->setStatusCode( 404 );
288                                 }
289                         }
290                         $user = htmlspecialchars( $userObj->getName() );
291                 } else {
292                         $user = $this->getLinkRenderer()->makeLink( $userObj->getUserPage(), $userObj->getName() );
293                 }
294                 $nt = $userObj->getUserPage();
295                 $talk = $userObj->getTalkPage();
296                 $links = '';
297                 if ( $talk ) {
298                         $tools = self::getUserLinks( $this, $userObj );
299                         $links = $this->getLanguage()->pipeList( $tools );
300
301                         // Show a note if the user is blocked and display the last block log entry.
302                         // Do not expose the autoblocks, since that may lead to a leak of accounts' IPs,
303                         // and also this will display a totally irrelevant log entry as a current block.
304                         if ( !$this->including() ) {
305                                 // For IP ranges you must give Block::newFromTarget the CIDR string and not a user object.
306                                 if ( $userObj->isIPRange() ) {
307                                         $block = Block::newFromTarget( $userObj->getName(), $userObj->getName() );
308                                 } else {
309                                         $block = Block::newFromTarget( $userObj, $userObj );
310                                 }
311
312                                 if ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) {
313                                         if ( $block->getType() == Block::TYPE_RANGE ) {
314                                                 $nt = MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget();
315                                         }
316
317                                         $out = $this->getOutput(); // showLogExtract() wants first parameter by reference
318                                         LogEventsList::showLogExtract(
319                                                 $out,
320                                                 'block',
321                                                 $nt,
322                                                 '',
323                                                 [
324                                                         'lim' => 1,
325                                                         'showIfEmpty' => false,
326                                                         'msgKey' => [
327                                                                 $userObj->isAnon() ?
328                                                                         'sp-contributions-blocked-notice-anon' :
329                                                                         'sp-contributions-blocked-notice',
330                                                                 $userObj->getName() # Support GENDER in 'sp-contributions-blocked-notice'
331                                                         ],
332                                                         'offset' => '' # don't use WebRequest parameter offset
333                                                 ]
334                                         );
335                                 }
336                         }
337                 }
338
339                 return $this->msg( 'contribsub2' )->rawParams( $user, $links )->params( $userObj->getName() );
340         }
341
342         /**
343          * Links to different places.
344          *
345          * @note This function is also called in DeletedContributionsPage
346          * @param SpecialPage $sp SpecialPage instance, for context
347          * @param User $target Target user object
348          * @return array
349          */
350         public static function getUserLinks( SpecialPage $sp, User $target ) {
351                 $id = $target->getId();
352                 $username = $target->getName();
353                 $userpage = $target->getUserPage();
354                 $talkpage = $target->getTalkPage();
355
356                 $linkRenderer = $sp->getLinkRenderer();
357
358                 # No talk pages for IP ranges.
359                 if ( !IP::isValidRange( $username ) ) {
360                         $tools['user-talk'] = $linkRenderer->makeLink(
361                                 $talkpage,
362                                 $sp->msg( 'sp-contributions-talk' )->text()
363                         );
364                 }
365
366                 if ( ( $id !== null ) || ( $id === null && IP::isIPAddress( $username ) ) ) {
367                         if ( $sp->getUser()->isAllowed( 'block' ) ) { # Block / Change block / Unblock links
368                                 if ( $target->isBlocked() && $target->getBlock()->getType() != Block::TYPE_AUTO ) {
369                                         $tools['block'] = $linkRenderer->makeKnownLink( # Change block link
370                                                 SpecialPage::getTitleFor( 'Block', $username ),
371                                                 $sp->msg( 'change-blocklink' )->text()
372                                         );
373                                         $tools['unblock'] = $linkRenderer->makeKnownLink( # Unblock link
374                                                 SpecialPage::getTitleFor( 'Unblock', $username ),
375                                                 $sp->msg( 'unblocklink' )->text()
376                                         );
377                                 } else { # User is not blocked
378                                         $tools['block'] = $linkRenderer->makeKnownLink( # Block link
379                                                 SpecialPage::getTitleFor( 'Block', $username ),
380                                                 $sp->msg( 'blocklink' )->text()
381                                         );
382                                 }
383                         }
384
385                         # Block log link
386                         $tools['log-block'] = $linkRenderer->makeKnownLink(
387                                 SpecialPage::getTitleFor( 'Log', 'block' ),
388                                 $sp->msg( 'sp-contributions-blocklog' )->text(),
389                                 [],
390                                 [ 'page' => $userpage->getPrefixedText() ]
391                         );
392
393                         # Suppression log link (T61120)
394                         if ( $sp->getUser()->isAllowed( 'suppressionlog' ) ) {
395                                 $tools['log-suppression'] = $linkRenderer->makeKnownLink(
396                                         SpecialPage::getTitleFor( 'Log', 'suppress' ),
397                                         $sp->msg( 'sp-contributions-suppresslog', $username )->text(),
398                                         [],
399                                         [ 'offender' => $username ]
400                                 );
401                         }
402                 }
403
404                 # Don't show some links for IP ranges
405                 if ( !IP::isValidRange( $username ) ) {
406                         # Uploads
407                         $tools['uploads'] = $linkRenderer->makeKnownLink(
408                                 SpecialPage::getTitleFor( 'Listfiles', $username ),
409                                 $sp->msg( 'sp-contributions-uploads' )->text()
410                         );
411
412                         # Other logs link
413                         $tools['logs'] = $linkRenderer->makeKnownLink(
414                                 SpecialPage::getTitleFor( 'Log', $username ),
415                                 $sp->msg( 'sp-contributions-logs' )->text()
416                         );
417
418                         # Add link to deleted user contributions for priviledged users
419                         if ( $sp->getUser()->isAllowed( 'deletedhistory' ) ) {
420                                 $tools['deletedcontribs'] = $linkRenderer->makeKnownLink(
421                                         SpecialPage::getTitleFor( 'DeletedContributions', $username ),
422                                         $sp->msg( 'sp-contributions-deleted', $username )->text()
423                                 );
424                         }
425                 }
426
427                 # Add a link to change user rights for privileged users
428                 $userrightsPage = new UserrightsPage();
429                 $userrightsPage->setContext( $sp->getContext() );
430                 if ( $userrightsPage->userCanChangeRights( $target ) ) {
431                         $tools['userrights'] = $linkRenderer->makeKnownLink(
432                                 SpecialPage::getTitleFor( 'Userrights', $username ),
433                                 $sp->msg( 'sp-contributions-userrights', $username )->text()
434                         );
435                 }
436
437                 Hooks::run( 'ContributionsToolLinks', [ $id, $userpage, &$tools, $sp ] );
438
439                 return $tools;
440         }
441
442         /**
443          * Generates the namespace selector form with hidden attributes.
444          * @return string HTML fragment
445          */
446         protected function getForm() {
447                 $this->opts['title'] = $this->getPageTitle()->getPrefixedText();
448                 if ( !isset( $this->opts['target'] ) ) {
449                         $this->opts['target'] = '';
450                 } else {
451                         $this->opts['target'] = str_replace( '_', ' ', $this->opts['target'] );
452                 }
453
454                 if ( !isset( $this->opts['namespace'] ) ) {
455                         $this->opts['namespace'] = '';
456                 }
457
458                 if ( !isset( $this->opts['nsInvert'] ) ) {
459                         $this->opts['nsInvert'] = '';
460                 }
461
462                 if ( !isset( $this->opts['associated'] ) ) {
463                         $this->opts['associated'] = false;
464                 }
465
466                 if ( !isset( $this->opts['contribs'] ) ) {
467                         $this->opts['contribs'] = 'user';
468                 }
469
470                 if ( !isset( $this->opts['start'] ) ) {
471                         $this->opts['start'] = '';
472                 }
473
474                 if ( !isset( $this->opts['end'] ) ) {
475                         $this->opts['end'] = '';
476                 }
477
478                 if ( $this->opts['contribs'] == 'newbie' ) {
479                         $this->opts['target'] = '';
480                 }
481
482                 if ( !isset( $this->opts['tagfilter'] ) ) {
483                         $this->opts['tagfilter'] = '';
484                 }
485
486                 if ( !isset( $this->opts['topOnly'] ) ) {
487                         $this->opts['topOnly'] = false;
488                 }
489
490                 if ( !isset( $this->opts['newOnly'] ) ) {
491                         $this->opts['newOnly'] = false;
492                 }
493
494                 if ( !isset( $this->opts['hideMinor'] ) ) {
495                         $this->opts['hideMinor'] = false;
496                 }
497
498                 $form = Html::openElement(
499                         'form',
500                         [
501                                 'method' => 'get',
502                                 'action' => wfScript(),
503                                 'class' => 'mw-contributions-form'
504                         ]
505                 );
506
507                 # Add hidden params for tracking except for parameters in $skipParameters
508                 $skipParameters = [
509                         'namespace',
510                         'nsInvert',
511                         'deletedOnly',
512                         'target',
513                         'contribs',
514                         'year',
515                         'month',
516                         'start',
517                         'end',
518                         'topOnly',
519                         'newOnly',
520                         'hideMinor',
521                         'associated',
522                         'tagfilter'
523                 ];
524
525                 foreach ( $this->opts as $name => $value ) {
526                         if ( in_array( $name, $skipParameters ) ) {
527                                 continue;
528                         }
529                         $form .= "\t" . Html::hidden( $name, $value ) . "\n";
530                 }
531
532                 $tagFilter = ChangeTags::buildTagFilterSelector(
533                         $this->opts['tagfilter'], false, $this->getContext() );
534
535                 if ( $tagFilter ) {
536                         $filterSelection = Html::rawElement(
537                                 'div',
538                                 [],
539                                 implode( '&#160;', $tagFilter )
540                         );
541                 } else {
542                         $filterSelection = Html::rawElement( 'div', [], '' );
543                 }
544
545                 $this->getOutput()->addModules( 'mediawiki.userSuggest' );
546
547                 $labelNewbies = Xml::radioLabel(
548                         $this->msg( 'sp-contributions-newbies' )->text(),
549                         'contribs',
550                         'newbie',
551                         'newbie',
552                         $this->opts['contribs'] == 'newbie',
553                         [ 'class' => 'mw-input' ]
554                 );
555                 $labelUsername = Xml::radioLabel(
556                         $this->msg( 'sp-contributions-username' )->text(),
557                         'contribs',
558                         'user',
559                         'user',
560                         $this->opts['contribs'] == 'user',
561                         [ 'class' => 'mw-input' ]
562                 );
563                 $input = Html::input(
564                         'target',
565                         $this->opts['target'],
566                         'text',
567                         [
568                                 'size' => '40',
569                                 'class' => [
570                                         'mw-input',
571                                         'mw-ui-input-inline',
572                                         'mw-autocomplete-user', // used by mediawiki.userSuggest
573                                 ],
574                         ] + (
575                                 // Only autofocus if target hasn't been specified or in non-newbies mode
576                                 ( $this->opts['contribs'] === 'newbie' || $this->opts['target'] )
577                                         ? [] : [ 'autofocus' => true ]
578                                 )
579                 );
580
581                 $targetSelection = Html::rawElement(
582                         'div',
583                         [],
584                         $labelNewbies . '<br>' . $labelUsername . ' ' . $input . ' '
585                 );
586
587                 $namespaceSelection = Xml::tags(
588                         'div',
589                         [],
590                         Xml::label(
591                                 $this->msg( 'namespace' )->text(),
592                                 'namespace',
593                                 ''
594                         ) . '&#160;' .
595                         Html::namespaceSelector(
596                                 [ 'selected' => $this->opts['namespace'], 'all' => '' ],
597                                 [
598                                         'name' => 'namespace',
599                                         'id' => 'namespace',
600                                         'class' => 'namespaceselector',
601                                 ]
602                         ) . '&#160;' .
603                                 Html::rawElement(
604                                         'span',
605                                         [ 'class' => 'mw-input-with-label' ],
606                                         Xml::checkLabel(
607                                                 $this->msg( 'invert' )->text(),
608                                                 'nsInvert',
609                                                 'nsInvert',
610                                                 $this->opts['nsInvert'],
611                                                 [
612                                                         'title' => $this->msg( 'tooltip-invert' )->text(),
613                                                         'class' => 'mw-input'
614                                                 ]
615                                         ) . '&#160;'
616                                 ) .
617                                 Html::rawElement( 'span', [ 'class' => 'mw-input-with-label' ],
618                                         Xml::checkLabel(
619                                                 $this->msg( 'namespace_association' )->text(),
620                                                 'associated',
621                                                 'associated',
622                                                 $this->opts['associated'],
623                                                 [
624                                                         'title' => $this->msg( 'tooltip-namespace_association' )->text(),
625                                                         'class' => 'mw-input'
626                                                 ]
627                                         ) . '&#160;'
628                                 )
629                 );
630
631                 $filters = [];
632
633                 if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) {
634                         $filters[] = Html::rawElement(
635                                 'span',
636                                 [ 'class' => 'mw-input-with-label' ],
637                                 Xml::checkLabel(
638                                         $this->msg( 'history-show-deleted' )->text(),
639                                         'deletedOnly',
640                                         'mw-show-deleted-only',
641                                         $this->opts['deletedOnly'],
642                                         [ 'class' => 'mw-input' ]
643                                 )
644                         );
645                 }
646
647                 $filters[] = Html::rawElement(
648                         'span',
649                         [ 'class' => 'mw-input-with-label' ],
650                         Xml::checkLabel(
651                                 $this->msg( 'sp-contributions-toponly' )->text(),
652                                 'topOnly',
653                                 'mw-show-top-only',
654                                 $this->opts['topOnly'],
655                                 [ 'class' => 'mw-input' ]
656                         )
657                 );
658                 $filters[] = Html::rawElement(
659                         'span',
660                         [ 'class' => 'mw-input-with-label' ],
661                         Xml::checkLabel(
662                                 $this->msg( 'sp-contributions-newonly' )->text(),
663                                 'newOnly',
664                                 'mw-show-new-only',
665                                 $this->opts['newOnly'],
666                                 [ 'class' => 'mw-input' ]
667                         )
668                 );
669                 $filters[] = Html::rawElement(
670                         'span',
671                         [ 'class' => 'mw-input-with-label' ],
672                         Xml::checkLabel(
673                                 $this->msg( 'sp-contributions-hideminor' )->text(),
674                                 'hideMinor',
675                                 'mw-hide-minor-edits',
676                                 $this->opts['hideMinor'],
677                                 [ 'class' => 'mw-input' ]
678                         )
679                 );
680
681                 Hooks::run(
682                         'SpecialContributions::getForm::filters',
683                         [ $this, &$filters ]
684                 );
685
686                 $extraOptions = Html::rawElement(
687                         'div',
688                         [],
689                         implode( '', $filters )
690                 );
691
692                 $dateRangeSelection = Html::rawElement(
693                         'div',
694                         [],
695                         Xml::label( wfMessage( 'date-range-from' )->text(), 'mw-date-start' ) . ' ' .
696                         new DateInputWidget( [
697                                 'infusable' => true,
698                                 'id' => 'mw-date-start',
699                                 'name' => 'start',
700                                 'value' => $this->opts['start'],
701                                 'longDisplayFormat' => true,
702                         ] ) . '<br>' .
703                         Xml::label( wfMessage( 'date-range-to' )->text(), 'mw-date-end' ) . ' ' .
704                         new DateInputWidget( [
705                                 'infusable' => true,
706                                 'id' => 'mw-date-end',
707                                 'name' => 'end',
708                                 'value' => $this->opts['end'],
709                                 'longDisplayFormat' => true,
710                         ] )
711                 );
712
713                 $submit = Xml::tags( 'div', [],
714                         Html::submitButton(
715                                 $this->msg( 'sp-contributions-submit' )->text(),
716                                 [ 'class' => 'mw-submit' ], [ 'mw-ui-progressive' ]
717                         )
718                 );
719
720                 $form .= Xml::fieldset(
721                         $this->msg( 'sp-contributions-search' )->text(),
722                         $targetSelection .
723                         $namespaceSelection .
724                         $filterSelection .
725                         $extraOptions .
726                         $dateRangeSelection .
727                         $submit,
728                         [ 'class' => 'mw-contributions-table' ]
729                 );
730
731                 $explain = $this->msg( 'sp-contributions-explain' );
732                 if ( !$explain->isBlank() ) {
733                         $form .= "<p id='mw-sp-contributions-explain'>{$explain->parse()}</p>";
734                 }
735
736                 $form .= Xml::closeElement( 'form' );
737
738                 return $form;
739         }
740
741         /**
742          * Return an array of subpages beginning with $search that this special page will accept.
743          *
744          * @param string $search Prefix to search for
745          * @param int $limit Maximum number of results to return (usually 10)
746          * @param int $offset Number of results to skip (usually 0)
747          * @return string[] Matching subpages
748          */
749         public function prefixSearchSubpages( $search, $limit, $offset ) {
750                 $user = User::newFromName( $search );
751                 if ( !$user ) {
752                         // No prefix suggestion for invalid user
753                         return [];
754                 }
755                 // Autocomplete subpage as user list - public to allow caching
756                 return UserNamePrefixSearch::search( 'public', $search, $limit, $offset );
757         }
758
759         protected function getGroupName() {
760                 return 'users';
761         }
762 }