Completed
Branch master (771964)
by
unknown
26:13
created

SpecialEditWatchlist   F

Complexity

Total Complexity 98

Size/Duplication

Total Lines 716
Duplicated Lines 4.61 %

Coupling/Cohesion

Components 1
Dependencies 26

Importance

Changes 3
Bugs 0 Features 1
Metric Value
wmc 98
c 3
b 0
f 1
lcom 1
cbo 26
dl 33
loc 716
rs 1.0434

25 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A initServices() 0 6 2
A doesWrites() 0 3 1
C execute() 16 50 8
A outputSubtitle() 0 5 1
A executeViewEditWatchlist() 0 12 3
A getSubpagesForPrefixSearch() 0 8 1
C extractTitles() 0 28 7
C submitRaw() 10 45 7
A submitClear() 0 11 1
C showTitles() 0 41 8
B getWatchlist() 0 34 6
A getWatchlistInfo() 0 21 3
B checkTitle() 0 18 7
A clearWatchlist() 0 8 1
B watchTitles() 0 23 4
C getNormalForm() 0 72 9
B buildRemoveLine() 0 37 5
A getRawForm() 0 21 1
A getClearForm() 0 14 1
B getMode() 0 17 7
B buildTools() 7 25 2
B cleanupWatchlist() 0 21 5
A unwatchTitles() 0 17 4
A submitNormal() 0 18 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like SpecialEditWatchlist often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SpecialEditWatchlist, and based on these observations, apply Extract Interface, too.

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
	/**
53
	 * @var TitleParser
54
	 */
55
	private $titleParser;
56
57
	public function __construct() {
58
		parent::__construct( 'EditWatchlist', 'editmywatchlist' );
59
	}
60
61
	/**
62
	 * Initialize any services we'll need (unless it has already been provided via a setter).
63
	 * This allows for dependency injection even though we don't control object creation.
64
	 */
65
	private function initServices() {
66
		if ( !$this->titleParser ) {
67
			$lang = $this->getContext()->getLanguage();
68
			$this->titleParser = new MediaWikiTitleCodec( $lang, GenderCache::singleton() );
0 ignored issues
show
Bug introduced by
It seems like \GenderCache::singleton() can be null; however, __construct() 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...
69
		}
70
	}
71
72
	public function doesWrites() {
73
		return true;
74
	}
75
76
	/**
77
	 * Main execution point
78
	 *
79
	 * @param int $mode
80
	 */
81
	public function execute( $mode ) {
82
		$this->initServices();
83
		$this->setHeaders();
84
85
		# Anons don't get a watchlist
86
		$this->requireLogin( 'watchlistanontext' );
87
88
		$out = $this->getOutput();
89
90
		$this->checkPermissions();
91
		$this->checkReadOnly();
92
93
		$this->outputHeader();
94
		$this->outputSubtitle();
95
		$out->addModuleStyles( 'mediawiki.special' );
96
97
		# B/C: $mode used to be waaay down the parameter list, and the first parameter
98
		# was $wgUser
99
		if ( $mode instanceof User ) {
100
			$args = func_get_args();
101
			if ( count( $args ) >= 4 ) {
102
				$mode = $args[3];
103
			}
104
		}
105
		$mode = self::getMode( $this->getRequest(), $mode );
106
107
		switch ( $mode ) {
108 View Code Duplication
			case self::EDIT_RAW:
109
				$out->setPageTitle( $this->msg( 'watchlistedit-raw-title' ) );
110
				$form = $this->getRawForm();
111
				if ( $form->show() ) {
112
					$out->addHTML( $this->successMessage );
113
					$out->addReturnTo( SpecialPage::getTitleFor( 'Watchlist' ) );
114
				}
115
				break;
116 View Code Duplication
			case self::EDIT_CLEAR:
117
				$out->setPageTitle( $this->msg( 'watchlistedit-clear-title' ) );
118
				$form = $this->getClearForm();
119
				if ( $form->show() ) {
120
					$out->addHTML( $this->successMessage );
121
					$out->addReturnTo( SpecialPage::getTitleFor( 'Watchlist' ) );
122
				}
123
				break;
124
125
			case self::EDIT_NORMAL:
126
			default:
127
				$this->executeViewEditWatchlist();
128
				break;
129
		}
130
	}
