Completed
Branch master (98c62a)
by
unknown
37:10
created

SpecialEditWatchlist::getNormalForm()   C

Complexity

Conditions 9
Paths 14

Size

Total Lines 72
Code Lines 45

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 72
rs 6.0413
cc 9
eloc 45
nc 14
nop 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
		$dbr = wfGetDB( DB_SLAVE );
351
352
		$res = $dbr->select(
353
			[ 'watchlist' ],
354
			[ 'wl_namespace', 'wl_title' ],
355
			[ 'wl_user' => $this->getUser()->getId() ],
356
			__METHOD__,
357
			[ 'ORDER BY' => [ 'wl_namespace', 'wl_title' ] ]
358
		);
359
360
		$lb = new LinkBatch();
361
362
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
363
			$lb->add( $row->wl_namespace, $row->wl_title );
364
			if ( !MWNamespace::isTalk( $row->wl_namespace ) ) {
365
				$titles[$row->wl_namespace][$row->wl_title] = 1;
366
			}
367
		}
368
369
		$lb->execute();
370
371
		return $titles;
372
	}
373
374
	/**
375
	 * Validates watchlist entry
376
	 *
377
	 * @param Title $title
378
	 * @param int $namespace
379
	 * @param string $dbKey
380
	 * @return bool Whether this item is valid
381
	 */
382
	private function checkTitle( $title, $namespace, $dbKey ) {
383
		if ( $title
384
			&& ( $title->isExternal()
385
				|| $title->getNamespace() < 0
386
			)
387
		) {
388
			$title = false; // unrecoverable
389
		}
390
391
		if ( !$title
392
			|| $title->getNamespace() != $namespace
393
			|| $title->getDBkey() != $dbKey
394
		) {
395
			$this->badItems[] = [ $title, $namespace, $dbKey ];
396
		}
397
398
		return (bool)$title;
399
	}
400
401
	/**
402
	 * Attempts to clean up broken items
403
	 */
404
	private function cleanupWatchlist() {
405
		if ( !count( $this->badItems ) ) {
406
			return; // nothing to do
407
		}
408
409
		$user = $this->getUser();
410
		$store = WatchedItemStore::getDefaultInstance();
411
412
		foreach ( $this->badItems as $row ) {
413
			list( $title, $namespace, $dbKey ) = $row;
414
			$action = $title ? 'cleaning up' : 'deleting';
415
			wfDebug( "User {$user->getName()} has broken watchlist item ns($namespace):$dbKey, $action.\n" );
416
417
			$store->removeWatch( $user, new TitleValue( $namespace, $dbKey ) );
418
419
			// Can't just do an UPDATE instead of DELETE/INSERT due to unique index
420
			if ( $title ) {
421
				$user->addWatch( $title );
422
			}
423
		}
424
	}
425
426
	/**
427
	 * Remove all titles from a user's watchlist
428
	 */
429
	private function clearWatchlist() {
430
		$dbw = wfGetDB( DB_MASTER );
431
		$dbw->delete(
432
			'watchlist',
433
			[ 'wl_user' => $this->getUser()->getId() ],
434
			__METHOD__
435
		);
436
	}
437
438
	/**
439
	 * Add a list of titles to a user's watchlist
440
	 *
441
	 * $titles can be an array of strings or Title objects; the former
442
	 * is preferred, since Titles are very memory-heavy
443
	 *
444
	 * @param array $titles Array of strings, or Title objects
445
	 */
446
	private function watchTitles( $titles ) {
447
		$dbw = wfGetDB( DB_MASTER );
448
		$rows = [];
449
450
		foreach ( $titles as $title ) {
451
			if ( !$title instanceof Title ) {
452
				$title = Title::newFromText( $title );
453
			}
454
455
			if ( $title instanceof Title ) {
456
				$rows[] = [
457
					'wl_user' => $this->getUser()->getId(),
458
					'wl_namespace' => MWNamespace::getSubject( $title->getNamespace() ),
459
					'wl_title' => $title->getDBkey(),
460
					'wl_notificationtimestamp' => null,
461
				];
462
				$rows[] = [
463
					'wl_user' => $this->getUser()->getId(),
464
					'wl_namespace' => MWNamespace::getTalk( $title->getNamespace() ),
465
					'wl_title' => $title->getDBkey(),
466
					'wl_notificationtimestamp' => null,
467
				];
468
			}
469
		}
470
471
		$dbw->insert( 'watchlist', $rows, __METHOD__, 'IGNORE' );
472
	}
