102 parent::__construct(
'EditWatchlist',
'editmywatchlist' );
104 $services = MediaWikiServices::getInstance();
106 $this->titleParser =
$titleParser ?? $services->getTitleParser();
107 $this->genderCache =
$genderCache ?? $services->getGenderCache();
109 $this->nsInfo =
$nsInfo ?? $services->getNamespaceInfo();
110 $this->wikiPageFactory =
$wikiPageFactory ?? $services->getWikiPageFactory();
126 # Anons don't get a watchlist
135 $out->addModuleStyles( [
136 'mediawiki.interface.helpers.styles',
141 $this->currentMode = $mode;
146 $out->setPageTitle( $this->
msg(
'watchlistedit-raw-title' ) );
148 if ( $form->show() ) {
149 $out->addHTML( $this->successMessage );
154 $out->setPageTitle( $this->
msg(
'watchlistedit-clear-title' ) );
156 if ( $form->show() ) {
157 $out->addHTML( $this->successMessage );
178 'class' =>
'mw-watchlist-owner'
199 $out->setPageTitle( $this->
msg(
'watchlistedit-normal-title' ) );
201 if ( $form->show() ) {
202 $out->addHTML( $this->successMessage );
204 } elseif ( $this->toc !==
false ) {
205 $out->prependHTML( $this->toc );
232 $list = explode(
"\n", trim( $list ) );
236 foreach ( $list as $text ) {
237 $text = trim( $text );
238 if ( strlen( $text ) > 0 ) {
240 if (
$title instanceof
Title && $this->watchlistManager->isWatchable(
$title ) ) {
246 $this->genderCache->doTitlesArray( $titles );
250 foreach ( $titles as
$title ) {
251 $list[] =
$title->getPrefixedText();
254 return array_unique( $list );
261 if ( count( $wanted ) > 0 ) {
262 $toWatch = array_diff( $wanted, $current );
263 $toUnwatch = array_diff( $current, $wanted );
266 $this->
getUser()->invalidateCache();
268 if ( count( $toWatch ) > 0 || count( $toUnwatch ) > 0 ) {
269 $this->successMessage = $this->
msg(
'watchlistedit-raw-done' )->parse();
274 if ( count( $toWatch ) > 0 ) {
275 $this->successMessage .=
' ' . $this->
msg(
'watchlistedit-raw-added' )
276 ->numParams( count( $toWatch ) )->parse();
277 $this->
showTitles( $toWatch, $this->successMessage );
280 if ( count( $toUnwatch ) > 0 ) {
281 $this->successMessage .=
' ' . $this->
msg(
'watchlistedit-raw-removed' )
282 ->numParams( count( $toUnwatch ) )->parse();
283 $this->
showTitles( $toUnwatch, $this->successMessage );
287 if ( count( $current ) === 0 ) {
292 $this->
showTitles( $current, $this->successMessage );
316 if ( $this->watchedItemStore->mustClearWatchedItemsUsingJobQueue( $this->
getUser() ) ) {
317 $this->clearUserWatchedItemsUsingJobQueue();
319 $this->clearUserWatchedItemsNow( $messageFor );
329 $current = $this->getWatchlist();
330 if ( !$this->watchedItemStore->clearUserWatchedItems( $this->getUser() ) ) {
331 throw new LogicException(
332 __METHOD__ .
' should only be called when able to clear synchronously'
335 $this->successMessage = $this->msg(
'watchlistedit-' . $messageFor .
'-done' )->parse();
336 $this->successMessage .=
' ' . $this->msg(
'watchlistedit-' . $messageFor .
'-removed' )
337 ->numParams( count( $current ) )->parse();
338 $this->
getUser()->invalidateCache();
339 $this->showTitles( $current, $this->successMessage );
346 $this->watchedItemStore->clearUserWatchedItemsUsingJobQueue( $this->
getUser() );
347 $this->successMessage = $this->msg(
'watchlistedit-clear-jobqueue' )->parse();
360 $talk = $this->msg(
'talkpagelinktext' )->text();
362 $batch = $this->linkBatchFactory->newLinkBatch();
363 if ( count( $titles ) >= 100 ) {
364 $output = $this->msg(
'watchlistedit-too-many' )->parse();
367 foreach ( $titles as
$title ) {
374 $batch->addObj(
$title->getTalkPage() );
383 $linkRenderer = $this->getLinkRenderer();
384 foreach ( $titles as
$title ) {
391 $linkRenderer->makeLink(
$title ) .
' ' .
392 $this->msg(
'parentheses' )->rawParams(
393 $linkRenderer->makeLink(
$title->getTalkPage(), $talk )
399 $output .=
"</ul>\n";
411 $watchedItems = $this->watchedItemStore->getWatchedItemsForUser(
413 [
'forWrite' => $this->getRequest()->wasPosted() ]
416 if ( $watchedItems ) {
419 foreach ( $watchedItems as $watchedItem ) {
420 $namespace = $watchedItem->getTarget()->getNamespace();
421 $dbKey = $watchedItem->getTarget()->getDBkey();
424 if ( $this->checkTitle(
$title, $namespace, $dbKey )
431 $this->genderCache->doTitlesArray( $titles );
433 foreach ( $titles as
$title ) {
434 $list[] =
$title->getPrefixedText();
438 $this->cleanupWatchlist();
453 if ( $this->getConfig()->
get( MainConfigNames::WatchlistExpiry ) ) {
454 $options[
'sortByExpiry'] =
true;
457 $watchedItems = $this->watchedItemStore->getWatchedItemsForUser(
461 $lb = $this->linkBatchFactory->newLinkBatch();
464 foreach ( $watchedItems as $watchedItem ) {
465 $namespace = $watchedItem->getTarget()->getNamespace();
466 $dbKey = $watchedItem->getTarget()->getDBkey();
467 $lb->add( $namespace, $dbKey );
468 if ( !$this->nsInfo->isTalk( $namespace ) ) {
469 $titles[$namespace][$dbKey] = $watchedItem->getExpiryInDaysText( $context );
489 ||
$title->getNamespace() < 0
496 ||
$title->getNamespace() != $namespace
497 ||
$title->getDBkey() != $dbKey
499 $this->badItems[] = [
$title, $namespace, $dbKey ];
509 if ( $this->badItems === [] ) {
514 $badItems = $this->badItems;
516 foreach ( $badItems as [
$title, $namespace, $dbKey ] ) {
517 $action =
$title ?
'cleaning up' :
'deleting';
518 wfDebug(
"User {$user->getName()} has broken watchlist item " .
519 "ns($namespace):$dbKey, $action." );
524 $this->watchedItemStore->removeWatch( $user,
Title::makeTitle( (
int)$namespace, $dbKey ) );
527 $this->watchlistManager->addWatch( $user,
$title );
542 return $this->watchedItemStore->addWatchBatchForUser(
543 $this->
getUser(), $this->getExpandedTargets( $targets )
544 ) && $this->runWatchUnwatchCompleteHook(
'Watch', $targets );
560 return $this->watchedItemStore->removeWatchBatchForUser(
561 $this->
getUser(), $this->getExpandedTargets( $targets )
562 ) && $this->runWatchUnwatchCompleteHook(
'Unwatch', $targets );
574 foreach ( $targets as $target ) {
578 $page = $this->wikiPageFactory->newFromTitle(
$title );
580 if ( $action ===
'Watch' ) {
581 $this->getHookRunner()->onWatchArticleComplete( $user, $page );
583 $this->getHookRunner()->onUnwatchArticleComplete( $user, $page );
594 $expandedTargets = [];
595 foreach ( $targets as $target ) {
598 $target = $this->titleParser->parseTitle( $target,
NS_MAIN );
605 $ns = $target->getNamespace();
606 $dbKey = $target->getDBkey();
608 new TitleValue( $this->nsInfo->getSubject( $ns ), $dbKey );
610 new TitleValue( $this->nsInfo->getTalk( $ns ), $dbKey );
612 return $expandedTargets;
618 foreach ( $data as $titles ) {
619 $this->unwatchTitles( $titles );
620 $removed = array_merge( $removed, $titles );
623 if ( count( $removed ) > 0 ) {
624 $this->successMessage = $this->msg(
'watchlistedit-normal-done'
625 )->numParams( count( $removed ) )->parse();
626 $this->showTitles( $removed, $this->successMessage );
645 $watchlistInfo = $this->getWatchlistInfo();
646 $this->getHookRunner()->onWatchlistEditorBeforeFormRender( $watchlistInfo );
648 foreach ( $watchlistInfo as $namespace => $pages ) {
650 foreach ( $pages as $dbkey => $expiryDaysText ) {
653 if ( $this->checkTitle(
$title, $namespace, $dbkey ) ) {
654 $text = $this->buildRemoveLine(
$title, $expiryDaysText );
655 $options[$text] =
$title->getPrefixedText();
661 if ( count( $options ) > 0 ) {
662 $fields[
'TitlesNs' . $namespace] = [
663 'class' => EditWatchlistCheckboxSeriesField::class,
664 'options' => $options,
665 'section' =>
"ns$namespace",
669 $this->cleanupWatchlist();
671 if ( count( $fields ) > 1 && $count > 30 ) {
674 $contLang = $this->getContentLanguage();
676 foreach ( $fields as $data ) {
677 # strip out the 'ns' prefix from the section name:
678 $ns = (int)substr( $data[
'section'], 2 );
681 ? $this->msg(
'blanknamespace' )->escaped()
682 : htmlspecialchars( $contLang->getFormattedNsText( $ns ) );
683 $this->toc .=
Linker::tocLine(
"editwatchlist-{$data['section']}", $nsText,
693 $form->setTitle( $this->getPageTitle() );
694 $form->setSubmitTextMsg(
'watchlistedit-normal-submit' );
695 $form->setSubmitDestructive();
697 # 'accesskey-watchlistedit-normal-submit', 'tooltip-watchlistedit-normal-submit'
698 $form->setSubmitTooltip(
'watchlistedit-normal-submit' );
699 $form->setWrapperLegendMsg(
'watchlistedit-normal-legend' );
700 $form->addHeaderText( $this->msg(
'watchlistedit-normal-explain' )->parse() );
701 $form->setSubmitCallback( [ $this,
'submitNormal' ] );
715 $linkRenderer = $this->getLinkRenderer();
716 $link = $linkRenderer->makeLink(
$title );
719 $tools[
'talk'] = $linkRenderer->makeLink(
721 $this->msg(
'talkpagelinktext' )->text()
725 $tools[
'history'] = $linkRenderer->makeKnownLink(
727 $this->msg(
'history_small' )->text(),
729 [
'action' =>
'history' ]
734 $tools[
'contributions'] = $linkRenderer->makeKnownLink(
736 $this->msg(
'contribslink' )->text()
740 $this->getHookRunner()->onWatchlistEditorBuildRemoveLine(
741 $tools,
$title,
$title->isRedirect(), $this->getSkin(), $link );
743 if (
$title->isRedirect() ) {
745 $link =
'<span class="watchlistredir">' . $link .
'</span>';
748 $watchlistExpiringMessage =
'';
749 if ( $this->getConfig()->
get( MainConfigNames::WatchlistExpiry ) && $expiryDaysText ) {
752 [
'class' =>
'mw-watchlistexpiry-msg' ],
757 return $link .
' ' .
Html::openElement(
'span', [
'class' =>
'mw-changeslist-links' ] ) .
760 array_map(
static function ( $tool ) {
765 $watchlistExpiringMessage;
774 $titles = implode(
"\n", $this->getWatchlist() );
777 'type' =>
'textarea',
778 'label-message' =>
'watchlistedit-raw-titles',
779 'default' => $titles,
783 $form->setTitle( $this->getPageTitle(
'raw' ) );
784 $form->setSubmitTextMsg(
'watchlistedit-raw-submit' );
785 # Used message keys: 'accesskey-watchlistedit-raw-submit', 'tooltip-watchlistedit-raw-submit'
786 $form->setSubmitTooltip(
'watchlistedit-raw-submit' );
787 $form->setWrapperLegendMsg(
'watchlistedit-raw-legend' );
788 $form->addHeaderText( $this->msg(
'watchlistedit-raw-explain' )->parse() );
789 $form->setSubmitCallback( [ $this,
'submitRaw' ] );
801 $form->setTitle( $this->getPageTitle(
'clear' ) );
802 $form->setSubmitTextMsg(
'watchlistedit-clear-submit' );
803 # Used message keys: 'accesskey-watchlistedit-clear-submit', 'tooltip-watchlistedit-clear-submit'
804 $form->setSubmitTooltip(
'watchlistedit-clear-submit' );
805 $form->setWrapperLegendMsg(
'watchlistedit-clear-legend' );
806 $form->addHeaderText( $this->msg(
'watchlistedit-clear-explain' )->parse() );
807 $form->setSubmitCallback( [ $this,
'submitClear' ] );
808 $form->setSubmitDestructive();
822 public static function getMode( $request, $par, $defaultValue =
false ) {
823 $mode = strtolower( $request->getRawVal(
'action', $par ) );
827 case self::EDIT_CLEAR:
828 return self::EDIT_CLEAR;
831 return self::EDIT_RAW;
833 case self::EDIT_NORMAL:
834 return self::EDIT_NORMAL;
836 return $defaultValue;
855 if ( !$linkRenderer ) {
856 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
861 'view' => [
'Watchlist',
false, false ],
862 'edit' => [
'EditWatchlist',
false, self::EDIT_NORMAL ],
863 'raw' => [
'EditWatchlist',
'raw', self::EDIT_RAW ],
864 'clear' => [
'EditWatchlist',
'clear', self::EDIT_CLEAR ],
867 foreach ( $modes as $mode => $arr ) {
869 $link = $linkRenderer->makeKnownLink(
871 wfMessage(
"watchlisttools-{$mode}" )->text()
873 $isSelected = $selectedMode === $arr[2];
875 'mw-watchlist-toollink',
876 'mw-watchlist-toollink-' . $mode,
877 $isSelected ?
'mw-watchlist-toollink-active' :
878 'mw-watchlist-toollink-inactive'
887 [
'class' =>
'mw-watchlist-toollinks mw-changeslist-links' ],
888 implode(
'', $tools )
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(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode) $wgLang
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add an update to the pending update queue that invokes the specified callback when run.
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()).
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing '/'...
static closeElement( $element)
Returns "</$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.
A class containing constants representing the names of configuration variables.
Service for creating WikiPage objects.
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.
static buildTools( $lang, LinkRenderer $linkRenderer=null, $selectedMode=false)
Build a set of links for convenient navigation between watchlist viewing and editing modes.
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.
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.
int false $currentMode
where the value is one of the EDIT_ prefixed constants (e.g.
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.
static getMode( $request, $par, $defaultValue=false)
Determine whether we are editing the watchlist, and if so, what kind of editing operation.
buildRemoveLine( $title, string $expiryDaysText='')
Build the label for a checkbox, with a link to the title, and various additional bits.
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.
getUser()
Shortcut to get the User executing this instance.
checkPermissions()
Checks if userCanExecute, and if not throws a PermissionsError.
requireNamedUser( $reasonMsg='exception-nologin-text', $titleMsg='exception-nologin')
If the user is not logged in or is a temporary user, throws UserNotLoggedIn.
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.
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Shortcut to construct a special page which is unlisted by default.
A title parser service for MediaWiki.
if(!isset( $args[0])) $lang