131
132
	/**
133
	 * Renders a subheader on the watchlist page.
134
	 */
135
	protected function outputSubtitle() {
136
		$out = $this->getOutput();
137
		$out->addSubtitle( $this->msg( 'watchlistfor2', $this->getUser()->getName() )
138
			->rawParams( SpecialEditWatchlist::buildTools( null ) ) );
139
	}
140
141
	/**
142
	 * Executes an edit mode for the watchlist view, from which you can manage your watchlist
143
	 *
144
	 */
145
	protected function executeViewEditWatchlist() {
146
		$out = $this->getOutput();
147
		$out->setPageTitle( $this->msg( 'watchlistedit-normal-title' ) );
148
		$form = $this->getNormalForm();
149
		if ( $form->show() ) {
150
			$out->addHTML( $this->successMessage );
151
			$out->addReturnTo( SpecialPage::getTitleFor( 'Watchlist' ) );
152
		} elseif ( $this->toc !== false ) {
153
			$out->prependHTML( $this->toc );
154
			$out->addModules( 'mediawiki.toc' );
155
		}
156
	}
157
158
	/**
159
	 * Return an array of subpages that this special page will accept.
160
	 *
161
	 * @see also SpecialWatchlist::getSubpagesForPrefixSearch
162
	 * @return string[] subpages
163
	 */
164
	public function getSubpagesForPrefixSearch() {
165
		// SpecialWatchlist uses SpecialEditWatchlist::getMode, so new types should be added
166
		// here and there - no 'edit' here, because that the default for this page
167
		return [
168
			'clear',
169
			'raw',
170
		];
171
	}
172
173
	/**
174
	 * Extract a list of titles from a blob of text, returning
175
	 * (prefixed) strings; unwatchable titles are ignored
176
	 *
177
	 * @param string $list
178
	 * @return array
179
	 */
180
	private function extractTitles( $list ) {
181
		$list = explode( "\n", trim( $list ) );
182
		if ( !is_array( $list ) ) {
183
			return [];
184
		}
185
186
		$titles = [];
187
188
		foreach ( $list as $text ) {
189
			$text = trim( $text );
190
			if ( strlen( $text ) > 0 ) {
191
				$title = Title::newFromText( $text );
192
				if ( $title instanceof Title && $title->isWatchable() ) {
193
					$titles[] = $title;
194
				}
195
			}
196
		}
197
198
		GenderCache::singleton()->doTitlesArray( $titles );
199
200
		$list = [];
201
		/** @var Title $title */
202
		foreach ( $titles as $title ) {
203
			$list[] = $title->getPrefixedText();
204
		}
205
206
		return array_unique( $list );
207
	}
208
209
	public function submitRaw( $data ) {
210
		$wanted = $this->extractTitles( $data['Titles'] );
211
		$current = $this->getWatchlist();
212
213
		if ( count( $wanted ) > 0 ) {
214
			$toWatch = array_diff( $wanted, $current );
215
			$toUnwatch = array_diff( $current, $wanted );
216
			$this->watchTitles( $toWatch );
217
			$this->unwatchTitles( $toUnwatch );
218
			$this->getUser()->invalidateCache();
219
220
			if ( count( $toWatch ) > 0 || count( $toUnwatch ) > 0 ) {
221
				$this->successMessage = $this->msg( 'watchlistedit-raw-done' )->parse();
222
			} else {
223
				return false;
224
			}
225
226 View Code Duplication
			if ( count( $toWatch ) > 0 ) {
227
				$this->successMessage .= ' ' . $this->msg( 'watchlistedit-raw-added' )
228
					->numParams( count( $toWatch ) )->parse();
229
				$this->showTitles( $toWatch, $this->successMessage );
230
			}
231
232 View Code Duplication
			if ( count( $toUnwatch ) > 0 ) {
233
				$this->successMessage .= ' ' . $this->msg( 'watchlistedit-raw-removed' )
234
					->numParams( count( $toUnwatch ) )->parse();
235
				$this->showTitles( $toUnwatch, $this->successMessage );
236
			}
237
		} else {
238
			$this->clearWatchlist();
239
			$this->getUser()->invalidateCache();
240
241
			if ( count( $current ) > 0 ) {
242
				$this->successMessage = $this->msg( 'watchlistedit-raw-done' )->parse();
243
			} else {
244
				return false;
245
			}
246
247
			$this->successMessage .= ' ' . $this->msg( 'watchlistedit-raw-removed' )
248
				->numParams( count( $current ) )->parse();
249
			$this->showTitles( $current, $this->successMessage );
250
		}
251
252
		return true;
253
	}