473
474
	/**
475
	 * Remove a list of titles from a user's watchlist
476
	 *
477
	 * $titles can be an array of strings or Title objects; the former
478
	 * is preferred, since Titles are very memory-heavy
479
	 *
480
	 * @param array $titles Array of strings, or Title objects
481
	 */
482
	private function unwatchTitles( $titles ) {
483
		$store = WatchedItemStore::getDefaultInstance();
484
485
		foreach ( $titles as $title ) {
486
			if ( !$title instanceof Title ) {
487
				$title = Title::newFromText( $title );
488
			}
489
490
			if ( $title instanceof Title ) {
491
				$store->removeWatch( $this->getUser(), $title->getSubjectPage() );
492
				$store->removeWatch( $this->getUser(), $title->getTalkPage() );
493
494
				$page = WikiPage::factory( $title );
495
				Hooks::run( 'UnwatchArticleComplete', [ $this->getUser(), &$page ] );
496
			}
497
		}
498
	}
499
500
	public function submitNormal( $data ) {
501
		$removed = [];
502
503
		foreach ( $data as $titles ) {
504
			$this->unwatchTitles( $titles );
505
			$removed = array_merge( $removed, $titles );
506
		}
507
508
		if ( count( $removed ) > 0 ) {
509
			$this->successMessage = $this->msg( 'watchlistedit-normal-done'
510
			)->numParams( count( $removed ) )->parse();
511
			$this->showTitles( $removed, $this->successMessage );
512
513
			return true;
514
		} else {
515
			return false;
516
		}
517
	}
518
519
	/**
520
	 * Get the standard watchlist editing form
521
	 *
522
	 * @return HTMLForm
523
	 */
524
	protected function getNormalForm() {
525
		global $wgContLang;
526
527
		$fields = [];
528
		$count = 0;
529
530
		// Allow subscribers to manipulate the list of watched pages (or use it
531
		// to preload lots of details at once)
532
		$watchlistInfo = $this->getWatchlistInfo();
533
		Hooks::run(
534
			'WatchlistEditorBeforeFormRender',
535
			[ &$watchlistInfo ]
536
		);
537
538
		foreach ( $watchlistInfo as $namespace => $pages ) {
539
			$options = [];
540
541
			foreach ( array_keys( $pages ) as $dbkey ) {
542
				$title = Title::makeTitleSafe( $namespace, $dbkey );
543
544
				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 542 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...
545
					$text = $this->buildRemoveLine( $title );
0 ignored issues
show
Bug introduced by
It seems like $title defined by \Title::makeTitleSafe($namespace, $dbkey) on line 542 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...
546
					$options[$text] = $title->getPrefixedText();
547
					$count++;
548
				}
549
			}
550
551
			// checkTitle can filter some options out, avoid empty sections
552
			if ( count( $options ) > 0 ) {
553
				$fields['TitlesNs' . $namespace] = [
554
					'class' => 'EditWatchlistCheckboxSeriesField',
555
					'options' => $options,
556
					'section' => "ns$namespace",
557
				];
558
			}
559
		}
560
		$this->cleanupWatchlist();
561
562
		if ( count( $fields ) > 1 && $count > 30 ) {
563
			$this->toc = Linker::tocIndent();
564
			$tocLength = 0;
565
566
			foreach ( $fields as $data ) {
567
				# strip out the 'ns' prefix from the section name:
568
				$ns = substr( $data['section'], 2 );
569
570
				$nsText = ( $ns == NS_MAIN )
571
					? $this->msg( 'blanknamespace' )->escaped()
572
					: htmlspecialchars( $wgContLang->getFormattedNsText( $ns ) );
573
				$this->toc .= Linker::tocLine( "editwatchlist-{$data['section']}", $nsText,
574
					$this->getLanguage()->formatNum( ++$tocLength ), 1 ) . Linker::tocLineEnd();
575
			}
576
577
			$this->toc = Linker::tocList( $this->toc );
578
		} else {
579
			$this->toc = false;
580
		}
