Completed
Branch master (a5b975)
by
unknown
25:47
created

SpecialEditWatchlist::buildTools()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 25
Code Lines 16

Duplication

Lines 7
Ratio 28 %
Metric Value
dl 7
loc 25
rs 8.8571
cc 2
eloc 16
nc 2
nop 1
1
<?php
2
/**
3
 * @defgroup Watchlist Users watchlist handling
4
 */
5
6
/**
7
 * Implements Special:EditWatchlist
8
 *
9
 * This program is free software; you can redistribute it and/or modify
10
 * it under the terms of the GNU General Public License as published by
11
 * the Free Software Foundation; either version 2 of the License, or
12
 * (at your option) any later version.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU General Public License along
20
 * with this program; if not, write to the Free Software Foundation, Inc.,
21
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22
 * http://www.gnu.org/copyleft/gpl.html
23
 *
24
 * @file
25
 * @ingroup SpecialPage
26
 * @ingroup Watchlist
27
 */
28
29
/**
30
 * Provides the UI through which users can perform editing
31
 * operations on their watchlist
32
 *
33
 * @ingroup SpecialPage
34
 * @ingroup Watchlist
35
 * @author Rob Church <[email protected]>
36
 */
37
class SpecialEditWatchlist extends UnlistedSpecialPage {
38
	/**
39
	 * Editing modes. EDIT_CLEAR is no longer used; the "Clear" link scared people
40
	 * too much. Now it's passed on to the raw editor, from which it's very easy to clear.
41
	 */
42
	const EDIT_CLEAR = 1;
43
	const EDIT_RAW = 2;
44
	const EDIT_NORMAL = 3;
45
46
	protected $successMessage;
47
48
	protected $toc;
49
50
	private $badItems = [];
51
52
	public function __construct() {
53
		parent::__construct( 'EditWatchlist', 'editmywatchlist' );
54
	}
55
56
	public function doesWrites() {
57
		return true;
58
	}
59
60
	/**
61
	 * Main execution point
62
	 *
63
	 * @param int $mode
64
	 */
65
	public function execute( $mode ) {
66
		$this->setHeaders();
67
68
		# Anons don't get a watchlist
69
		$this->requireLogin( 'watchlistanontext' );
70
71
		$out = $this->getOutput();
72
73
		$this->checkPermissions();
74
		$this->checkReadOnly();
75
76
		$this->outputHeader();
77
		$this->outputSubtitle();
78
		$out->addModuleStyles( 'mediawiki.special' );
79
80
		# B/C: $mode used to be waaay down the parameter list, and the first parameter
81
		# was $wgUser
82
		if ( $mode instanceof User ) {
83
			$args = func_get_args();
84
			if ( count( $args ) >= 4 ) {
85
				$mode = $args[3];
86
			}
87
		}
88
		$mode = self::getMode( $this->getRequest(), $mode );
89
90
		switch ( $mode ) {
91 View Code Duplication
			case self::EDIT_RAW:
92
				$out->setPageTitle( $this->msg( 'watchlistedit-raw-title' ) );
93
				$form = $this->getRawForm();
94
				if ( $form->show() ) {
95
					$out->addHTML( $this->successMessage );
96
					$out->addReturnTo( SpecialPage::getTitleFor( 'Watchlist' ) );
97
				}
98
				break;
99 View Code Duplication
			case self::EDIT_CLEAR:
100
				$out->setPageTitle( $this->msg( 'watchlistedit-clear-title' ) );
101
				$form = $this->getClearForm();
102
				if ( $form->show() ) {
103
					$out->addHTML( $this->successMessage );
104
					$out->addReturnTo( SpecialPage::getTitleFor( 'Watchlist' ) );
105
				}
106
				break;
107
108
			case self::EDIT_NORMAL:
109
			default:
110
				$this->executeViewEditWatchlist();
111
				break;
112
		}
113
	}
114
115
	/**
116
	 * Renders a subheader on the watchlist page.
117
	 */
118
	protected function outputSubtitle() {
119
		$out = $this->getOutput();
120
		$out->addSubtitle( $this->msg( 'watchlistfor2', $this->getUser()->getName() )
121
			->rawParams( SpecialEditWatchlist::buildTools( null ) ) );
122
	}
123
124
	/**
125
	 * Executes an edit mode for the watchlist view, from which you can manage your watchlist
126
	 *
127
	 */
128
	protected function executeViewEditWatchlist() {
129
		$out = $this->getOutput();
130
		$out->setPageTitle( $this->msg( 'watchlistedit-normal-title' ) );
131
		$form = $this->getNormalForm();
132
		if ( $form->show() ) {
133
			$out->addHTML( $this->successMessage );
134
			$out->addReturnTo( SpecialPage::getTitleFor( 'Watchlist' ) );
135
		} elseif ( $this->toc !== false ) {
136
			$out->prependHTML( $this->toc );
137
			$out->addModules( 'mediawiki.toc' );
138
		}
139
	}
140
141
	/**
142
	 * Return an array of subpages that this special page will accept.
143
	 *
144
	 * @see also SpecialWatchlist::getSubpagesForPrefixSearch
145
	 * @return string[] subpages
146
	 */
147
	public function getSubpagesForPrefixSearch() {
148
		// SpecialWatchlist uses SpecialEditWatchlist::getMode, so new types should be added
149
		// here and there - no 'edit' here, because that the default for this page
150
		return [
151
			'clear',
152
			'raw',
153
		];
154
	}
155
156
	/**
157
	 * Extract a list of titles from a blob of text, returning
158
	 * (prefixed) strings; unwatchable titles are ignored
159
	 *
160
	 * @param string $list
161
	 * @return array
162
	 */
163
	private function extractTitles( $list ) {
164
		$list = explode( "\n", trim( $list ) );
165
		if ( !is_array( $list ) ) {
166
			return [];
167
		}
168
169
		$titles = [];
170
171
		foreach ( $list as $text ) {
172
			$text = trim( $text );
173
			if ( strlen( $text ) > 0 ) {
174
				$title = Title::newFromText( $text );
175
				if ( $title instanceof Title && $title->isWatchable() ) {
176
					$titles[] = $title;
177
				}
178
			}
179
		}
180
181
		GenderCache::singleton()->doTitlesArray( $titles );
182
183
		$list = [];
184
		/** @var Title $title */
185
		foreach ( $titles as $title ) {
186
			$list[] = $title->getPrefixedText();
187
		}
188
189
		return array_unique( $list );
190
	}
191
192
	public function submitRaw( $data ) {
193
		$wanted = $this->extractTitles( $data['Titles'] );
194
		$current = $this->getWatchlist();
195
196
		if ( count( $wanted ) > 0 ) {
197
			$toWatch = array_diff( $wanted, $current );
198
			$toUnwatch = array_diff( $current, $wanted );
199
			$this->watchTitles( $toWatch );
200
			$this->unwatchTitles( $toUnwatch );
201
			$this->getUser()->invalidateCache();
202
203
			if ( count( $toWatch ) > 0 || count( $toUnwatch ) > 0 ) {
204
				$this->successMessage = $this->msg( 'watchlistedit-raw-done' )->parse();
205
			} else {
206
				return false;
207
			}
208
209 View Code Duplication
			if ( count( $toWatch ) > 0 ) {
210
				$this->successMessage .= ' ' . $this->msg( 'watchlistedit-raw-added' )
211
					->numParams( count( $toWatch ) )->parse();
212
				$this->showTitles( $toWatch, $this->successMessage );
213
			}
214
215 View Code Duplication
			if ( count( $toUnwatch ) > 0 ) {
216
				$this->successMessage .= ' ' . $this->msg( 'watchlistedit-raw-removed' )
217
					->numParams( count( $toUnwatch ) )->parse();
218
				$this->showTitles( $toUnwatch, $this->successMessage );
219
			}
220
		} else {
221
			$this->clearWatchlist();
222
			$this->getUser()->invalidateCache();
223
224
			if ( count( $current ) > 0 ) {
225
				$this->successMessage = $this->msg( 'watchlistedit-raw-done' )->parse();
226
			} else {
227
				return false;
228
			}
229
230
			$this->successMessage .= ' ' . $this->msg( 'watchlistedit-raw-removed' )
231
				->numParams( count( $current ) )->parse();
232
			$this->showTitles( $current, $this->successMessage );
233
		}
234
235
		return true;
236
	}
237
238
	public function submitClear( $data ) {
239
		$current = $this->getWatchlist();
240
		$this->clearWatchlist();
241
		$this->getUser()->invalidateCache();
242
		$this->successMessage = $this->msg( 'watchlistedit-clear-done' )->parse();
243
		$this->successMessage .= ' ' . $this->msg( 'watchlistedit-clear-removed' )
244
			->numParams( count( $current ) )->parse();
245
		$this->showTitles( $current, $this->successMessage );
246
247
		return true;
248
	}
249
250
	/**
251
	 * Print out a list of linked titles
252
	 *
253
	 * $titles can be an array of strings or Title objects; the former
254
	 * is preferred, since Titles are very memory-heavy
255
	 *
256
	 * @param array $titles Array of strings, or Title objects
257
	 * @param string $output
258
	 */
259
	private function showTitles( $titles, &$output ) {
260
		$talk = $this->msg( 'talkpagelinktext' )->escaped();
261
		// Do a batch existence check
262
		$batch = new LinkBatch();
263
		if ( count( $titles ) >= 100 ) {
264
			$output = $this->msg( 'watchlistedit-too-many' )->parse();
265
			return;
266
		}
267
		foreach ( $titles as $title ) {
268
			if ( !$title instanceof Title ) {
269
				$title = Title::newFromText( $title );
270
			}
271
272
			if ( $title instanceof Title ) {
273
				$batch->addObj( $title );
274
				$batch->addObj( $title->getTalkPage() );
275
			}
276
		}
277
278
		$batch->execute();
279
280
		// Print out the list
281
		$output .= "<ul>\n";
282
283
		foreach ( $titles as $title ) {
284
			if ( !$title instanceof Title ) {
285
				$title = Title::newFromText( $title );
286
			}
287
288
			if ( $title instanceof Title ) {
289
				$output .= '<li>' .
290
					Linker::link( $title ) . ' ' .
291
					$this->msg( 'parentheses' )->rawParams(
292
						Linker::link( $title->getTalkPage(), $talk )
293
					)->escaped() .
294
					"</li>\n";
295
			}
296
		}
297
298
		$output .= "</ul>\n";
299
	}
300
301
	/**
302
	 * Prepare a list of titles on a user's watchlist (excluding talk pages)
303
	 * and return an array of (prefixed) strings
304
	 *
305
	 * @return array
306
	 */
307
	private function getWatchlist() {
308
		$list = [];
309
310
		$watchedItems = WatchedItemStore::getDefaultInstance()->getWatchedItemsForUser(
311
			$this->getUser(),
312
			[ 'forWrite' => $this->getRequest()->wasPosted() ]
313
		);
314
315
		if ( $watchedItems ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $watchedItems of type WatchedItem[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
316
			/** @var Title[] $titles */
317
			$titles = [];
318
			foreach ( $watchedItems as $watchedItem ) {
319
				$namespace = $watchedItem->getLinkTarget()->getNamespace();
320
				$dbKey = $watchedItem->getLinkTarget()->getDBkey();
321
				$title = Title::makeTitleSafe( $namespace, $dbKey );
322
323
				if ( $this->checkTitle( $title, $namespace, $dbKey )
0 ignored issues
show
Bug introduced by
It seems like $title defined by \Title::makeTitleSafe($namespace, $dbKey) on line 321 can be null; however, SpecialEditWatchlist::checkTitle() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
324
					&& !$title->isTalkPage()
325
				) {
326
					$titles[] = $title;
327
				}
328
			}
329
330
			GenderCache::singleton()->doTitlesArray( $titles );
331
332
			foreach ( $titles as $title ) {
333
				$list[] = $title->getPrefixedText();
334
			}
335
		}
336
337
		$this->cleanupWatchlist();
338
339
		return $list;
340
	}
341
342
	/**
343
	 * Get a list of titles on a user's watchlist, excluding talk pages,
344
	 * and return as a two-dimensional array with namespace and title.
345
	 *
346
	 * @return array
347
	 */
348
	protected function getWatchlistInfo() {
349
		$titles = [];
350
351
		$watchedItems = WatchedItemStore::getDefaultInstance()
352
			->getWatchedItemsForUser( $this->getUser(), [ 'sort' => WatchedItemStore::SORT_ASC ] );
353
354
		$lb = new LinkBatch();
355
356
		foreach ( $watchedItems as $watchedItem ) {
357
			$namespace = $watchedItem->getLinkTarget()->getNamespace();
358
			$dbKey = $watchedItem->getLinkTarget()->getDBkey();
359
			$lb->add( $namespace, $dbKey );
360
			if ( !MWNamespace::isTalk( $namespace ) ) {
361
				$titles[$namespace][$dbKey] = 1;
362
			}
363
		}
364
365
		$lb->execute();
366
367
		return $titles;
368
	}
369
370
	/**
371
	 * Validates watchlist entry
372
	 *
373
	 * @param Title $title
374
	 * @param int $namespace
375
	 * @param string $dbKey
376
	 * @return bool Whether this item is valid
377
	 */
378
	private function checkTitle( $title, $namespace, $dbKey ) {
379
		if ( $title
380
			&& ( $title->isExternal()
381
				|| $title->getNamespace() < 0
382
			)
383
		) {
384
			$title = false; // unrecoverable
385
		}
386
387
		if ( !$title
388
			|| $title->getNamespace() != $namespace
389
			|| $title->getDBkey() != $dbKey
390
		) {
391
			$this->badItems[] = [ $title, $namespace, $dbKey ];
392
		}
393
394
		return (bool)$title;
395
	}
396
397
	/**
398
	 * Attempts to clean up broken items
399
	 */
400
	private function cleanupWatchlist() {
401
		if ( !count( $this->badItems ) ) {
402
			return; // nothing to do
403
		}
404
405
		$user = $this->getUser();
406
		$store = WatchedItemStore::getDefaultInstance();
407
408
		foreach ( $this->badItems as $row ) {
409
			list( $title, $namespace, $dbKey ) = $row;
410
			$action = $title ? 'cleaning up' : 'deleting';
411
			wfDebug( "User {$user->getName()} has broken watchlist item ns($namespace):$dbKey, $action.\n" );
412
413
			$store->removeWatch( $user, new TitleValue( (int)$namespace, $dbKey ) );
414
415
			// Can't just do an UPDATE instead of DELETE/INSERT due to unique index
416
			if ( $title ) {
417
				$user->addWatch( $title );
418
			}
419
		}
420
	}
421
422
	/**
423
	 * Remove all titles from a user's watchlist
424
	 */
425
	private function clearWatchlist() {
426
		$dbw = wfGetDB( DB_MASTER );
427
		$dbw->delete(
428
			'watchlist',
429
			[ 'wl_user' => $this->getUser()->getId() ],
430
			__METHOD__
431
		);
432
	}
433
434
	/**
435
	 * Add a list of titles to a user's watchlist
436
	 *
437
	 * $titles can be an array of strings or Title objects; the former
438
	 * is preferred, since Titles are very memory-heavy
439
	 *
440
	 * @param array $titles Array of strings, or Title objects
441
	 */
442
	private function watchTitles( $titles ) {
443
		$dbw = wfGetDB( DB_MASTER );
444
		$rows = [];
445
446
		foreach ( $titles as $title ) {
447
			if ( !$title instanceof Title ) {
448
				$title = Title::newFromText( $title );
449
			}
450
451
			if ( $title instanceof Title ) {
452
				$rows[] = [
453
					'wl_user' => $this->getUser()->getId(),
454
					'wl_namespace' => MWNamespace::getSubject( $title->getNamespace() ),
455
					'wl_title' => $title->getDBkey(),
456
					'wl_notificationtimestamp' => null,
457
				];
458
				$rows[] = [
459
					'wl_user' => $this->getUser()->getId(),
460
					'wl_namespace' => MWNamespace::getTalk( $title->getNamespace() ),
461
					'wl_title' => $title->getDBkey(),
462
					'wl_notificationtimestamp' => null,
463
				];
464
			}
465
		}
466
467
		$dbw->insert( 'watchlist', $rows, __METHOD__, 'IGNORE' );
468
	}
469
470
	/**
471
	 * Remove a list of titles from a user's watchlist
472
	 *
473
	 * $titles can be an array of strings or Title objects; the former
474
	 * is preferred, since Titles are very memory-heavy
475
	 *
476
	 * @param array $titles Array of strings, or Title objects
477
	 */
478
	private function unwatchTitles( $titles ) {
479
		$store = WatchedItemStore::getDefaultInstance();
480
481
		foreach ( $titles as $title ) {
482
			if ( !$title instanceof Title ) {
483
				$title = Title::newFromText( $title );
484
			}
485
486
			if ( $title instanceof Title ) {
487
				$store->removeWatch( $this->getUser(), $title->getSubjectPage() );
488
				$store->removeWatch( $this->getUser(), $title->getTalkPage() );
489
490
				$page = WikiPage::factory( $title );
491
				Hooks::run( 'UnwatchArticleComplete', [ $this->getUser(), &$page ] );
492
			}
493
		}
494
	}
495
496
	public function submitNormal( $data ) {
497
		$removed = [];
498
499
		foreach ( $data as $titles ) {
500
			$this->unwatchTitles( $titles );
501
			$removed = array_merge( $removed, $titles );
502
		}
503
504
		if ( count( $removed ) > 0 ) {
505
			$this->successMessage = $this->msg( 'watchlistedit-normal-done'
506
			)->numParams( count( $removed ) )->parse();
507
			$this->showTitles( $removed, $this->successMessage );
508
509
			return true;
510
		} else {
511
			return false;
512
		}
513
	}
514
515
	/**
516
	 * Get the standard watchlist editing form
517
	 *
518
	 * @return HTMLForm
519
	 */
520
	protected function getNormalForm() {
521
		global $wgContLang;
522
523
		$fields = [];
524
		$count = 0;
525
526
		// Allow subscribers to manipulate the list of watched pages (or use it
527
		// to preload lots of details at once)
528
		$watchlistInfo = $this->getWatchlistInfo();
529
		Hooks::run(
530
			'WatchlistEditorBeforeFormRender',
531
			[ &$watchlistInfo ]
532
		);
533
534
		foreach ( $watchlistInfo as $namespace => $pages ) {
535
			$options = [];
536
537
			foreach ( array_keys( $pages ) as $dbkey ) {
538
				$title = Title::makeTitleSafe( $namespace, $dbkey );
539
540
				if ( $this->checkTitle( $title, $namespace, $dbkey ) ) {
0 ignored issues
show
Bug introduced by
It seems like $title defined by \Title::makeTitleSafe($namespace, $dbkey) on line 538 can be null; however, SpecialEditWatchlist::checkTitle() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
541
					$text = $this->buildRemoveLine( $title );
0 ignored issues
show
Bug introduced by
It seems like $title defined by \Title::makeTitleSafe($namespace, $dbkey) on line 538 can be null; however, SpecialEditWatchlist::buildRemoveLine() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
542
					$options[$text] = $title->getPrefixedText();
543
					$count++;
544
				}
545
			}
546
547
			// checkTitle can filter some options out, avoid empty sections
548
			if ( count( $options ) > 0 ) {
549
				$fields['TitlesNs' . $namespace] = [
550
					'class' => 'EditWatchlistCheckboxSeriesField',
551
					'options' => $options,
552
					'section' => "ns$namespace",
553
				];
554
			}
555
		}
556
		$this->cleanupWatchlist();
557
558
		if ( count( $fields ) > 1 && $count > 30 ) {
559
			$this->toc = Linker::tocIndent();
560
			$tocLength = 0;
561
562
			foreach ( $fields as $data ) {
563
				# strip out the 'ns' prefix from the section name:
564
				$ns = substr( $data['section'], 2 );
565
566
				$nsText = ( $ns == NS_MAIN )
567
					? $this->msg( 'blanknamespace' )->escaped()
568
					: htmlspecialchars( $wgContLang->getFormattedNsText( $ns ) );
569
				$this->toc .= Linker::tocLine( "editwatchlist-{$data['section']}", $nsText,
570
					$this->getLanguage()->formatNum( ++$tocLength ), 1 ) . Linker::tocLineEnd();
571
			}
572
573
			$this->toc = Linker::tocList( $this->toc );
574
		} else {
575
			$this->toc = false;
576
		}
577
578
		$context = new DerivativeContext( $this->getContext() );
579
		$context->setTitle( $this->getPageTitle() ); // Remove subpage
580
		$form = new EditWatchlistNormalHTMLForm( $fields, $context );
581
		$form->setSubmitTextMsg( 'watchlistedit-normal-submit' );
582
		$form->setSubmitDestructive();
583
		# Used message keys:
584
		# 'accesskey-watchlistedit-normal-submit', 'tooltip-watchlistedit-normal-submit'
585
		$form->setSubmitTooltip( 'watchlistedit-normal-submit' );
586
		$form->setWrapperLegendMsg( 'watchlistedit-normal-legend' );
587
		$form->addHeaderText( $this->msg( 'watchlistedit-normal-explain' )->parse() );
588
		$form->setSubmitCallback( [ $this, 'submitNormal' ] );
589
590
		return $form;
591
	}
592
593
	/**
594
	 * Build the label for a checkbox, with a link to the title, and various additional bits
595
	 *
596
	 * @param Title $title
597
	 * @return string
598
	 */
599
	private function buildRemoveLine( $title ) {
600
		$link = Linker::link( $title );
601
602
		$tools['talk'] = Linker::link(
0 ignored issues
show
Coding Style Comprehensibility introduced by
$tools was never initialized. Although not strictly required by PHP, it is generally a good practice to add $tools = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
603
			$title->getTalkPage(),
604
			$this->msg( 'talkpagelinktext' )->escaped()
605
		);
606
607
		if ( $title->exists() ) {
608
			$tools['history'] = Linker::linkKnown(
609
				$title,
610
				$this->msg( 'history_short' )->escaped(),
611
				[],
612
				[ 'action' => 'history' ]
613
			);
614
		}
615
616
		if ( $title->getNamespace() == NS_USER && !$title->isSubpage() ) {
617
			$tools['contributions'] = Linker::linkKnown(
618
				SpecialPage::getTitleFor( 'Contributions', $title->getText() ),
619
				$this->msg( 'contributions' )->escaped()
620
			);
621
		}
622
623
		Hooks::run(
624
			'WatchlistEditorBuildRemoveLine',
625
			[ &$tools, $title, $title->isRedirect(), $this->getSkin(), &$link ]
626
		);
627
628
		if ( $title->isRedirect() ) {
629
			// Linker already makes class mw-redirect, so this is redundant
630
			$link = '<span class="watchlistredir">' . $link . '</span>';
631
		}
632
633
		return $link . ' ' .
634
			$this->msg( 'parentheses' )->rawParams( $this->getLanguage()->pipeList( $tools ) )->escaped();
635
	}
636
637
	/**
638
	 * Get a form for editing the watchlist in "raw" mode
639
	 *
640
	 * @return HTMLForm
641
	 */
642
	protected function getRawForm() {
643
		$titles = implode( $this->getWatchlist(), "\n" );
644
		$fields = [
645
			'Titles' => [
646
				'type' => 'textarea',
647
				'label-message' => 'watchlistedit-raw-titles',
648
				'default' => $titles,
649
			],
650
		];
651
		$context = new DerivativeContext( $this->getContext() );
652
		$context->setTitle( $this->getPageTitle( 'raw' ) ); // Reset subpage
653
		$form = new HTMLForm( $fields, $context );
654
		$form->setSubmitTextMsg( 'watchlistedit-raw-submit' );
655
		# Used message keys: 'accesskey-watchlistedit-raw-submit', 'tooltip-watchlistedit-raw-submit'
656
		$form->setSubmitTooltip( 'watchlistedit-raw-submit' );
657
		$form->setWrapperLegendMsg( 'watchlistedit-raw-legend' );
658
		$form->addHeaderText( $this->msg( 'watchlistedit-raw-explain' )->parse() );
659
		$form->setSubmitCallback( [ $this, 'submitRaw' ] );
660
661
		return $form;
662
	}
663
664
	/**
665
	 * Get a form for clearing the watchlist
666
	 *
667
	 * @return HTMLForm
668
	 */
669
	protected function getClearForm() {
670
		$context = new DerivativeContext( $this->getContext() );
671
		$context->setTitle( $this->getPageTitle( 'clear' ) ); // Reset subpage
672
		$form = new HTMLForm( [], $context );
673
		$form->setSubmitTextMsg( 'watchlistedit-clear-submit' );
674
		# Used message keys: 'accesskey-watchlistedit-clear-submit', 'tooltip-watchlistedit-clear-submit'
675
		$form->setSubmitTooltip( 'watchlistedit-clear-submit' );
676
		$form->setWrapperLegendMsg( 'watchlistedit-clear-legend' );
677
		$form->addHeaderText( $this->msg( 'watchlistedit-clear-explain' )->parse() );
678
		$form->setSubmitCallback( [ $this, 'submitClear' ] );
679
		$form->setSubmitDestructive();
680
681
		return $form;
682
	}
683
684
	/**
685
	 * Determine whether we are editing the watchlist, and if so, what
686
	 * kind of editing operation
687
	 *
688
	 * @param WebRequest $request
689
	 * @param string $par
690
	 * @return int
691
	 */
692
	public static function getMode( $request, $par ) {
693
		$mode = strtolower( $request->getVal( 'action', $par ) );
694
695
		switch ( $mode ) {
696
			case 'clear':
697
			case self::EDIT_CLEAR:
698
				return self::EDIT_CLEAR;
699
			case 'raw':
700
			case self::EDIT_RAW:
701
				return self::EDIT_RAW;
702
			case 'edit':
703
			case self::EDIT_NORMAL:
704
				return self::EDIT_NORMAL;
705
			default:
706
				return false;
707
		}
708
	}
709
710
	/**
711
	 * Build a set of links for convenient navigation
712
	 * between watchlist viewing and editing modes
713
	 *
714
	 * @param null $unused
715
	 * @return string
716
	 */
717
	public static function buildTools( $unused ) {
718
		global $wgLang;
719
720
		$tools = [];
721
		$modes = [
722
			'view' => [ 'Watchlist', false ],
723
			'edit' => [ 'EditWatchlist', false ],
724
			'raw' => [ 'EditWatchlist', 'raw' ],
725
			'clear' => [ 'EditWatchlist', 'clear' ],
726
		];
727
728 View Code Duplication
		foreach ( $modes as $mode => $arr ) {
729
			// can use messages 'watchlisttools-view', 'watchlisttools-edit', 'watchlisttools-raw'
730
			$tools[] = Linker::linkKnown(
731
				SpecialPage::getTitleFor( $arr[0], $arr[1] ),
0 ignored issues
show
Security Bug introduced by
It seems like $arr[0] can also be of type false; however, SpecialPage::getTitleFor() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
732
				wfMessage( "watchlisttools-{$mode}" )->escaped()
733
			);
734
		}
735
736
		return Html::rawElement(
737
			'span',
738
			[ 'class' => 'mw-watchlist-toollinks' ],
739
			wfMessage( 'parentheses' )->rawParams( $wgLang->pipeList( $tools ) )->escaped()
740
		);
741
	}
742
}
743
744
/**
745
 * Extend HTMLForm purely so we can have a more sane way of getting the section headers
746
 */
747
class EditWatchlistNormalHTMLForm extends HTMLForm {
748
	public function getLegend( $namespace ) {
749
		$namespace = substr( $namespace, 2 );
750
751
		return $namespace == NS_MAIN
752
			? $this->msg( 'blanknamespace' )->escaped()
753
			: htmlspecialchars( $this->getContext()->getLanguage()->getFormattedNsText( $namespace ) );
754
	}
755
756
	public function getBody() {
757
		return $this->displaySection( $this->mFieldTree, '', 'editwatchlist-' );
758
	}
759
}
760
761
class EditWatchlistCheckboxSeriesField extends HTMLMultiSelectField {
762
	/**
763
	 * HTMLMultiSelectField throws validation errors if we get input data
764
	 * that doesn't match the data set in the form setup. This causes
765
	 * problems if something gets removed from the watchlist while the
766
	 * form is open (bug 32126), but we know that invalid items will
767
	 * be harmless so we can override it here.
768
	 *
769
	 * @param string $value The value the field was submitted with
770
	 * @param array $alldata The data collected from the form
771
	 * @return bool|string Bool true on success, or String error to display.
772
	 */
773
	function validate( $value, $alldata ) {
774
		// Need to call into grandparent to be a good citizen. :)
775
		return HTMLFormField::validate( $value, $alldata );
776
	}
777
}
778