254
255
	public function submitClear( $data ) {
256
		$current = $this->getWatchlist();
257
		$this->clearWatchlist();
258
		$this->getUser()->invalidateCache();
259
		$this->successMessage = $this->msg( 'watchlistedit-clear-done' )->parse();
260
		$this->successMessage .= ' ' . $this->msg( 'watchlistedit-clear-removed' )
261
			->numParams( count( $current ) )->parse();
262
		$this->showTitles( $current, $this->successMessage );
263
264
		return true;
265
	}
266
267
	/**
268
	 * Print out a list of linked titles
269
	 *
270
	 * $titles can be an array of strings or Title objects; the former
271
	 * is preferred, since Titles are very memory-heavy
272
	 *
273
	 * @param array $titles Array of strings, or Title objects
274
	 * @param string $output
275
	 */
276
	private function showTitles( $titles, &$output ) {
277
		$talk = $this->msg( 'talkpagelinktext' )->escaped();
278
		// Do a batch existence check
279
		$batch = new LinkBatch();
280
		if ( count( $titles ) >= 100 ) {
281
			$output = $this->msg( 'watchlistedit-too-many' )->parse();
282
			return;
283
		}
284
		foreach ( $titles as $title ) {
285
			if ( !$title instanceof Title ) {
286
				$title = Title::newFromText( $title );
287
			}
288
289
			if ( $title instanceof Title ) {
290
				$batch->addObj( $title );
291
				$batch->addObj( $title->getTalkPage() );
292
			}
293
		}
294
295
		$batch->execute();
296
297
		// Print out the list
298
		$output .= "<ul>\n";
299
300
		foreach ( $titles as $title ) {
301
			if ( !$title instanceof Title ) {
302
				$title = Title::newFromText( $title );
303
			}
304
305
			if ( $title instanceof Title ) {
306
				$output .= '<li>' .
307
					Linker::link( $title ) . ' ' .
308
					$this->msg( 'parentheses' )->rawParams(
309
						Linker::link( $title->getTalkPage(), $talk )
310
					)->escaped() .
311
					"</li>\n";
312
			}
313
		}
314
315
		$output .= "</ul>\n";
316
	}
317
318
	/**
319
	 * Prepare a list of titles on a user's watchlist (excluding talk pages)
320
	 * and return an array of (prefixed) strings
321
	 *
322
	 * @return array
323
	 */
324
	private function getWatchlist() {
325
		$list = [];
326
327
		$watchedItems = WatchedItemStore::getDefaultInstance()->getWatchedItemsForUser(
328
			$this->getUser(),
329
			[ 'forWrite' => $this->getRequest()->wasPosted() ]
330
		);
331
332
		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...
333
			/** @var Title[] $titles */
334
			$titles = [];
335
			foreach ( $watchedItems as $watchedItem ) {
336
				$namespace = $watchedItem->getLinkTarget()->getNamespace();
337
				$dbKey = $watchedItem->getLinkTarget()->getDBkey();
338
				$title = Title::makeTitleSafe( $namespace, $dbKey );
339
340
				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 338 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...
341
					&& !$title->isTalkPage()
342
				) {
343
					$titles[] = $title;
344
				}
345
			}
346
347
			GenderCache::singleton()->doTitlesArray( $titles );
348
349
			foreach ( $titles as $title ) {
350
				$list[] = $title->getPrefixedText();
351
			}
352
		}
353
354
		$this->cleanupWatchlist();
