98 parent::__construct(
'EditWatchlist',
'editmywatchlist' );
100 $services = MediaWikiServices::getInstance();
102 $this->titleParser =
$titleParser ?? $services->getTitleParser();
103 $this->genderCache =
$genderCache ?? $services->getGenderCache();
105 $this->nsInfo =
$nsInfo ?? $services->getNamespaceInfo();
106 $this->wikiPageFactory =
$wikiPageFactory ?? $services->getWikiPageFactory();
122 # Anons don't get a watchlist
132 $out->addModuleStyles(
'mediawiki.special' );
138 $out->setPageTitle( $this->
msg(
'watchlistedit-raw-title' ) );
140 if ( $form->show() ) {
141 $out->addHTML( $this->successMessage );
146 $out->setPageTitle( $this->
msg(
'watchlistedit-clear-title' ) );
148 if ( $form->show() ) {
149 $out->addHTML( $this->successMessage );
181 $out->setPageTitle( $this->
msg(
'watchlistedit-normal-title' ) );
183 if ( $form->show() ) {
184 $out->addHTML( $this->successMessage );
186 } elseif ( $this->toc !==
false ) {
187 $out->prependHTML( $this->toc );
214 $list = explode(
"\n", trim( $list ) );
215 if ( !is_array( $list ) ) {
221 foreach ( $list as $text ) {
222 $text = trim( $text );
223 if ( strlen( $text ) > 0 ) {
224 $title = Title::newFromText( $text );
231 $this->genderCache->doTitlesArray( $titles );
235 foreach ( $titles as
$title ) {
236 $list[] =
$title->getPrefixedText();
239 return array_unique( $list );
246 if ( count( $wanted ) > 0 ) {
247 $toWatch = array_diff( $wanted, $current );
248 $toUnwatch = array_diff( $current, $wanted );
251 $this->
getUser()->invalidateCache();
253 if ( count( $toWatch ) > 0 || count( $toUnwatch ) > 0 ) {
254 $this->successMessage = $this->
msg(
'watchlistedit-raw-done' )->parse();
259 if ( count( $toWatch ) > 0 ) {
260 $this->successMessage .=
' ' . $this->
msg(
'watchlistedit-raw-added' )
261 ->numParams( count( $toWatch ) )->parse();
262 $this->
showTitles( $toWatch, $this->successMessage );
265 if ( count( $toUnwatch ) > 0 ) {
266 $this->successMessage .=
' ' . $this->
msg(
'watchlistedit-raw-removed' )
267 ->numParams( count( $toUnwatch ) )->parse();
268 $this->
showTitles( $toUnwatch, $this->successMessage );
272 if ( count( $current ) === 0 ) {
277 $this->
showTitles( $current, $this->successMessage );
301 if ( $this->watchedItemStore->mustClearWatchedItemsUsingJobQueue( $this->getUser() ) ) {
302 $this->clearUserWatchedItemsUsingJobQueue();
304 $this->clearUserWatchedItemsNow( $messageFor );
314 $current = $this->getWatchlist();
315 if ( !$this->watchedItemStore->clearUserWatchedItems( $this->getUser() ) ) {
316 throw new LogicException(
317 __METHOD__ .
' should only be called when able to clear synchronously'
320 $this->successMessage = $this->msg(
'watchlistedit-' . $messageFor .
'-done' )->parse();
321 $this->successMessage .=
' ' . $this->msg(
'watchlistedit-' . $messageFor .
'-removed' )
322 ->numParams( count( $current ) )->parse();
323 $this->getUser()->invalidateCache();
324 $this->showTitles( $current, $this->successMessage );
331 $this->watchedItemStore->clearUserWatchedItemsUsingJobQueue( $this->getUser() );
332 $this->successMessage = $this->msg(
'watchlistedit-clear-jobqueue' )->parse();
345 $talk = $this->msg(
'talkpagelinktext' )->text();
347 $batch = $this->linkBatchFactory->newLinkBatch();
348 if ( count( $titles ) >= 100 ) {
349 $output = $this->msg(
'watchlistedit-too-many' )->parse();
352 foreach ( $titles as
$title ) {
359 $batch->addObj(
$title->getTalkPage() );
368 $linkRenderer = $this->getLinkRenderer();
369 foreach ( $titles as
$title ) {
376 $linkRenderer->makeLink(
$title ) .
' ' .
377 $this->msg(
'parentheses' )->rawParams(
378 $linkRenderer->makeLink(
$title->getTalkPage(), $talk )
384 $output .=
"</ul>\n";
396 $watchedItems = $this->watchedItemStore->getWatchedItemsForUser(
398 [
'forWrite' => $this->getRequest()->wasPosted() ]
401 if ( $watchedItems ) {
404 foreach ( $watchedItems as $watchedItem ) {
405 $namespace = $watchedItem->getTarget()->getNamespace();
406 $dbKey = $watchedItem->getTarget()->getDBkey();
407 $title = Title::makeTitleSafe( $namespace, $dbKey );
409 if ( $this->checkTitle(
$title, $namespace, $dbKey )
416 $this->genderCache->doTitlesArray( $titles );
418 foreach ( $titles as
$title ) {
419 $list[] =
$title->getPrefixedText();
423 $this->cleanupWatchlist();
438 if ( $this->getConfig()->
get(
'WatchlistExpiry' ) ) {
439 $options[
'sortByExpiry'] =
true;
442 $watchedItems = $this->watchedItemStore->getWatchedItemsForUser(
443 $this->getUser(), $options
446 $lb = $this->linkBatchFactory->newLinkBatch();
449 foreach ( $watchedItems as $watchedItem ) {
450 $namespace = $watchedItem->getTarget()->getNamespace();
451 $dbKey = $watchedItem->getTarget()->getDBkey();
452 $lb->add( $namespace, $dbKey );
453 if ( !$this->nsInfo->isTalk( $namespace ) ) {
454 $titles[$namespace][$dbKey] = $watchedItem->getExpiryInDaysText( $context );
474 ||
$title->getNamespace() < 0
481 ||
$title->getNamespace() != $namespace
482 ||
$title->getDBkey() != $dbKey
484 $this->badItems[] = [
$title, $namespace, $dbKey ];
494 if ( $this->badItems === [] ) {
498 $user = $this->getUser();
499 $badItems = $this->badItems;
500 DeferredUpdates::addCallableUpdate(
function () use ( $user, $badItems ) {
501 foreach ( $badItems as [
$title, $namespace, $dbKey ] ) {
502 $action =
$title ?
'cleaning up' :
'deleting';
503 wfDebug(
"User {$user->getName()} has broken watchlist item " .
504 "ns($namespace):$dbKey, $action." );
509 $this->watchedItemStore->removeWatch( $user, Title::makeTitle( (
int)$namespace, $dbKey ) );
512 $this->watchlistManager->addWatch( $user,
$title );
527 return $this->watchedItemStore->addWatchBatchForUser(
528 $this->getUser(), $this->getExpandedTargets( $targets )
529 ) && $this->runWatchUnwatchCompleteHook(
'Watch', $targets );
545 return $this->watchedItemStore->removeWatchBatchForUser(
546 $this->getUser(), $this->getExpandedTargets( $targets )
547 ) && $this->runWatchUnwatchCompleteHook(
'Unwatch', $targets );
559 foreach ( $targets as $target ) {
561 Title::newFromLinkTarget( $target ) :
562 Title::newFromText( $target );
563 $page = $this->wikiPageFactory->newFromTitle(
$title );
564 $user = $this->getUser();
565 if ( $action ===
'Watch' ) {
566 $this->getHookRunner()->onWatchArticleComplete( $user, $page );
568 $this->getHookRunner()->onUnwatchArticleComplete( $user, $page );
579 $expandedTargets = [];
580 foreach ( $targets as $target ) {
583 $target = $this->titleParser->parseTitle( $target,
NS_MAIN );
590 $ns = $target->getNamespace();
591 $dbKey = $target->getDBkey();
593 new TitleValue( $this->nsInfo->getSubject( $ns ), $dbKey );
595 new TitleValue( $this->nsInfo->getTalk( $ns ), $dbKey );
597 return $expandedTargets;
603 foreach ( $data as $titles ) {
604 $this->unwatchTitles( $titles );
605 $removed = array_merge( $removed, $titles );
608 if ( count( $removed ) > 0 ) {
609 $this->successMessage = $this->msg(
'watchlistedit-normal-done'
610 )->numParams( count( $removed ) )->parse();
611 $this->showTitles( $removed, $this->successMessage );
630 $watchlistInfo = $this->getWatchlistInfo();
631 $this->getHookRunner()->onWatchlistEditorBeforeFormRender( $watchlistInfo );
633 foreach ( $watchlistInfo as $namespace => $pages ) {
635 foreach ( $pages as $dbkey => $expiryDaysText ) {
636 $title = Title::makeTitleSafe( $namespace, $dbkey );
638 if ( $this->checkTitle(
$title, $namespace, $dbkey ) ) {
639 $text = $this->buildRemoveLine(
$title, $expiryDaysText );
640 $options[$text] =
$title->getPrefixedText();
646 if ( count( $options ) > 0 ) {
647 $fields[
'TitlesNs' . $namespace] = [
648 'class' => EditWatchlistCheckboxSeriesField::class,
649 'options' => $options,
650 'section' =>
"ns$namespace",
654 $this->cleanupWatchlist();
656 if ( count( $fields ) > 1 && $count > 30 ) {
659 $contLang = $this->getContentLanguage();
661 foreach ( $fields as $data ) {
662 # strip out the 'ns' prefix from the section name:
663 $ns = substr( $data[
'section'], 2 );
666 ? $this->msg(
'blanknamespace' )->escaped()
667 : htmlspecialchars( $contLang->getFormattedNsText( $ns ) );
668 $this->toc .=
Linker::tocLine(
"editwatchlist-{$data['section']}", $nsText,
678 $context->setTitle( $this->getPageTitle() );
680 $form->setSubmitTextMsg(
'watchlistedit-normal-submit' );
681 $form->setSubmitDestructive();
683 # 'accesskey-watchlistedit-normal-submit', 'tooltip-watchlistedit-normal-submit'
684 $form->setSubmitTooltip(
'watchlistedit-normal-submit' );
685 $form->setWrapperLegendMsg(
'watchlistedit-normal-legend' );
686 $form->addHeaderText( $this->msg(
'watchlistedit-normal-explain' )->parse() );
687 $form->setSubmitCallback( [ $this,
'submitNormal' ] );
701 $linkRenderer = $this->getLinkRenderer();
702 $link = $linkRenderer->makeLink(
$title );
705 $tools[
'talk'] = $linkRenderer->makeLink(
707 $this->msg(
'talkpagelinktext' )->text()
711 $tools[
'history'] = $linkRenderer->makeKnownLink(
713 $this->msg(
'history_small' )->text(),
715 [
'action' =>
'history' ]
720 $tools[
'contributions'] = $linkRenderer->makeKnownLink(
722 $this->msg(
'contribslink' )->text()
726 $this->getHookRunner()->onWatchlistEditorBuildRemoveLine(
727 $tools,
$title,
$title->isRedirect(), $this->getSkin(), $link );
729 if (
$title->isRedirect() ) {
731 $link =
'<span class="watchlistredir">' . $link .
'</span>';
734 $watchlistExpiringMessage =
'';
735 if ( $this->getConfig()->
get(
'WatchlistExpiry' ) && $expiryDaysText ) {
738 [
'class' =>
'mw-watchlistexpiry-msg' ],
744 $this->msg(
'parentheses' )->rawParams( $this->getLanguage()->pipeList( $tools ) )->escaped() .
745 $watchlistExpiringMessage;
754 $titles = implode(
"\n", $this->getWatchlist() );
757 'type' =>
'textarea',
758 'label-message' =>
'watchlistedit-raw-titles',
759 'default' => $titles,
763 $context->setTitle( $this->getPageTitle(
'raw' ) );
765 $form->setSubmitTextMsg(
'watchlistedit-raw-submit' );
766 # Used message keys: 'accesskey-watchlistedit-raw-submit', 'tooltip-watchlistedit-raw-submit'
767 $form->setSubmitTooltip(
'watchlistedit-raw-submit' );
768 $form->setWrapperLegendMsg(
'watchlistedit-raw-legend' );
769 $form->addHeaderText( $this->msg(
'watchlistedit-raw-explain' )->parse() );
770 $form->setSubmitCallback( [ $this,
'submitRaw' ] );
782 $context->setTitle( $this->getPageTitle(
'clear' ) );
784 $form->setSubmitTextMsg(
'watchlistedit-clear-submit' );
785 # Used message keys: 'accesskey-watchlistedit-clear-submit', 'tooltip-watchlistedit-clear-submit'
786 $form->setSubmitTooltip(
'watchlistedit-clear-submit' );
787 $form->setWrapperLegendMsg(
'watchlistedit-clear-legend' );
788 $form->addHeaderText( $this->msg(
'watchlistedit-clear-explain' )->parse() );
789 $form->setSubmitCallback( [ $this,
'submitClear' ] );
790 $form->setSubmitDestructive();
803 public static function getMode( $request, $par ) {
804 $mode = strtolower( $request->getRawVal(
'action', $par ??
'' ) );
808 case self::EDIT_CLEAR:
809 return self::EDIT_CLEAR;
812 return self::EDIT_RAW;
814 case self::EDIT_NORMAL:
815 return self::EDIT_NORMAL;
835 if ( !$linkRenderer ) {
836 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
841 'view' => [
'Watchlist', false ],
842 'edit' => [
'EditWatchlist', false ],
843 'raw' => [
'EditWatchlist',
'raw' ],
844 'clear' => [
'EditWatchlist',
'clear' ],
847 foreach ( $modes as $mode => $arr ) {
849 $tools[] = $linkRenderer->makeKnownLink(
851 wfMessage(
"watchlisttools-{$mode}" )->text()
855 return Html::rawElement(
857 [
'class' =>
'mw-watchlist-toollinks' ],
858 wfMessage(
'parentheses' )->rawParams(
$lang->pipeList( $tools ) )->escaped()
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
if(ini_get('mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
An IContextSource implementation which will inherit context from another source but allow individual ...
Caches user genders when needed to use correct namespace aliases.
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
static tocLine( $anchor, $tocline, $tocnumber, $level, $sectionIndex=false)
parameter level defines if we are on an indentation level
static tocIndent()
Add another level to the Table of Contents.
static tocList( $toc, Language $lang=null)
Wraps the TOC in a div with ARIA navigation role and provides the hide/collapse JavaScript.
static tocLineEnd()
End a Table Of Contents line.
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Provides the UI through which users can perform editing operations on their watchlist.
getRawForm()
Get a form for editing the watchlist in "raw" mode.
WatchedItemStoreInterface $watchedItemStore
extractTitles( $list)
Extract a list of titles from a blob of text, returning (prefixed) strings; unwatchable titles are ig...
clearUserWatchedItemsNow(string $messageFor)
You should call clearUserWatchedItems() instead to decide if this should use the JobQueue.
getNormalForm()
Get the standard watchlist editing form.
unwatchTitles(array $targets)
Remove a list of titles from a user's watchlist.
doesWrites()
Indicates whether this special page may perform database writes.
cleanupWatchlist()
Attempts to clean up broken items.
executeViewEditWatchlist()
Executes an edit mode for the watchlist view, from which you can manage your watchlist.
WikiPageFactory $wikiPageFactory
runWatchUnwatchCompleteHook( $action, $targets)
clearUserWatchedItemsUsingJobQueue()
You should call clearUserWatchedItems() instead to decide if this should use the JobQueue.
WatchlistManager $watchlistManager
LinkBatchFactory $linkBatchFactory
getSubpagesForPrefixSearch()
Return an array of subpages that this special page will accept.
static buildTools( $lang, LinkRenderer $linkRenderer=null)
Build a set of links for convenient navigation between watchlist viewing and editing modes.
getWatchlistInfo()
Get a list of titles on a user's watchlist, excluding talk pages, and return as a two-dimensional arr...
checkTitle( $title, $namespace, $dbKey)
Validates watchlist entry.
getClearForm()
Get a form for clearing the watchlist.
getExpandedTargets(array $targets)
const EDIT_CLEAR
Editing modes.
execute( $mode)
Main execution point.
__construct(WatchedItemStoreInterface $watchedItemStore=null, TitleParser $titleParser=null, GenderCache $genderCache=null, LinkBatchFactory $linkBatchFactory=null, NamespaceInfo $nsInfo=null, WikiPageFactory $wikiPageFactory=null, WatchlistManager $watchlistManager=null)
outputSubtitle()
Renders a subheader on the watchlist page.
showTitles( $titles, &$output)
Print out a list of linked titles.
submitClear( $data)
Handler for the clear form submission.
watchTitles(array $targets)
Add a list of targets to a user's watchlist.
buildRemoveLine( $title, string $expiryDaysText='')
Build the label for a checkbox, with a link to the title, and various additional bits.
static getMode( $request, $par)
Determine whether we are editing the watchlist, and if so, what kind of editing operation.
clearUserWatchedItems(string $messageFor)
Makes a decision about using the JobQueue or not for clearing a users watchlist.
getWatchlist()
Prepare a list of titles on a user's watchlist (excluding talk pages) and return an array of (prefixe...
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
getName()
Get the name of this Special Page.
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
getOutput()
Get the OutputPage being used for this instance.
requireLogin( $reasonMsg='exception-nologin-text', $titleMsg='exception-nologin')
If the user is not logged in, throws UserNotLoggedIn error.
getUser()
Shortcut to get the User executing this instance.
checkPermissions()
Checks if userCanExecute, and if not throws a PermissionsError.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getRequest()
Get the WebRequest being used for this instance.
checkReadOnly()
If the wiki is currently in readonly mode, throws a ReadOnlyError.
getLanguage()
Shortcut to get user's language.
Represents a page (or page fragment) title within MediaWiki.
Represents a title within MediaWiki.
isWatchable()
Can this title be added to a user's watchlist?
Shortcut to construct a special page which is unlisted by default.
A title parser service for MediaWiki.
if(!isset( $args[0])) $lang