Merge "mediawiki.widgets: Remove use of bind() for lexical 'this' binding"
[lhc/web/wiklou.git] / includes / specials / SpecialEditWatchlist.php
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 <robchur@gmail.com>
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() );
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 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 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 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 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 ) {
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 )
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 ) ) {
551 $text = $this->buildRemoveLine( $title );
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(
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 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] ),
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 }