581
582
		$context = new DerivativeContext( $this->getContext() );
583
		$context->setTitle( $this->getPageTitle() ); // Remove subpage
584
		$form = new EditWatchlistNormalHTMLForm( $fields, $context );
585
		$form->setSubmitTextMsg( 'watchlistedit-normal-submit' );
586
		$form->setSubmitDestructive();
587
		# Used message keys:
588
		# 'accesskey-watchlistedit-normal-submit', 'tooltip-watchlistedit-normal-submit'
589
		$form->setSubmitTooltip( 'watchlistedit-normal-submit' );
590
		$form->setWrapperLegendMsg( 'watchlistedit-normal-legend' );
591
		$form->addHeaderText( $this->msg( 'watchlistedit-normal-explain' )->parse() );
592
		$form->setSubmitCallback( [ $this, 'submitNormal' ] );
593
594
		return $form;
595
	}
596
597
	/**
598
	 * Build the label for a checkbox, with a link to the title, and various additional bits
599
	 *
600
	 * @param Title $title
601
	 * @return string
602
	 */
603
	private function buildRemoveLine( $title ) {
604
		$link = Linker::link( $title );
605
606
		$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...
607
			$title->getTalkPage(),
608
			$this->msg( 'talkpagelinktext' )->escaped()
609
		);
610
611
		if ( $title->exists() ) {
612
			$tools['history'] = Linker::linkKnown(
613
				$title,
614
				$this->msg( 'history_short' )->escaped(),
615
				[],
616
				[ 'action' => 'history' ]
617
			);
618
		}
619
620
		if ( $title->getNamespace() == NS_USER && !$title->isSubpage() ) {
621
			$tools['contributions'] = Linker::linkKnown(
622
				SpecialPage::getTitleFor( 'Contributions', $title->getText() ),
623
				$this->msg( 'contributions' )->escaped()
624
			);
625
		}
626
627
		Hooks::run(
628
			'WatchlistEditorBuildRemoveLine',
629
			[ &$tools, $title, $title->isRedirect(), $this->getSkin(), &$link ]
630
		);
631
632
		if ( $title->isRedirect() ) {
633
			// Linker already makes class mw-redirect, so this is redundant
634
			$link = '<span class="watchlistredir">' . $link . '</span>';
635
		}
636
637
		return $link . ' ' .
638
			$this->msg( 'parentheses' )->rawParams( $this->getLanguage()->pipeList( $tools ) )->escaped();
639
	}
640
641
	/**
642
	 * Get a form for editing the watchlist in "raw" mode
643
	 *
644
	 * @return HTMLForm
645
	 */
646
	protected function getRawForm() {
647
		$titles = implode( $this->getWatchlist(), "\n" );
648
		$fields = [
649
			'Titles' => [
650
				'type' => 'textarea',
651
				'label-message' => 'watchlistedit-raw-titles',
652
				'default' => $titles,
653
			],
654
		];
655
		$context = new DerivativeContext( $this->getContext() );
656
		$context->setTitle( $this->getPageTitle( 'raw' ) ); // Reset subpage
657
		$form = new HTMLForm( $fields, $context );
658
		$form->setSubmitTextMsg( 'watchlistedit-raw-submit' );
659
		# Used message keys: 'accesskey-watchlistedit-raw-submit', 'tooltip-watchlistedit-raw-submit'
660
		$form->setSubmitTooltip( 'watchlistedit-raw-submit' );
661
		$form->setWrapperLegendMsg( 'watchlistedit-raw-legend' );
662
		$form->addHeaderText( $this->msg( 'watchlistedit-raw-explain' )->parse() );
663
		$form->setSubmitCallback( [ $this, 'submitRaw' ] );
664
665
		return $form;
666
	}
667
668
	/**
669
	 * Get a form for clearing the watchlist
670
	 *
671
	 * @return HTMLForm
672
	 */