355
356
		return $list;
357
	}
358
359
	/**
360
	 * Get a list of titles on a user's watchlist, excluding talk pages,
361
	 * and return as a two-dimensional array with namespace and title.
362
	 *
363
	 * @return array
364
	 */
365
	protected function getWatchlistInfo() {
366
		$titles = [];
367
368
		$watchedItems = WatchedItemStore::getDefaultInstance()
369
			->getWatchedItemsForUser( $this->getUser(), [ 'sort' => WatchedItemStore::SORT_ASC ] );
370
371
		$lb = new LinkBatch();
372
373
		foreach ( $watchedItems as $watchedItem ) {
374
			$namespace = $watchedItem->getLinkTarget()->getNamespace();
375
			$dbKey = $watchedItem->getLinkTarget()->getDBkey();
376
			$lb->add( $namespace, $dbKey );
377
			if ( !MWNamespace::isTalk( $namespace ) ) {
378
				$titles[$namespace][$dbKey] = 1;
379
			}
380
		}
381
382
		$lb->execute();
383
384
		return $titles;
385
	}
386
387
	/**
388
	 * Validates watchlist entry
389
	 *
390
	 * @param Title $title
391
	 * @param int $namespace
392
	 * @param string $dbKey
393
	 * @return bool Whether this item is valid
394
	 */
395
	private function checkTitle( $title, $namespace, $dbKey ) {
396
		if ( $title
397
			&& ( $title->isExternal()
398
				|| $title->getNamespace() < 0
399
			)
400
		) {
401
			$title = false; // unrecoverable
402
		}
403
404
		if ( !$title
405
			|| $title->getNamespace() != $namespace
406
			|| $title->getDBkey() != $dbKey
407
		) {
408
			$this->badItems[] = [ $title, $namespace, $dbKey ];
409
		}
410
411
		return (bool)$title;
412
	}
413
414
	/**
415
	 * Attempts to clean up broken items
416
	 */
417
	private function cleanupWatchlist() {
418
		if ( !count( $this->badItems ) ) {
419
			return; // nothing to do
420
		}
421
422
		$user = $this->getUser();
423
		$store = WatchedItemStore::getDefaultInstance();
424
425
		foreach ( $this->badItems as $row ) {
426
			list( $title, $namespace, $dbKey ) = $row;
427
			$action = $title ? 'cleaning up' : 'deleting';
428
			wfDebug( "User {$user->getName()} has broken watchlist item ns($namespace):$dbKey, $action.\n" );
429
430
			$store->removeWatch( $user, new TitleValue( (int)$namespace, $dbKey ) );
431
432
			// Can't just do an UPDATE instead of DELETE/INSERT due to unique index
433
			if ( $title ) {
434
				$user->addWatch( $title );
435
			}
436
		}
437
	}
438
439
	/**
440
	 * Remove all titles from a user's watchlist
441
	 */
442
	private function clearWatchlist() {
443
		$dbw = wfGetDB( DB_MASTER );
444
		$dbw->delete(
445
			'watchlist',
446
			[ 'wl_user' => $this->getUser()->getId() ],
447
			__METHOD__
448
		);
449
	}
450
451
	/**
452
	 * Add a list of targets to a user's watchlist
453
	 *
454
	 * @param string[]|LinkTarget[] $targets
455
	 */
456
	private function watchTitles( $targets ) {
457
		$expandedTargets = [];
458
		foreach ( $targets as $target ) {
459
			if ( !$target instanceof LinkTarget ) {
460
				try {
461
					$target = $this->titleParser->parseTitle( $target, NS_MAIN );
462
				}
463
				catch ( MalformedTitleException $e ) {
464
					continue;
465
				}
466
			}
467
468
			$ns = $target->getNamespace();
469
			$dbKey = $target->getDBkey();
470
			$expandedTargets[] = new TitleValue( MWNamespace::getSubject( $ns ), $dbKey );
471
			$expandedTargets[] = new TitleValue( MWNamespace::getTalk( $ns ), $dbKey );
472
		}
473
474
		WatchedItemStore::getDefaultInstance()->addWatchBatchForUser(
475
			$this->getUser(),
476
			$expandedTargets
477
		);
478
	}
