Merge "Show protection log on creation-protected pages"
[lhc/web/wiklou.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 $this->getSkin()->setRelevantUser( $userObj );
107 } else {
108 $out->addSubtitle( $this->msg( 'sp-contributions-newbies-sub' ) );
109 $out->setHTMLTitle( $this->msg(
110 'pagetitle',
111 $this->msg( 'sp-contributions-newbies-title' )->plain()
112 )->inContentLanguage() );
113 }
114
115 $ns = $request->getVal( 'namespace', null );
116 if ( $ns !== null && $ns !== '' ) {
117 $this->opts['namespace'] = intval( $ns );
118 } else {
119 $this->opts['namespace'] = '';
120 }
121
122 $this->opts['associated'] = $request->getBool( 'associated' );
123 $this->opts['nsInvert'] = (bool)$request->getVal( 'nsInvert' );
124 $this->opts['tagfilter'] = (string)$request->getVal( 'tagfilter' );
125
126 // Allows reverts to have the bot flag in recent changes. It is just here to
127 // be passed in the form at the top of the page
128 if ( $user->isAllowed( 'markbotedits' ) && $request->getBool( 'bot' ) ) {
129 $this->opts['bot'] = '1';
130 }
131
132 $skip = $request->getText( 'offset' ) || $request->getText( 'dir' ) == 'prev';
133 # Offset overrides year/month selection
134 if ( !$skip ) {
135 $this->opts['year'] = $request->getVal( 'year' );
136 $this->opts['month'] = $request->getVal( 'month' );
137
138 $this->opts['start'] = $request->getVal( 'start' );
139 $this->opts['end'] = $request->getVal( 'end' );
140 }
141 $this->opts = ContribsPager::processDateFilter( $this->opts );
142
143 $feedType = $request->getVal( 'feed' );
144
145 $feedParams = [
146 'action' => 'feedcontributions',
147 'user' => $target,
148 ];
149 if ( $this->opts['topOnly'] ) {
150 $feedParams['toponly'] = true;
151 }
152 if ( $this->opts['newOnly'] ) {
153 $feedParams['newonly'] = true;
154 }
155 if ( $this->opts['hideMinor'] ) {
156 $feedParams['hideminor'] = true;
157 }
158 if ( $this->opts['deletedOnly'] ) {
159 $feedParams['deletedonly'] = true;
160 }
161 if ( $this->opts['tagfilter'] !== '' ) {
162 $feedParams['tagfilter'] = $this->opts['tagfilter'];
163 }
164 if ( $this->opts['namespace'] !== '' ) {
165 $feedParams['namespace'] = $this->opts['namespace'];
166 }
167 // Don't use year and month for the feed URL, but pass them on if
168 // we redirect to API (if $feedType is specified)
169 if ( $feedType && $this->opts['year'] !== null ) {
170 $feedParams['year'] = $this->opts['year'];
171 }
172 if ( $feedType && $this->opts['month'] !== null ) {
173 $feedParams['month'] = $this->opts['month'];
174 }
175
176 if ( $feedType ) {
177 // Maintain some level of backwards compatibility
178 // If people request feeds using the old parameters, redirect to API
179 $feedParams['feedformat'] = $feedType;
180 $url = wfAppendQuery( wfScript( 'api' ), $feedParams );
181
182 $out->redirect( $url, '301' );
183
184 return;
185 }
186
187 // Add RSS/atom links
188 $this->addFeedLinks( $feedParams );
189
190 if ( Hooks::run( 'SpecialContributionsBeforeMainOutput', [ $id, $userObj, $this ] ) ) {
191 if ( !$this->including() ) {
192 $out->addHTML( $this->getForm() );
193 }
194 $pager = new ContribsPager( $this->getContext(), [
195 'target' => $target,
196 'contribs' => $this->opts['contribs'],
197 'namespace' => $this->opts['namespace'],
198 'tagfilter' => $this->opts['tagfilter'],
199 'start' => $this->opts['start'],
200 'end' => $this->opts['end'],
201 'deletedOnly' => $this->opts['deletedOnly'],
202 'topOnly' => $this->opts['topOnly'],
203 'newOnly' => $this->opts['newOnly'],
204 'hideMinor' => $this->opts['hideMinor'],
205 'nsInvert' => $this->opts['nsInvert'],
206 'associated' => $this->opts['associated'],
207 ] );
208
209 if ( !$pager->getNumRows() ) {
210 $out->addWikiMsg( 'nocontribs', $target );
211 } else {
212 # Show a message about replica DB lag, if applicable
213 $lag = wfGetLB()->safeGetLag( $pager->getDatabase() );
214 if ( $lag > 0 ) {
215 $out->showLagWarning( $lag );
216 }
217
218 $output = $pager->getBody();
219 if ( !$this->including() ) {
220 $output = '<p>' . $pager->getNavigationBar() . '</p>' .
221 $output .
222 '<p>' . $pager->getNavigationBar() . '</p>';
223 }
224 $out->addHTML( $output );
225 }
226 $out->preventClickjacking( $pager->getPreventClickjacking() );
227
228 # Show the appropriate "footer" message - WHOIS tools, etc.
229 if ( $this->opts['contribs'] == 'newbie' ) {
230 $message = 'sp-contributions-footer-newbies';
231 } elseif ( IP::isIPAddress( $target ) ) {
232 $message = 'sp-contributions-footer-anon';
233 } elseif ( $userObj->isAnon() ) {
234 // No message for non-existing users
235 $message = '';
236 } else {
237 $message = 'sp-contributions-footer';
238 }
239
240 if ( $message ) {
241 if ( !$this->including() ) {
242 if ( !$this->msg( $message, $target )->isDisabled() ) {
243 $out->wrapWikiMsg(
244 "<div class='mw-contributions-footer'>\n$1\n</div>",
245 [ $message, $target ] );
246 }
247 }
248 }
249 }
250 }
251
252 /**
253 * Generates the subheading with links
254 * @param User $userObj User object for the target
255 * @return string Appropriately-escaped HTML to be output literally
256 * @todo FIXME: Almost the same as getSubTitle in SpecialDeletedContributions.php.
257 * Could be combined.
258 */
259 protected function contributionsSub( $userObj ) {
260 if ( $userObj->isAnon() ) {
261 // Show a warning message that the user being searched for doesn't exists
262 if ( !User::isIP( $userObj->getName() ) ) {
263 $this->getOutput()->wrapWikiMsg(
264 "<div class=\"mw-userpage-userdoesnotexist error\">\n\$1\n</div>",
265 [
266 'contributions-userdoesnotexist',
267 wfEscapeWikiText( $userObj->getName() ),
268 ]
269 );
270 if ( !$this->including() ) {
271 $this->getOutput()->setStatusCode( 404 );
272 }
273 }
274 $user = htmlspecialchars( $userObj->getName() );
275 } else {
276 $user = $this->getLinkRenderer()->makeLink( $userObj->getUserPage(), $userObj->getName() );
277 }
278 $nt = $userObj->getUserPage();
279 $talk = $userObj->getTalkPage();
280 $links = '';
281 if ( $talk ) {
282 $tools = self::getUserLinks( $this, $userObj );
283 $links = $this->getLanguage()->pipeList( $tools );
284
285 // Show a note if the user is blocked and display the last block log entry.
286 // Do not expose the autoblocks, since that may lead to a leak of accounts' IPs,
287 // and also this will display a totally irrelevant log entry as a current block.
288 if ( !$this->including() ) {
289 $block = Block::newFromTarget( $userObj, $userObj );
290 if ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) {
291 if ( $block->getType() == Block::TYPE_RANGE ) {
292 $nt = MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget();
293 }
294
295 $out = $this->getOutput(); // showLogExtract() wants first parameter by reference
296 LogEventsList::showLogExtract(
297 $out,
298 'block',
299 $nt,
300 '',
301 [
302 'lim' => 1,
303 'showIfEmpty' => false,
304 'msgKey' => [
305 $userObj->isAnon() ?
306 'sp-contributions-blocked-notice-anon' :
307 'sp-contributions-blocked-notice',
308 $userObj->getName() # Support GENDER in 'sp-contributions-blocked-notice'
309 ],
310 'offset' => '' # don't use WebRequest parameter offset
311 ]
312 );
313 }
314 }
315 }
316
317 return $this->msg( 'contribsub2' )->rawParams( $user, $links )->params( $userObj->getName() );
318 }
319
320 /**
321 * Links to different places.
322 *
323 * @note This function is also called in DeletedContributionsPage
324 * @param SpecialPage $sp SpecialPage instance, for context
325 * @param User $target Target user object
326 * @return array
327 */
328 public static function getUserLinks( SpecialPage $sp, User $target ) {
329 $id = $target->getId();
330 $username = $target->getName();
331 $userpage = $target->getUserPage();
332 $talkpage = $target->getTalkPage();
333
334 $linkRenderer = $sp->getLinkRenderer();
335 $tools['user-talk'] = $linkRenderer->makeLink(
336 $talkpage,
337 $sp->msg( 'sp-contributions-talk' )->text()
338 );
339
340 if ( ( $id !== null ) || ( $id === null && IP::isIPAddress( $username ) ) ) {
341 if ( $sp->getUser()->isAllowed( 'block' ) ) { # Block / Change block / Unblock links
342 if ( $target->isBlocked() && $target->getBlock()->getType() != Block::TYPE_AUTO ) {
343 $tools['block'] = $linkRenderer->makeKnownLink( # Change block link
344 SpecialPage::getTitleFor( 'Block', $username ),
345 $sp->msg( 'change-blocklink' )->text()
346 );
347 $tools['unblock'] = $linkRenderer->makeKnownLink( # Unblock link
348 SpecialPage::getTitleFor( 'Unblock', $username ),
349 $sp->msg( 'unblocklink' )->text()
350 );
351 } else { # User is not blocked
352 $tools['block'] = $linkRenderer->makeKnownLink( # Block link
353 SpecialPage::getTitleFor( 'Block', $username ),
354 $sp->msg( 'blocklink' )->text()
355 );
356 }
357 }
358
359 # Block log link
360 $tools['log-block'] = $linkRenderer->makeKnownLink(
361 SpecialPage::getTitleFor( 'Log', 'block' ),
362 $sp->msg( 'sp-contributions-blocklog' )->text(),
363 [],
364 [ 'page' => $userpage->getPrefixedText() ]
365 );
366
367 # Suppression log link (T61120)
368 if ( $sp->getUser()->isAllowed( 'suppressionlog' ) ) {
369 $tools['log-suppression'] = $linkRenderer->makeKnownLink(
370 SpecialPage::getTitleFor( 'Log', 'suppress' ),
371 $sp->msg( 'sp-contributions-suppresslog', $username )->text(),
372 [],
373 [ 'offender' => $username ]
374 );
375 }
376 }
377 # Uploads
378 $tools['uploads'] = $linkRenderer->makeKnownLink(
379 SpecialPage::getTitleFor( 'Listfiles', $username ),
380 $sp->msg( 'sp-contributions-uploads' )->text()
381 );
382
383 # Other logs link
384 $tools['logs'] = $linkRenderer->makeKnownLink(
385 SpecialPage::getTitleFor( 'Log', $username ),
386 $sp->msg( 'sp-contributions-logs' )->text()
387 );
388
389 # Add link to deleted user contributions for priviledged users
390 if ( $sp->getUser()->isAllowed( 'deletedhistory' ) ) {
391 $tools['deletedcontribs'] = $linkRenderer->makeKnownLink(
392 SpecialPage::getTitleFor( 'DeletedContributions', $username ),
393 $sp->msg( 'sp-contributions-deleted', $username )->text()
394 );
395 }
396
397 # Add a link to change user rights for privileged users
398 $userrightsPage = new UserrightsPage();
399 $userrightsPage->setContext( $sp->getContext() );
400 if ( $userrightsPage->userCanChangeRights( $target ) ) {
401 $tools['userrights'] = $linkRenderer->makeKnownLink(
402 SpecialPage::getTitleFor( 'Userrights', $username ),
403 $sp->msg( 'sp-contributions-userrights', $username )->text()
404 );
405 }
406
407 Hooks::run( 'ContributionsToolLinks', [ $id, $userpage, &$tools, $sp ] );
408
409 return $tools;
410 }
411
412 /**
413 * Generates the namespace selector form with hidden attributes.
414 * @return string HTML fragment
415 */
416 protected function getForm() {
417 $this->opts['title'] = $this->getPageTitle()->getPrefixedText();
418 if ( !isset( $this->opts['target'] ) ) {
419 $this->opts['target'] = '';
420 } else {
421 $this->opts['target'] = str_replace( '_', ' ', $this->opts['target'] );
422 }
423
424 if ( !isset( $this->opts['namespace'] ) ) {
425 $this->opts['namespace'] = '';
426 }
427
428 if ( !isset( $this->opts['nsInvert'] ) ) {
429 $this->opts['nsInvert'] = '';
430 }
431
432 if ( !isset( $this->opts['associated'] ) ) {
433 $this->opts['associated'] = false;
434 }
435
436 if ( !isset( $this->opts['contribs'] ) ) {
437 $this->opts['contribs'] = 'user';
438 }
439
440 if ( !isset( $this->opts['start'] ) ) {
441 $this->opts['start'] = '';
442 }
443
444 if ( !isset( $this->opts['end'] ) ) {
445 $this->opts['end'] = '';
446 }
447
448 if ( $this->opts['contribs'] == 'newbie' ) {
449 $this->opts['target'] = '';
450 }
451
452 if ( !isset( $this->opts['tagfilter'] ) ) {
453 $this->opts['tagfilter'] = '';
454 }
455
456 if ( !isset( $this->opts['topOnly'] ) ) {
457 $this->opts['topOnly'] = false;
458 }
459
460 if ( !isset( $this->opts['newOnly'] ) ) {
461 $this->opts['newOnly'] = false;
462 }
463
464 if ( !isset( $this->opts['hideMinor'] ) ) {
465 $this->opts['hideMinor'] = false;
466 }
467
468 $form = Html::openElement(
469 'form',
470 [
471 'method' => 'get',
472 'action' => wfScript(),
473 'class' => 'mw-contributions-form'
474 ]
475 );
476
477 # Add hidden params for tracking except for parameters in $skipParameters
478 $skipParameters = [
479 'namespace',
480 'nsInvert',
481 'deletedOnly',
482 'target',
483 'contribs',
484 'year',
485 'month',
486 'start',
487 'end',
488 'topOnly',
489 'newOnly',
490 'hideMinor',
491 'associated',
492 'tagfilter'
493 ];
494
495 foreach ( $this->opts as $name => $value ) {
496 if ( in_array( $name, $skipParameters ) ) {
497 continue;
498 }
499 $form .= "\t" . Html::hidden( $name, $value ) . "\n";
500 }
501
502 $tagFilter = ChangeTags::buildTagFilterSelector(
503 $this->opts['tagfilter'], false, $this->getContext() );
504
505 if ( $tagFilter ) {
506 $filterSelection = Html::rawElement(
507 'div',
508 [],
509 implode( '&#160;', $tagFilter )
510 );
511 } else {
512 $filterSelection = Html::rawElement( 'div', [], '' );
513 }
514
515 $this->getOutput()->addModules( 'mediawiki.userSuggest' );
516
517 $labelNewbies = Xml::radioLabel(
518 $this->msg( 'sp-contributions-newbies' )->text(),
519 'contribs',
520 'newbie',
521 'newbie',
522 $this->opts['contribs'] == 'newbie',
523 [ 'class' => 'mw-input' ]
524 );
525 $labelUsername = Xml::radioLabel(
526 $this->msg( 'sp-contributions-username' )->text(),
527 'contribs',
528 'user',
529 'user',
530 $this->opts['contribs'] == 'user',
531 [ 'class' => 'mw-input' ]
532 );
533 $input = Html::input(
534 'target',
535 $this->opts['target'],
536 'text',
537 [
538 'size' => '40',
539 'class' => [
540 'mw-input',
541 'mw-ui-input-inline',
542 'mw-autocomplete-user', // used by mediawiki.userSuggest
543 ],
544 ] + (
545 // Only autofocus if target hasn't been specified or in non-newbies mode
546 ( $this->opts['contribs'] === 'newbie' || $this->opts['target'] )
547 ? [] : [ 'autofocus' => true ]
548 )
549 );
550
551 $targetSelection = Html::rawElement(
552 'div',
553 [],
554 $labelNewbies . '<br>' . $labelUsername . ' ' . $input . ' '
555 );
556
557 $namespaceSelection = Xml::tags(
558 'div',
559 [],
560 Xml::label(
561 $this->msg( 'namespace' )->text(),
562 'namespace',
563 ''
564 ) . '&#160;' .
565 Html::namespaceSelector(
566 [ 'selected' => $this->opts['namespace'], 'all' => '' ],
567 [
568 'name' => 'namespace',
569 'id' => 'namespace',
570 'class' => 'namespaceselector',
571 ]
572 ) . '&#160;' .
573 Html::rawElement(
574 'span',
575 [ 'class' => 'mw-input-with-label' ],
576 Xml::checkLabel(
577 $this->msg( 'invert' )->text(),
578 'nsInvert',
579 'nsInvert',
580 $this->opts['nsInvert'],
581 [
582 'title' => $this->msg( 'tooltip-invert' )->text(),
583 'class' => 'mw-input'
584 ]
585 ) . '&#160;'
586 ) .
587 Html::rawElement( 'span', [ 'class' => 'mw-input-with-label' ],
588 Xml::checkLabel(
589 $this->msg( 'namespace_association' )->text(),
590 'associated',
591 'associated',
592 $this->opts['associated'],
593 [
594 'title' => $this->msg( 'tooltip-namespace_association' )->text(),
595 'class' => 'mw-input'
596 ]
597 ) . '&#160;'
598 )
599 );
600
601 $filters = [];
602
603 if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) {
604 $filters[] = Html::rawElement(
605 'span',
606 [ 'class' => 'mw-input-with-label' ],
607 Xml::checkLabel(
608 $this->msg( 'history-show-deleted' )->text(),
609 'deletedOnly',
610 'mw-show-deleted-only',
611 $this->opts['deletedOnly'],
612 [ 'class' => 'mw-input' ]
613 )
614 );
615 }
616
617 $filters[] = Html::rawElement(
618 'span',
619 [ 'class' => 'mw-input-with-label' ],
620 Xml::checkLabel(
621 $this->msg( 'sp-contributions-toponly' )->text(),
622 'topOnly',
623 'mw-show-top-only',
624 $this->opts['topOnly'],
625 [ 'class' => 'mw-input' ]
626 )
627 );
628 $filters[] = Html::rawElement(
629 'span',
630 [ 'class' => 'mw-input-with-label' ],
631 Xml::checkLabel(
632 $this->msg( 'sp-contributions-newonly' )->text(),
633 'newOnly',
634 'mw-show-new-only',
635 $this->opts['newOnly'],
636 [ 'class' => 'mw-input' ]
637 )
638 );
639 $filters[] = Html::rawElement(
640 'span',
641 [ 'class' => 'mw-input-with-label' ],
642 Xml::checkLabel(
643 $this->msg( 'sp-contributions-hideminor' )->text(),
644 'hideMinor',
645 'mw-hide-minor-edits',
646 $this->opts['hideMinor'],
647 [ 'class' => 'mw-input' ]
648 )
649 );
650
651 Hooks::run(
652 'SpecialContributions::getForm::filters',
653 [ $this, &$filters ]
654 );
655
656 $extraOptions = Html::rawElement(
657 'div',
658 [],
659 implode( '', $filters )
660 );
661
662 $dateRangeSelection = Html::rawElement(
663 'div',
664 [],
665 Xml::label( wfMessage( 'date-range-from' )->text(), 'mw-date-start' ) . ' ' .
666 new DateInputWidget( [
667 'infusable' => true,
668 'id' => 'mw-date-start',
669 'name' => 'start',
670 'value' => $this->opts['start'],
671 'longDisplayFormat' => true,
672 ] ) . '<br>' .
673 Xml::label( wfMessage( 'date-range-to' )->text(), 'mw-date-end' ) . ' ' .
674 new DateInputWidget( [
675 'infusable' => true,
676 'id' => 'mw-date-end',
677 'name' => 'end',
678 'value' => $this->opts['end'],
679 'longDisplayFormat' => true,
680 ] )
681 );
682
683 $submit = Xml::tags( 'div', [],
684 Html::submitButton(
685 $this->msg( 'sp-contributions-submit' )->text(),
686 [ 'class' => 'mw-submit' ], [ 'mw-ui-progressive' ]
687 )
688 );
689
690 $form .= Xml::fieldset(
691 $this->msg( 'sp-contributions-search' )->text(),
692 $targetSelection .
693 $namespaceSelection .
694 $filterSelection .
695 $extraOptions .
696 $dateRangeSelection .
697 $submit,
698 [ 'class' => 'mw-contributions-table' ]
699 );
700
701 $explain = $this->msg( 'sp-contributions-explain' );
702 if ( !$explain->isBlank() ) {
703 $form .= "<p id='mw-sp-contributions-explain'>{$explain->parse()}</p>";
704 }
705
706 $form .= Xml::closeElement( 'form' );
707
708 return $form;
709 }
710
711 /**
712 * Return an array of subpages beginning with $search that this special page will accept.
713 *
714 * @param string $search Prefix to search for
715 * @param int $limit Maximum number of results to return (usually 10)
716 * @param int $offset Number of results to skip (usually 0)
717 * @return string[] Matching subpages
718 */
719 public function prefixSearchSubpages( $search, $limit, $offset ) {
720 $user = User::newFromName( $search );
721 if ( !$user ) {
722 // No prefix suggestion for invalid user
723 return [];
724 }
725 // Autocomplete subpage as user list - public to allow caching
726 return UserNamePrefixSearch::search( 'public', $search, $limit, $offset );
727 }
728
729 protected function getGroupName() {
730 return 'users';
731 }
732 }