673
	protected function getClearForm() {
674
		$context = new DerivativeContext( $this->getContext() );
675
		$context->setTitle( $this->getPageTitle( 'clear' ) ); // Reset subpage
676
		$form = new HTMLForm( [], $context );
677
		$form->setSubmitTextMsg( 'watchlistedit-clear-submit' );
678
		# Used message keys: 'accesskey-watchlistedit-clear-submit', 'tooltip-watchlistedit-clear-submit'
679
		$form->setSubmitTooltip( 'watchlistedit-clear-submit' );
680
		$form->setWrapperLegendMsg( 'watchlistedit-clear-legend' );
681
		$form->addHeaderText( $this->msg( 'watchlistedit-clear-explain' )->parse() );
682
		$form->setSubmitCallback( [ $this, 'submitClear' ] );
683
		$form->setSubmitDestructive();
684
685
		return $form;
686
	}
687
688
	/**
689
	 * Determine whether we are editing the watchlist, and if so, what
690
	 * kind of editing operation
691
	 *
692
	 * @param WebRequest $request
693
	 * @param string $par
694
	 * @return int
695
	 */
696
	public static function getMode( $request, $par ) {
697
		$mode = strtolower( $request->getVal( 'action', $par ) );
698
699
		switch ( $mode ) {
700
			case 'clear':
701
			case self::EDIT_CLEAR:
702
				return self::EDIT_CLEAR;
703
			case 'raw':
704
			case self::EDIT_RAW:
705
				return self::EDIT_RAW;
706
			case 'edit':
707
			case self::EDIT_NORMAL:
708
				return self::EDIT_NORMAL;
709
			default:
710
				return false;
711
		}
712
	}
713
714
	/**
715
	 * Build a set of links for convenient navigation
716
	 * between watchlist viewing and editing modes
717
	 *
718
	 * @param null $unused
719
	 * @return string
720
	 */
721
	public static function buildTools( $unused ) {
722
		global $wgLang;
723
724
		$tools = [];
725
		$modes = [
726
			'view' => [ 'Watchlist', false ],
727
			'edit' => [ 'EditWatchlist', false ],
728
			'raw' => [ 'EditWatchlist', 'raw' ],
729
			'clear' => [ 'EditWatchlist', 'clear' ],
730
		];
731
732 View Code Duplication
		foreach ( $modes as $mode => $arr ) {
733
			// can use messages 'watchlisttools-view', 'watchlisttools-edit', 'watchlisttools-raw'
734
			$tools[] = Linker::linkKnown(
735
				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...
736
				wfMessage( "watchlisttools-{$mode}" )->escaped()
737
			);
738
		}
739
740
		return Html::rawElement(
741
			'span',
742
			[ 'class' => 'mw-watchlist-toollinks' ],
743
			wfMessage( 'parentheses' )->rawParams( $wgLang->pipeList( $tools ) )->escaped()
744
		);
745
	}
746
}
747
748
/**
749
 * Extend HTMLForm purely so we can have a more sane way of getting the section headers
750
 */
751
class EditWatchlistNormalHTMLForm extends HTMLForm {
752
	public function getLegend( $namespace ) {
753
		$namespace = substr( $namespace, 2 );
754
755
		return $namespace == NS_MAIN
756
			? $this->msg( 'blanknamespace' )->escaped()
757
			: htmlspecialchars( $this->getContext()->getLanguage()->getFormattedNsText( $namespace ) );
758
	}
759
760
	public function getBody() {
761
		return $this->displaySection( $this->mFieldTree, '', 'editwatchlist-' );
762
	}
763
}
764
765
class EditWatchlistCheckboxSeriesField extends HTMLMultiSelectField {
766
	/**
767
	 * HTMLMultiSelectField throws validation errors if we get input data
768
	 * that doesn't match the data set in the form setup. This causes
769
	 * problems if something gets removed from the watchlist while the
770
	 * form is open (bug 32126), but we know that invalid items will
771
	 * be harmless so we can override it here.
772
	 *
773
	 * @param string $value The value the field was submitted with
774
	 * @param array $alldata The data collected from the form
775
	 * @return bool|string Bool true on success, or String error to display.
776
	 */
777
	function validate( $value, $alldata ) {
778
		// Need to call into grandparent to be a good citizen. :)
779
		return HTMLFormField::validate( $value, $alldata );
780
	}
781
}
782