479
480
	/**
481
	 * Remove a list of titles from a user's watchlist
482
	 *
483
	 * $titles can be an array of strings or Title objects; the former
484
	 * is preferred, since Titles are very memory-heavy
485
	 *
486
	 * @param array $titles Array of strings, or Title objects
487
	 */
488
	private function unwatchTitles( $titles ) {
489
		$store = WatchedItemStore::getDefaultInstance();
490
491
		foreach ( $titles as $title ) {
492
			if ( !$title instanceof Title ) {
493
				$title = Title::newFromText( $title );
494
			}
495
496
			if ( $title instanceof Title ) {
497
				$store->removeWatch( $this->getUser(), $title->getSubjectPage() );
498
				$store->removeWatch( $this->getUser(), $title->getTalkPage() );
499
500
				$page = WikiPage::factory( $title );
501
				Hooks::run( 'UnwatchArticleComplete', [ $this->getUser(), &$page ] );
502
			}
503
		}
504
	}
505
506
	public function submitNormal( $data ) {
507
		$removed = [];
508
509
		foreach ( $data as $titles ) {
510
			$this->unwatchTitles( $titles );
511
			$removed = array_merge( $removed, $titles );
512
		}
513
514
		if ( count( $removed ) > 0 ) {
515
			$this->successMessage = $this->msg( 'watchlistedit-normal-done'
516
			)->numParams( count( $removed ) )->parse();
517
			$this->showTitles( $removed, $this->successMessage );
518
519
			return true;
520
		} else {
521
			return false;
522
		}
523
	}
524
525
	/**
526
	 * Get the standard watchlist editing form
527
	 *
528
	 * @return HTMLForm
529
	 */
530
	protected function getNormalForm() {
531
		global $wgContLang;
532
533
		$fields = [];
534
		$count = 0;
535
536
		// Allow subscribers to manipulate the list of watched pages (or use it
537
		// to preload lots of details at once)
538
		$watchlistInfo = $this->getWatchlistInfo();
539
		Hooks::run(
540
			'WatchlistEditorBeforeFormRender',
541
			[ &$watchlistInfo ]
542
		);
543
544
		foreach ( $watchlistInfo as $namespace => $pages ) {
545
			$options = [];
546
547
			foreach ( array_keys( $pages ) as $dbkey ) {
548
				$title = Title::makeTitleSafe( $namespace, $dbkey );
549
550
				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 548 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...
551
					$text = $this->buildRemoveLine( $title );
0 ignored issues
show
Bug introduced by
It seems like $title defined by \Title::makeTitleSafe($namespace, $dbkey) on line 548 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...
552
					$options[$text] = $title->getPrefixedText();
553
					$count++;
554
				}
555
			}
556
557
			// checkTitle can filter some options out, avoid empty sections
558
			if ( count( $options ) > 0 ) {
559
				$fields['TitlesNs' . $namespace] = [
560
					'class' => 'EditWatchlistCheckboxSeriesField',
561
					'options' => $options,
562
					'section' => "ns$namespace",
563
				];
564
			}
565
		}
566
		$this->cleanupWatchlist();
567
568
		if ( count( $fields ) > 1 && $count > 30 ) {
569
			$this->toc = Linker::tocIndent();
570
			$tocLength = 0;
571
572
			foreach ( $fields as $data ) {
573
				# strip out the 'ns' prefix from the section name:
574
				$ns = substr( $data['section'], 2 );
575
576
				$nsText = ( $ns == NS_MAIN )
577
					? $this->msg( 'blanknamespace' )->escaped()
578
					: htmlspecialchars( $wgContLang->getFormattedNsText( $ns ) );
579
				$this->toc .= Linker::tocLine( "editwatchlist-{$data['section']}", $nsText,
580
					$this->getLanguage()->formatNum( ++$tocLength ), 1 ) . Linker::tocLineEnd();
581
			}
582
583
			$this->toc = Linker::tocList( $this->toc );
584
		} else {
585
			$this->toc = false;
586
		}
