3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
21 namespace MediaWiki\Specials
;
23 use EditWatchlistCheckboxSeriesField
;
24 use EditWatchlistNormalHTMLForm
;
26 use MediaWiki\Cache\GenderCache
;
27 use MediaWiki\Cache\LinkBatchFactory
;
28 use MediaWiki\Deferred\DeferredUpdates
;
29 use MediaWiki\Html\Html
;
30 use MediaWiki\HTMLForm\HTMLForm
;
31 use MediaWiki\HTMLForm\OOUIHTMLForm
;
32 use MediaWiki\Linker\LinkRenderer
;
33 use MediaWiki\Linker\LinkTarget
;
34 use MediaWiki\MainConfigNames
;
35 use MediaWiki\MediaWikiServices
;
36 use MediaWiki\Page\WikiPageFactory
;
37 use MediaWiki\Parser\Parser
;
38 use MediaWiki\Parser\ParserOutput
;
39 use MediaWiki\Parser\ParserOutputFlags
;
40 use MediaWiki\Request\WebRequest
;
41 use MediaWiki\SpecialPage\SpecialPage
;
42 use MediaWiki\SpecialPage\UnlistedSpecialPage
;
43 use MediaWiki\Status\Status
;
44 use MediaWiki\Title\MalformedTitleException
;
45 use MediaWiki\Title\NamespaceInfo
;
46 use MediaWiki\Title\Title
;
47 use MediaWiki\Title\TitleParser
;
48 use MediaWiki\Title\TitleValue
;
49 use MediaWiki\Watchlist\WatchedItemStore
;
50 use MediaWiki\Watchlist\WatchedItemStoreInterface
;
51 use MediaWiki\Watchlist\WatchlistManager
;
53 use Wikimedia\Parsoid\Core\SectionMetadata
;
54 use Wikimedia\Parsoid\Core\TOCData
;
57 * Users can edit their watchlist via this page.
59 * @ingroup SpecialPage
61 * @author Rob Church <robchur@gmail.com>
63 class SpecialEditWatchlist
extends UnlistedSpecialPage
{
65 * Editing modes. EDIT_CLEAR is no longer used; the "Clear" link scared people
66 * too much. Now it's passed on to the raw editor, from which it's very easy to clear.
68 public const EDIT_CLEAR
= 1;
69 public const EDIT_RAW
= 2;
70 public const EDIT_NORMAL
= 3;
71 public const VIEW
= 4;
73 /** @var string|null */
74 protected $successMessage;
80 private $badItems = [];
82 private TitleParser
$titleParser;
83 private WatchedItemStoreInterface
$watchedItemStore;
84 private GenderCache
$genderCache;
85 private LinkBatchFactory
$linkBatchFactory;
86 private NamespaceInfo
$nsInfo;
87 private WikiPageFactory
$wikiPageFactory;
88 private WatchlistManager
$watchlistManager;
90 /** @var int|false where the value is one of the EDIT_ prefixed constants (e.g. EDIT_NORMAL) */
94 * @param WatchedItemStoreInterface|null $watchedItemStore
95 * @param TitleParser|null $titleParser
96 * @param GenderCache|null $genderCache
97 * @param LinkBatchFactory|null $linkBatchFactory
98 * @param NamespaceInfo|null $nsInfo
99 * @param WikiPageFactory|null $wikiPageFactory
100 * @param WatchlistManager|null $watchlistManager
102 public function __construct(
103 ?WatchedItemStoreInterface
$watchedItemStore = null,
104 ?TitleParser
$titleParser = null,
105 ?GenderCache
$genderCache = null,
106 ?LinkBatchFactory
$linkBatchFactory = null,
107 ?NamespaceInfo
$nsInfo = null,
108 ?WikiPageFactory
$wikiPageFactory = null,
109 ?WatchlistManager
$watchlistManager = null
111 parent
::__construct( 'EditWatchlist', 'editmywatchlist' );
112 // This class is extended and therefor fallback to global state - T266065
113 $services = MediaWikiServices
::getInstance();
114 $this->watchedItemStore
= $watchedItemStore ??
$services->getWatchedItemStore();
115 $this->titleParser
= $titleParser ??
$services->getTitleParser();
116 $this->genderCache
= $genderCache ??
$services->getGenderCache();
117 $this->linkBatchFactory
= $linkBatchFactory ??
$services->getLinkBatchFactory();
118 $this->nsInfo
= $nsInfo ??
$services->getNamespaceInfo();
119 $this->wikiPageFactory
= $wikiPageFactory ??
$services->getWikiPageFactory();
120 $this->watchlistManager
= $watchlistManager ??
$services->getWatchlistManager();
123 public function doesWrites() {
128 * Main execution point
130 * @param string|null $mode
132 public function execute( $mode ) {
135 $user = $this->getUser();
136 if ( !$user->isRegistered()
137 ||
( $user->isTemp() && !$user->isAllowed( 'editmywatchlist' ) )
139 throw new UserNotLoggedIn( 'watchlistanontext' );
142 $out = $this->getOutput();
144 $this->checkPermissions();
145 $this->checkReadOnly();
147 $this->outputHeader();
148 $out->addModuleStyles( [
149 'mediawiki.interface.helpers.styles',
152 $out->addModules( [ 'mediawiki.special.watchlist' ] );
154 $mode = self
::getMode( $this->getRequest(), $mode, self
::EDIT_NORMAL
);
155 $this->currentMode
= $mode;
156 $this->outputSubtitle();
160 $title = SpecialPage
::getTitleFor( 'Watchlist' );
161 $out->redirect( $title->getLocalURL() );
164 $out->setPageTitleMsg( $this->msg( 'watchlistedit-raw-title' ) );
165 $form = $this->getRawForm();
166 if ( $form->show() ) {
167 $out->addHTML( $this->successMessage
);
168 $out->addReturnTo( SpecialPage
::getTitleFor( 'Watchlist' ) );
171 case self
::EDIT_CLEAR
:
172 $out->setPageTitleMsg( $this->msg( 'watchlistedit-clear-title' ) );
173 $form = $this->getClearForm();
174 if ( $form->show() ) {
175 $out->addHTML( $this->successMessage
);
176 $out->addReturnTo( SpecialPage
::getTitleFor( 'Watchlist' ) );
180 case self
::EDIT_NORMAL
:
182 $this->executeViewEditWatchlist();
188 * Renders a subheader on the watchlist page.
190 protected function outputSubtitle() {
191 $out = $this->getOutput();
192 $skin = $this->getSkin();
193 // For legacy skins render the tabs in the subtitle
194 $subpageSubtitle = $skin->supportsMenu( 'associated-pages' ) ?
'' :
198 $this->getLinkRenderer(),
205 'class' => 'mw-watchlist-owner'
207 // Previously the watchlistfor2 message took 2 parameters.
208 // It now only takes 1 so empty string is passed.
209 // Empty string parameter can be removed when all messages
210 // are updated to not use $2
211 $this->msg( 'watchlistfor2', $this->getUser()->getName(), '' )->text()
219 public function getAssociatedNavigationLinks() {
220 return SpecialWatchlist
::WATCHLIST_TAB_PATHS
;
226 public function getShortDescription( string $path = '' ): string {
227 return SpecialWatchlist
::getShortDescriptionHelper( $this, $path );
231 * Executes an edit mode for the watchlist view, from which you can manage your watchlist
233 protected function executeViewEditWatchlist() {
234 $out = $this->getOutput();
235 $out->setPageTitleMsg( $this->msg( 'watchlistedit-normal-title' ) );
237 $form = $this->getNormalForm();
238 $form->prepareForm();
240 $result = $form->tryAuthorizedSubmit();
241 if ( $result === true ||
( $result instanceof Status
&& $result->isGood() ) ) {
242 $out->addHTML( $this->successMessage
);
243 $out->addReturnTo( SpecialPage
::getTitleFor( 'Watchlist' ) );
247 $pout = new ParserOutput
;
248 $pout->setTOCData( $this->tocData
);
249 $pout->setOutputFlag( ParserOutputFlags
::SHOW_TOC
);
250 $pout->setRawText( Parser
::TOC_PLACEHOLDER
);
251 $out->addParserOutput( $pout );
253 $form->displayForm( $result );
257 * Return an array of subpages that this special page will accept.
259 * @see also SpecialWatchlist::getSubpagesForPrefixSearch
260 * @return string[] subpages
262 public function getSubpagesForPrefixSearch() {
263 // SpecialWatchlist uses SpecialEditWatchlist::getMode, so new types should be added
264 // here and there - no 'edit' here, because that the default for this page
272 * Extract a list of titles from a blob of text, returning
273 * (prefixed) strings; unwatchable titles are ignored
275 * @param string $list
278 private function extractTitles( $list ) {
279 $list = explode( "\n", trim( $list ) );
283 foreach ( $list as $text ) {
284 $text = trim( $text );
285 if ( strlen( $text ) > 0 ) {
286 $title = Title
::newFromText( $text );
287 if ( $title instanceof Title
&& $this->watchlistManager
->isWatchable( $title ) ) {
293 $this->genderCache
->doTitlesArray( $titles );
296 /** @var Title $title */
297 foreach ( $titles as $title ) {
298 $list[] = $title->getPrefixedText();
301 return array_unique( $list );
304 public function submitRaw( $data ) {
305 $wanted = $this->extractTitles( $data['Titles'] );
306 $current = $this->getWatchlist();
308 if ( count( $wanted ) > 0 ) {
309 $toWatch = array_diff( $wanted, $current );
310 $toUnwatch = array_diff( $current, $wanted );
311 if ( !$toWatch && !$toUnwatch ) {
315 $this->watchTitles( $toWatch );
316 $this->unwatchTitles( $toUnwatch );
317 $this->getUser()->invalidateCache();
318 $this->successMessage
= $this->msg( 'watchlistedit-raw-done' )->parse();
321 $this->successMessage
.= ' ' . $this->msg( 'watchlistedit-raw-added' )
322 ->numParams( count( $toWatch ) )->parse();
323 $this->showTitles( $toWatch, $this->successMessage
);
327 $this->successMessage
.= ' ' . $this->msg( 'watchlistedit-raw-removed' )
328 ->numParams( count( $toUnwatch ) )->parse();
329 $this->showTitles( $toUnwatch, $this->successMessage
);
336 $this->clearUserWatchedItems( 'raw' );
337 $this->showTitles( $current, $this->successMessage
);
344 * Handler for the clear form submission
349 public function submitClear( $data ): bool {
350 $this->clearUserWatchedItems( 'clear' );
355 * Makes a decision about using the JobQueue or not for clearing a users watchlist.
356 * Also displays the appropriate messages to the user based on that decision.
358 * @param string $messageFor 'raw' or 'clear'. Only used when JobQueue is not used.
360 private function clearUserWatchedItems( string $messageFor ): void
{
361 if ( $this->watchedItemStore
->mustClearWatchedItemsUsingJobQueue( $this->getUser() ) ) {
362 $this->clearUserWatchedItemsUsingJobQueue();
364 $this->clearUserWatchedItemsNow( $messageFor );
369 * You should call clearUserWatchedItems() instead to decide if this should use the JobQueue
371 * @param string $messageFor 'raw' or 'clear'
373 private function clearUserWatchedItemsNow( string $messageFor ): void
{
374 $current = $this->getWatchlist();
375 if ( !$this->watchedItemStore
->clearUserWatchedItems( $this->getUser() ) ) {
376 throw new LogicException(
377 __METHOD__
. ' should only be called when able to clear synchronously'
380 // Messages used: watchlistedit-clear-done, watchlistedit-raw-done
381 $this->successMessage
= $this->msg( 'watchlistedit-' . $messageFor . '-done' )->parse();
382 // Messages used: watchlistedit-clear-removed, watchlistedit-raw-removed
383 $this->successMessage
.= ' ' . $this->msg( 'watchlistedit-' . $messageFor . '-removed' )
384 ->numParams( count( $current ) )->parse();
385 $this->getUser()->invalidateCache();
386 $this->showTitles( $current, $this->successMessage
);
390 * You should call clearUserWatchedItems() instead to decide if this should use the JobQueue
392 private function clearUserWatchedItemsUsingJobQueue(): void
{
393 $this->watchedItemStore
->clearUserWatchedItemsUsingJobQueue( $this->getUser() );
394 $this->successMessage
= $this->msg( 'watchlistedit-clear-jobqueue' )->parse();
398 * Print out a list of linked titles
400 * $titles can be an array of strings or Title objects; the former
401 * is preferred, since Titles are very memory-heavy
403 * @param array $titles Array of strings, or Title objects
404 * @param string &$output
406 private function showTitles( $titles, &$output ) {
407 $talk = $this->msg( 'talkpagelinktext' )->text();
408 // Do a batch existence check
409 $batch = $this->linkBatchFactory
->newLinkBatch();
410 if ( count( $titles ) >= 100 ) {
411 $output = $this->msg( 'watchlistedit-too-many' )->parse();
414 foreach ( $titles as $title ) {
415 if ( !$title instanceof Title
) {
416 $title = Title
::newFromText( $title );
419 if ( $title instanceof Title
) {
420 $batch->addObj( $title );
421 $batch->addObj( $title->getTalkPage() );
427 // Print out the list
430 $linkRenderer = $this->getLinkRenderer();
431 foreach ( $titles as $title ) {
432 if ( !$title instanceof Title
) {
433 $title = Title
::newFromText( $title );
436 if ( $title instanceof Title
) {
438 $linkRenderer->makeLink( $title ) . ' ' .
439 $this->msg( 'parentheses' )->rawParams(
440 $linkRenderer->makeLink( $title->getTalkPage(), $talk )
446 $output .= "</ul>\n";
450 * Prepare a list of titles on a user's watchlist (excluding talk pages)
451 * and return an array of (prefixed) strings
455 private function getWatchlist() {
458 $watchedItems = $this->watchedItemStore
->getWatchedItemsForUser(
460 [ 'forWrite' => $this->getRequest()->wasPosted() ]
463 if ( $watchedItems ) {
464 /** @var Title[] $titles */
466 foreach ( $watchedItems as $watchedItem ) {
467 $namespace = $watchedItem->getTarget()->getNamespace();
468 $dbKey = $watchedItem->getTarget()->getDBkey();
469 $title = Title
::makeTitleSafe( $namespace, $dbKey );
471 if ( $this->checkTitle( $title, $namespace, $dbKey )
472 && !$title->isTalkPage()
478 $this->genderCache
->doTitlesArray( $titles );
480 foreach ( $titles as $title ) {
481 $list[] = $title->getPrefixedText();
485 $this->cleanupWatchlist();
491 * Get a list of titles on a user's watchlist, excluding talk pages,
492 * and return as a two-dimensional array with namespace and title.
496 protected function getWatchlistInfo() {
498 $options = [ 'sort' => WatchedItemStore
::SORT_ASC
];
500 if ( $this->getConfig()->get( MainConfigNames
::WatchlistExpiry
) ) {
501 $options[ 'sortByExpiry'] = true;
504 $watchedItems = $this->watchedItemStore
->getWatchedItemsForUser(
505 $this->getUser(), $options
508 $lb = $this->linkBatchFactory
->newLinkBatch();
509 $context = $this->getContext();
511 foreach ( $watchedItems as $watchedItem ) {
512 $namespace = $watchedItem->getTarget()->getNamespace();
513 $dbKey = $watchedItem->getTarget()->getDBkey();
514 $lb->add( $namespace, $dbKey );
515 if ( !$this->nsInfo
->isTalk( $namespace ) ) {
516 $titles[$namespace][$dbKey] = $watchedItem->getExpiryInDaysText( $context );
526 * Validates watchlist entry
528 * @param Title $title
529 * @param int $namespace
530 * @param string $dbKey
531 * @return bool Whether this item is valid
533 private function checkTitle( $title, $namespace, $dbKey ) {
535 && ( $title->isExternal()
536 ||
$title->getNamespace() < 0
539 $title = false; // unrecoverable
543 ||
$title->getNamespace() != $namespace
544 ||
$title->getDBkey() != $dbKey
546 $this->badItems
[] = [ $title, $namespace, $dbKey ];
553 * Attempts to clean up broken items
555 private function cleanupWatchlist() {
556 if ( $this->badItems
=== [] ) {
557 return; // nothing to do
560 $user = $this->getUser();
561 $badItems = $this->badItems
;
562 DeferredUpdates
::addCallableUpdate( function () use ( $user, $badItems ) {
563 foreach ( $badItems as [ $title, $namespace, $dbKey ] ) {
564 $action = $title ?
'cleaning up' : 'deleting';
565 wfDebug( "User {$user->getName()} has broken watchlist item " .
566 "ns($namespace):$dbKey, $action." );
568 // NOTE: We *know* that the title is invalid. TitleValue may refuse instantiation.
569 // XXX: We may need an InvalidTitleValue class that allows instantiation of
570 // known bad title values.
571 $this->watchedItemStore
->removeWatch( $user, Title
::makeTitle( (int)$namespace, $dbKey ) );
572 // Can't just do an UPDATE instead of DELETE/INSERT due to unique index
574 $this->watchlistManager
->addWatch( $user, $title );
581 * Add a list of targets to a user's watchlist
583 * @param string[]|LinkTarget[] $targets
585 private function watchTitles( array $targets ): void
{
587 $this->watchedItemStore
->addWatchBatchForUser(
588 $this->getUser(), $this->getExpandedTargets( $targets )
591 $this->runWatchUnwatchCompleteHook( 'Watch', $targets );
596 * Remove a list of titles from a user's watchlist
598 * $titles can be an array of strings or Title objects; the former
599 * is preferred, since Titles are very memory-heavy
601 * @param string[]|LinkTarget[] $targets
603 private function unwatchTitles( array $targets ): void
{
605 $this->watchedItemStore
->removeWatchBatchForUser(
606 $this->getUser(), $this->getExpandedTargets( $targets )
609 $this->runWatchUnwatchCompleteHook( 'Unwatch', $targets );
614 * @param string $action
615 * Can be "Watch" or "Unwatch"
616 * @param string[]|LinkTarget[] $targets
618 private function runWatchUnwatchCompleteHook( string $action, array $targets ): void
{
619 foreach ( $targets as $target ) {
620 $title = $target instanceof LinkTarget ?
621 Title
::newFromLinkTarget( $target ) :
622 Title
::newFromText( $target );
623 $page = $this->wikiPageFactory
->newFromTitle( $title );
624 $user = $this->getUser();
625 if ( $action === 'Watch' ) {
626 $this->getHookRunner()->onWatchArticleComplete( $user, $page );
628 $this->getHookRunner()->onUnwatchArticleComplete( $user, $page );
634 * @param string[]|LinkTarget[] $targets
635 * @return TitleValue[]
637 private function getExpandedTargets( array $targets ) {
638 $expandedTargets = [];
639 foreach ( $targets as $target ) {
640 if ( !$target instanceof LinkTarget
) {
642 $target = $this->titleParser
->parseTitle( $target, NS_MAIN
);
643 } catch ( MalformedTitleException
$e ) {
648 $ns = $target->getNamespace();
649 $dbKey = $target->getDBkey();
651 new TitleValue( $this->nsInfo
->getSubject( $ns ), $dbKey );
653 new TitleValue( $this->nsInfo
->getTalk( $ns ), $dbKey );
655 return $expandedTargets;
658 public function submitNormal( $data ) {
661 foreach ( $data as $titles ) {
662 // ignore the 'check all' checkbox, which is a boolean value
663 if ( is_array( $titles ) ) {
664 $this->unwatchTitles( $titles );
665 $removed = array_merge( $removed, $titles );
669 if ( count( $removed ) > 0 ) {
670 $this->successMessage
= $this->msg( 'watchlistedit-normal-done'
671 )->numParams( count( $removed ) )->parse();
672 $this->showTitles( $removed, $this->successMessage
);
681 * Get the standard watchlist editing form
685 protected function getNormalForm() {
689 // Allow subscribers to manipulate the list of watched pages (or use it
690 // to preload lots of details at once)
691 $watchlistInfo = $this->getWatchlistInfo();
692 $this->getHookRunner()->onWatchlistEditorBeforeFormRender( $watchlistInfo );
694 foreach ( $watchlistInfo as $namespace => $pages ) {
696 foreach ( $pages as $dbkey => $expiryDaysText ) {
697 $title = Title
::makeTitleSafe( $namespace, $dbkey );
699 if ( $this->checkTitle( $title, $namespace, $dbkey ) ) {
700 $text = $this->buildRemoveLine( $title, $expiryDaysText );
701 $options[$text] = $title->getPrefixedText();
706 // checkTitle can filter some options out, avoid empty sections
707 if ( count( $options ) > 0 ) {
708 // add a checkbox to select all entries in namespace
709 $fields['CheckAllNs' . $namespace] = [
710 'cssclass' => 'mw-watchlistedit-checkall',
712 'section' => "ns$namespace",
713 'label' => $this->msg( 'watchlistedit-normal-check-all' )->text()
716 $fields['TitlesNs' . $namespace] = [
717 'cssclass' => 'mw-watchlistedit-check',
718 'class' => EditWatchlistCheckboxSeriesField
::class,
719 'options' => $options,
720 'section' => "ns$namespace",
724 $this->cleanupWatchlist();
726 $this->tocData
= new TOCData();
727 if ( count( $fields ) > 1 && $count > 30 ) {
729 $contLang = $this->getContentLanguage();
730 foreach ( $fields as $key => $data ) {
731 // ignore the 'check all' field
732 if ( str_starts_with( $key, 'CheckAllNs' ) ) {
735 # strip out the 'ns' prefix from the section name:
736 $ns = (int)substr( $data['section'], 2 );
737 $nsText = ( $ns === NS_MAIN
)
738 ?
$this->msg( 'blanknamespace' )->text()
739 : $contLang->getFormattedNsText( $ns );
740 $anchor = "editwatchlist-{$data['section']}";
742 $this->tocData
->addSection( new SectionMetadata(
744 // This is supposed to be the heading level, e.g. 2 for a <h2> tag,
745 // but this page uses <legend> tags for the headings, so use a fake value
747 htmlspecialchars( $nsText ),
748 $this->getLanguage()->formatNum( $tocLength ),
758 $form = new EditWatchlistNormalHTMLForm( $fields, $this->getContext() );
759 $form->setTitle( $this->getPageTitle() ); // Remove subpage
760 $form->setSubmitTextMsg( 'watchlistedit-normal-submit' );
761 $form->setSubmitDestructive();
763 # 'accesskey-watchlistedit-normal-submit', 'tooltip-watchlistedit-normal-submit'
764 $form->setSubmitTooltip( 'watchlistedit-normal-submit' );
765 $form->setWrapperLegendMsg( 'watchlistedit-normal-legend' );
766 $form->addHeaderHtml( $this->msg( 'watchlistedit-normal-explain' )->parse() );
767 $form->setSubmitCallback( [ $this, 'submitNormal' ] );
773 * Build the label for a checkbox, with a link to the title, and various additional bits
775 * @param Title $title
776 * @param string $expiryDaysText message shows the number of days a title has remaining in a user's watchlist.
777 * If this param is not empty then include a message that states the time remaining in a watchlist.
780 private function buildRemoveLine( $title, string $expiryDaysText = '' ): string {
781 $linkRenderer = $this->getLinkRenderer();
782 $link = $linkRenderer->makeLink( $title );
785 $tools['talk'] = $linkRenderer->makeLink(
786 $title->getTalkPage(),
787 $this->msg( 'talkpagelinktext' )->text()
790 if ( $title->exists() ) {
791 $tools['history'] = $linkRenderer->makeKnownLink(
793 $this->msg( 'history_small' )->text(),
795 [ 'action' => 'history' ]
799 if ( $title->getNamespace() === NS_USER
&& !$title->isSubpage() ) {
800 $tools['contributions'] = $linkRenderer->makeKnownLink(
801 SpecialPage
::getTitleFor( 'Contributions', $title->getText() ),
802 $this->msg( 'contribslink' )->text()
806 $this->getHookRunner()->onWatchlistEditorBuildRemoveLine(
807 $tools, $title, $title->isRedirect(), $this->getSkin(), $link );
809 if ( $title->isRedirect() ) {
810 // Linker already makes class mw-redirect, so this is redundant
811 $link = '<span class="watchlistredir">' . $link . '</span>';
814 $watchlistExpiringMessage = '';
815 if ( $this->getConfig()->get( MainConfigNames
::WatchlistExpiry
) && $expiryDaysText ) {
816 $watchlistExpiringMessage = Html
::element(
818 [ 'class' => 'mw-watchlistexpiry-msg' ],
823 return $link . ' ' . Html
::openElement( 'span', [ 'class' => 'mw-changeslist-links' ] ) .
826 array_map( static function ( $tool ) {
827 return Html
::rawElement( 'span', [], $tool );
830 Html
::closeElement( 'span' ) .
831 $watchlistExpiringMessage;
835 * Get a form for editing the watchlist in "raw" mode
839 protected function getRawForm() {
840 $titles = implode( "\n", $this->getWatchlist() );
843 'type' => 'textarea',
844 'label-message' => 'watchlistedit-raw-titles',
845 'default' => $titles,
848 $form = new OOUIHTMLForm( $fields, $this->getContext() );
849 $form->setTitle( $this->getPageTitle( 'raw' ) ); // Reset subpage
850 $form->setSubmitTextMsg( 'watchlistedit-raw-submit' );
851 # Used message keys: 'accesskey-watchlistedit-raw-submit', 'tooltip-watchlistedit-raw-submit'
852 $form->setSubmitTooltip( 'watchlistedit-raw-submit' );
853 $form->setWrapperLegendMsg( 'watchlistedit-raw-legend' );
854 $form->addHeaderHtml( $this->msg( 'watchlistedit-raw-explain' )->parse() );
855 $form->setSubmitCallback( [ $this, 'submitRaw' ] );
861 * Get a form for clearing the watchlist
865 protected function getClearForm() {
866 $form = new OOUIHTMLForm( [], $this->getContext() );
867 $form->setTitle( $this->getPageTitle( 'clear' ) ); // Reset subpage
868 $form->setSubmitTextMsg( 'watchlistedit-clear-submit' );
869 # Used message keys: 'accesskey-watchlistedit-clear-submit', 'tooltip-watchlistedit-clear-submit'
870 $form->setSubmitTooltip( 'watchlistedit-clear-submit' );
871 $form->setWrapperLegendMsg( 'watchlistedit-clear-legend' );
872 $form->addHeaderHtml( $this->msg( 'watchlistedit-clear-explain' )->parse() );
873 $form->setSubmitCallback( [ $this, 'submitClear' ] );
874 $form->setSubmitDestructive();
880 * Determine whether we are editing the watchlist, and if so, what
881 * kind of editing operation
883 * @param WebRequest $request
884 * @param string|null $par
885 * @param int|false $defaultValue to use if not known.
888 public static function getMode( $request, $par, $defaultValue = false ) {
889 $mode = strtolower( $request->getRawVal( 'action' ) ??
$par ??
'' );
895 case self
::EDIT_CLEAR
:
896 return self
::EDIT_CLEAR
;
899 return self
::EDIT_RAW
;
901 case self
::EDIT_NORMAL
:
902 return self
::EDIT_NORMAL
;
904 return $defaultValue;
909 * Build a set of links for convenient navigation
910 * between watchlist viewing and editing modes
912 * @param mixed $unused
913 * @param LinkRenderer|null $linkRenderer
914 * @param int|false $selectedMode result of self::getMode
917 public static function buildTools( $unused, ?LinkRenderer
$linkRenderer = null, $selectedMode = false ) {
918 if ( !$linkRenderer ) {
919 $linkRenderer = MediaWikiServices
::getInstance()->getLinkRenderer();
924 'view' => [ 'Watchlist', false, false ],
925 'edit' => [ 'EditWatchlist', false, self
::EDIT_NORMAL
],
926 'raw' => [ 'EditWatchlist', 'raw', self
::EDIT_RAW
],
927 'clear' => [ 'EditWatchlist', 'clear', self
::EDIT_CLEAR
],
930 foreach ( $modes as $mode => $arr ) {
931 // can use messages 'watchlisttools-view', 'watchlisttools-edit', 'watchlisttools-raw'
932 $link = $linkRenderer->makeKnownLink(
933 SpecialPage
::getTitleFor( $arr[0], $arr[1] ),
934 wfMessage( "watchlisttools-{$mode}" )->text()
936 $isSelected = $selectedMode === $arr[2];
938 'mw-watchlist-toollink',
939 'mw-watchlist-toollink-' . $mode,
940 $isSelected ?
'mw-watchlist-toollink-active' :
941 'mw-watchlist-toollink-inactive'
943 $tools[] = Html
::rawElement( 'span', [
948 return Html
::rawElement(
950 [ 'class' => 'mw-watchlist-toollinks mw-changeslist-links' ],
951 implode( '', $tools )
956 /** @deprecated class alias since 1.41 */
957 class_alias( SpecialEditWatchlist
::class, 'SpecialEditWatchlist' );