Completed
Branch master (939199)
by
unknown
39:35
created

includes/specials/SpecialContributions.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
/**
25
 * Special:Contributions, show user contributions in a paged list
26
 *
27
 * @ingroup SpecialPage
28
 */
29
class SpecialContributions extends IncludableSpecialPage {
30
	protected $opts;
31
32
	public function __construct() {
33
		parent::__construct( 'Contributions' );
34
	}
35
36
	public function execute( $par ) {
37
		$this->setHeaders();
38
		$this->outputHeader();
39
		$out = $this->getOutput();
40
		$out->addModuleStyles( [
41
			'mediawiki.special',
42
			'mediawiki.special.changeslist',
43
		] );
44
		$this->addHelpLink( 'Help:User contributions' );
45
46
		$this->opts = [];
47
		$request = $this->getRequest();
48
49
		if ( $par !== null ) {
50
			$target = $par;
51
		} else {
52
			$target = $request->getVal( 'target' );
53
		}
54
55
		if ( $request->getVal( 'contribs' ) == 'newbie' || $par === 'newbies' ) {
56
			$target = 'newbies';
57
			$this->opts['contribs'] = 'newbie';
58
		} else {
59
			$this->opts['contribs'] = 'user';
60
		}
61
62
		$this->opts['deletedOnly'] = $request->getBool( 'deletedOnly' );
63
64
		if ( !strlen( $target ) ) {
65
			if ( !$this->including() ) {
66
				$out->addHTML( $this->getForm() );
67
			}
68
69
			return;
70
		}
71
72
		$user = $this->getUser();
73
74
		$this->opts['limit'] = $request->getInt( 'limit', $user->getOption( 'rclimit' ) );
75
		$this->opts['target'] = $target;
76
		$this->opts['topOnly'] = $request->getBool( 'topOnly' );
77
		$this->opts['newOnly'] = $request->getBool( 'newOnly' );
78
		$this->opts['hideMinor'] = $request->getBool( 'hideMinor' );
79
80
		$nt = Title::makeTitleSafe( NS_USER, $target );
81
		if ( !$nt ) {
82
			$out->addHTML( $this->getForm() );
83
84
			return;
85
		}
86
		$userObj = User::newFromName( $nt->getText(), false );
87
		if ( !$userObj ) {
88
			$out->addHTML( $this->getForm() );
89
90
			return;
91
		}
92
		$id = $userObj->getId();
93
94
		if ( $this->opts['contribs'] != 'newbie' ) {
95
			$target = $nt->getText();
96
			$out->addSubtitle( $this->contributionsSub( $userObj ) );
97
			$out->setHTMLTitle( $this->msg(
98
				'pagetitle',
99
				$this->msg( 'contributions-title', $target )->plain()
100
			)->inContentLanguage() );
101
			$this->getSkin()->setRelevantUser( $userObj );
102
		} else {
103
			$out->addSubtitle( $this->msg( 'sp-contributions-newbies-sub' ) );
104
			$out->setHTMLTitle( $this->msg(
105
				'pagetitle',
106
				$this->msg( 'sp-contributions-newbies-title' )->plain()
107
			)->inContentLanguage() );
108
		}
109
110
		$ns = $request->getVal( 'namespace', null );
111
		if ( $ns !== null && $ns !== '' ) {
112
			$this->opts['namespace'] = intval( $ns );
113
		} else {
114
			$this->opts['namespace'] = '';
115
		}
116
117
		$this->opts['associated'] = $request->getBool( 'associated' );
118
		$this->opts['nsInvert'] = (bool)$request->getVal( 'nsInvert' );
119
		$this->opts['tagfilter'] = (string)$request->getVal( 'tagfilter' );
120
121
		// Allows reverts to have the bot flag in recent changes. It is just here to
122
		// be passed in the form at the top of the page
123
		if ( $user->isAllowed( 'markbotedits' ) && $request->getBool( 'bot' ) ) {
124
			$this->opts['bot'] = '1';
125
		}
126
127
		$skip = $request->getText( 'offset' ) || $request->getText( 'dir' ) == 'prev';
128
		# Offset overrides year/month selection
129
		if ( $skip ) {
130
			$this->opts['year'] = '';
131
			$this->opts['month'] = '';
132
		} else {
133
			$this->opts['year'] = $request->getIntOrNull( 'year' );
134
			$this->opts['month'] = $request->getIntOrNull( 'month' );
135
		}
136
137
		$feedType = $request->getVal( 'feed' );
138
139
		$feedParams = [
140
			'action' => 'feedcontributions',
141
			'user' => $target,
142
		];
143
		if ( $this->opts['topOnly'] ) {
144
			$feedParams['toponly'] = true;
145
		}
146
		if ( $this->opts['newOnly'] ) {
147
			$feedParams['newonly'] = true;
148
		}
149
		if ( $this->opts['hideMinor'] ) {
150
			$feedParams['hideminor'] = true;
151
		}
152
		if ( $this->opts['deletedOnly'] ) {
153
			$feedParams['deletedonly'] = true;
154
		}
155
		if ( $this->opts['tagfilter'] !== '' ) {
156
			$feedParams['tagfilter'] = $this->opts['tagfilter'];
157
		}
158
		if ( $this->opts['namespace'] !== '' ) {
159
			$feedParams['namespace'] = $this->opts['namespace'];
160
		}
161
		// Don't use year and month for the feed URL, but pass them on if
162
		// we redirect to API (if $feedType is specified)
163 View Code Duplication
		if ( $feedType && $this->opts['year'] !== null ) {
164
			$feedParams['year'] = $this->opts['year'];
165
		}
166 View Code Duplication
		if ( $feedType && $this->opts['month'] !== null ) {
167
			$feedParams['month'] = $this->opts['month'];
168
		}
169
170
		if ( $feedType ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $feedType of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

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