587
588
		$context = new DerivativeContext( $this->getContext() );
589
		$context->setTitle( $this->getPageTitle() ); // Remove subpage
590
		$form = new EditWatchlistNormalHTMLForm( $fields, $context );
591
		$form->setSubmitTextMsg( 'watchlistedit-normal-submit' );
592
		$form->setSubmitDestructive();
593
		# Used message keys:
594
		# 'accesskey-watchlistedit-normal-submit', 'tooltip-watchlistedit-normal-submit'
595
		$form->setSubmitTooltip( 'watchlistedit-normal-submit' );
596
		$form->setWrapperLegendMsg( 'watchlistedit-normal-legend' );
597
		$form->addHeaderText( $this->msg( 'watchlistedit-normal-explain' )->parse() );
598
		$form->setSubmitCallback( [ $this, 'submitNormal' ] );
599
600
		return $form;
601
	}
602
603
	/**
604
	 * Build the label for a checkbox, with a link to the title, and various additional bits
605
	 *
606
	 * @param Title $title
607
	 * @return string
608
	 */
609
	private function buildRemoveLine( $title ) {
610
		$link = Linker::link( $title );
611
612
		$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...
613
			$title->getTalkPage(),
614
			$this->msg( 'talkpagelinktext' )->escaped()
615
		);
616
617
		if ( $title->exists() ) {
618
			$tools['history'] = Linker::linkKnown(
619
				$title,
620
				$this->msg( 'history_short' )->escaped(),
621
				[],
622
				[ 'action' => 'history' ]
623
			);
624
		}
625
626
		if ( $title->getNamespace() == NS_USER && !$title->isSubpage() ) {
627
			$tools['contributions'] = Linker::linkKnown(
628
				SpecialPage::getTitleFor( 'Contributions', $title->getText() ),
629
				$this->msg( 'contributions' )->escaped()
630
			);
631
		}
632
633
		Hooks::run(
634
			'WatchlistEditorBuildRemoveLine',
635
			[ &$tools, $title, $title->isRedirect(), $this->getSkin(), &$link ]
636
		);
637
638
		if ( $title->isRedirect() ) {
639
			// Linker already makes class mw-redirect, so this is redundant
640
			$link = '<span class="watchlistredir">' . $link . '</span>';
641
		}
642
643
		return $link . ' ' .
644
			$this->msg( 'parentheses' )->rawParams( $this->getLanguage()->pipeList( $tools ) )->escaped();
645
	}
646
647
	/**
648
	 * Get a form for editing the watchlist in "raw" mode
649
	 *
650
	 * @return HTMLForm
651
	 */
652
	protected function getRawForm() {
653
		$titles = implode( $this->getWatchlist(), "\n" );
654
		$fields = [
655
			'Titles' => [
656
				'type' => 'textarea',
657
				'label-message' => 'watchlistedit-raw-titles',
658
				'default' => $titles,
659
			],
660
		];
661
		$context = new DerivativeContext( $this->getContext() );
662
		$context->setTitle( $this->getPageTitle( 'raw' ) ); // Reset subpage
663
		$form = new HTMLForm( $fields, $context );
664
		$form->setSubmitTextMsg( 'watchlistedit-raw-submit' );
665
		# Used message keys: 'accesskey-watchlistedit-raw-submit', 'tooltip-watchlistedit-raw-submit'
666
		$form->setSubmitTooltip( 'watchlistedit-raw-submit' );
667
		$form->setWrapperLegendMsg( 'watchlistedit-raw-legend' );
668
		$form->addHeaderText( $this->msg( 'watchlistedit-raw-explain' )->parse() );
669
		$form->setSubmitCallback( [ $this, 'submitRaw' ] );
670
671
		return $form;
672
	}
673
674
	/**
675
	 * Get a form for clearing the watchlist
676
	 *
677
	 * @return HTMLForm
678
	 */
679
	protected function getClearForm() {
680
		$context = new DerivativeContext( $this->getContext() );
681
		$context->setTitle( $this->getPageTitle( 'clear' ) ); // Reset subpage
682
		$form = new HTMLForm( [], $context );
683
		$form->setSubmitTextMsg( 'watchlistedit-clear-submit' );
684
		# Used message keys: 'accesskey-watchlistedit-clear-submit', 'tooltip-watchlistedit-clear-submit'
685
		$form->setSubmitTooltip( 'watchlistedit-clear-submit' );
686
		$form->setWrapperLegendMsg( 'watchlistedit-clear-legend' );
687
		$form->addHeaderText( $this->msg( 'watchlistedit-clear-explain' )->parse() );
688
		$form->setSubmitCallback( [ $this, 'submitClear' ] );
689
		$form->setSubmitDestructive();
690
691
		return $form;
692
	}
693
694
	/**
695
	 * Determine whether we are editing the watchlist, and if so, what
696
	 * kind of editing operation
697
	 *
698
	 * @param WebRequest $request
699
	 * @param string $par
700
	 * @return int
701
	 */
702
	public static function getMode( $request, $par ) {
703
		$mode = strtolower( $request->getVal( 'action', $par ) );
704
705
		switch ( $mode ) {
706
			case 'clear':
707
			case self::EDIT_CLEAR:
708
				return self::EDIT_CLEAR;
709
			case 'raw':
710
			case self::EDIT_RAW:
711
				return self::EDIT_RAW;
712
			case 'edit':
713
			case self::EDIT_NORMAL:
714
				return self::EDIT_NORMAL;
715
			default:
716
				return false;
717
		}
718
	}
719
720
	/**
721
	 * Build a set of links for convenient navigation
722
	 * between watchlist viewing and editing modes
723
	 *
724
	 * @param null $unused
725
	 * @return string
726
	 */
727
	public static function buildTools( $unused ) {
728
		global $wgLang;
729
730
		$tools = [];
731
		$modes = [
732
			'view' => [ 'Watchlist', false ],
733
			'edit' => [ 'EditWatchlist', false ],
734
			'raw' => [ 'EditWatchlist', 'raw' ],
735
			'clear' => [ 'EditWatchlist', 'clear' ],
736
		];
737
738 View Code Duplication
		foreach ( $modes as $mode => $arr ) {
739
			// can use messages 'watchlisttools-view', 'watchlisttools-edit', 'watchlisttools-raw'
740
			$tools[] = Linker::linkKnown(
741
				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...
742
				wfMessage( "watchlisttools-{$mode}" )->escaped()
743
			);
744
		}
745
746
		return Html::rawElement(
747
			'span',
748
			[ 'class' => 'mw-watchlist-toollinks' ],
749
			wfMessage( 'parentheses' )->rawParams( $wgLang->pipeList( $tools ) )->escaped()
750
		);
751
	}
752
}
753
754
/**
755
 * Extend HTMLForm purely so we can have a more sane way of getting the section headers
756
 */
757
class EditWatchlistNormalHTMLForm extends HTMLForm {
758
	public function getLegend( $namespace ) {
759
		$namespace = substr( $namespace, 2 );
760
761
		return $namespace == NS_MAIN
762
			? $this->msg( 'blanknamespace' )->escaped()
763
			: htmlspecialchars( $this->getContext()->getLanguage()->getFormattedNsText( $namespace ) );
764
	}
765
766
	public function getBody() {
767
		return $this->displaySection( $this->mFieldTree, '', 'editwatchlist-' );
768
	}
769
}
770
771
class EditWatchlistCheckboxSeriesField extends HTMLMultiSelectField {
772
	/**
773
	 * HTMLMultiSelectField throws validation errors if we get input data
774
	 * that doesn't match the data set in the form setup. This causes
775
	 * problems if something gets removed from the watchlist while the
776
	 * form is open (bug 32126), but we know that invalid items will
777
	 * be harmless so we can override it here.
778
	 *
779
	 * @param string $value The value the field was submitted with
780
	 * @param array $alldata The data collected from the form
781
	 * @return bool|string Bool true on success, or String error to display.
782
	 */
783
	function validate( $value, $alldata ) {
784
		// Need to call into grandparent to be a good citizen. :)
785
		return HTMLFormField::validate( $value, $alldata );
786
	}
787
}
788