49 public function __construct( $page =
'Watchlist', $restriction =
'viewmywatchlist' ) {
50 parent::__construct( $page, $restriction );
52 $this->maxDays = $this->
getConfig()->get(
'RCMaxAge' ) / ( 3600 * 24 );
53 $this->watchStore = MediaWikiServices::getInstance()->getWatchedItemStore();
54 $this->isWatchlistExpiryEnabled = $this->
getConfig()->get(
'WatchlistExpiry' );
73 $output->addModuleStyles( [
'mediawiki.special' ] );
74 $output->addModules( [
75 'mediawiki.special.recentchanges',
76 'mediawiki.special.watchlist',
80 if ( $mode !==
false ) {
89 $output->redirect(
$title->getLocalURL() );
100 if ( ( $config->get(
'EnotifWatchlist' ) || $config->get(
'ShowUpdatedMarker' ) )
101 && $request->getVal(
'reset' )
102 && $request->wasPosted()
103 && $user->matchEditToken( $request->getVal(
'token' ) )
105 MediaWikiServices::getInstance()
106 ->getWatchlistNotificationManager()
107 ->clearAllUserNotifications( $user );
108 $output->redirect( $this->
getPageTitle()->getFullURL( $opts->getChangedValues() ) );
113 parent::execute( $subpage );
116 $output->addModuleStyles( [
'mediawiki.rcfilters.highlightCircles.seenunseen.styles' ] );
124 if ( $user instanceof
Config ) {
125 wfDeprecated( __METHOD__ .
' with Config argument',
'1.34' );
126 $user = func_get_arg( 1 );
128 return !$user->getOption(
'wlenhancedfilters-disable' );
149 if ( isset( $filterDefinition[
'showHideSuffix'] ) ) {
150 $filterDefinition[
'showHide'] =
'wl' . $filterDefinition[
'showHideSuffix'];
153 return $filterDefinition;
161 parent::registerFilters();
165 'name' =>
'extended-group',
168 'name' =>
'extended',
169 'isReplacedInStructuredUi' =>
true,
170 'activeValue' =>
false,
171 'default' => $this->
getUser()->getBoolOption(
'extendwatchlist' ),
172 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables,
173 &$fields, &$conds, &$query_options, &$join_conds ) {
174 $nonRevisionTypes = [
RC_LOG ];
175 $this->
getHookRunner()->onSpecialWatchlistGetNonRevisionTypes(
177 if ( $nonRevisionTypes ) {
178 $conds[] =
$dbr->makeList(
180 'rc_this_oldid=page_latest',
181 'rc_type' => $nonRevisionTypes,
194 ->getFilter(
'hidepreviousrevisions' )
195 ->setDefault( !$this->
getUser()->getBoolOption(
'extendwatchlist' ) );
199 'name' =>
'watchlistactivity',
200 'title' =>
'rcfilters-filtergroup-watchlistactivity',
201 'class' => ChangesListStringOptionsFilterGroup::class,
203 'isFullCoverage' =>
true,
207 'label' =>
'rcfilters-filter-watchlistactivity-unseen-label',
208 'description' =>
'rcfilters-filter-watchlistactivity-unseen-description',
209 'cssClassSuffix' =>
'watchedunseen',
210 'isRowApplicableCallable' =>
function ( $ctx,
RecentChange $rc ) {
216 'label' =>
'rcfilters-filter-watchlistactivity-seen-label',
217 'description' =>
'rcfilters-filter-watchlistactivity-seen-description',
218 'cssClassSuffix' =>
'watchedseen',
219 'isRowApplicableCallable' =>
function ( $ctx,
RecentChange $rc ) {
225 'queryCallable' =>
function (
226 $specialPageClassName,
236 if ( $selectedValues === [
'seen' ] ) {
237 $conds[] =
$dbr->makeList( [
238 'wl_notificationtimestamp IS NULL',
239 'rc_timestamp < wl_notificationtimestamp'
241 } elseif ( $selectedValues === [
'unseen' ] ) {
242 $conds[] =
$dbr->makeList( [
243 'wl_notificationtimestamp IS NOT NULL',
244 'rc_timestamp >= wl_notificationtimestamp'
253 $hideMinor = $significance->getFilter(
'hideminor' );
254 $hideMinor->setDefault( $user->getBoolOption(
'watchlisthideminor' ) );
257 $hideBots = $automated->getFilter(
'hidebots' );
258 $hideBots->setDefault( $user->getBoolOption(
'watchlisthidebots' ) );
261 $hideAnons = $registration->getFilter(
'hideanons' );
262 $hideAnons->setDefault( $user->getBoolOption(
'watchlisthideanons' ) );
263 $hideLiu = $registration->getFilter(
'hideliu' );
264 $hideLiu->setDefault( $user->getBoolOption(
'watchlisthideliu' ) );
268 if ( $user->getBoolOption(
'watchlisthideanons' ) &&
269 !$user->getBoolOption(
'watchlisthideliu' )
272 ->setDefault(
'registered' );
275 if ( $user->getBoolOption(
'watchlisthideliu' ) &&
276 !$user->getBoolOption(
'watchlisthideanons' )
279 ->setDefault(
'unregistered' );
283 if ( $reviewStatus !==
null ) {
285 if ( $user->getBoolOption(
'watchlisthidepatrolled' ) ) {
286 $reviewStatus->setDefault(
'unpatrolled' );
287 $legacyReviewStatus = $this->
getFilterGroup(
'legacyReviewStatus' );
288 $legacyHidePatrolled = $legacyReviewStatus->getFilter(
'hidepatrolled' );
289 $legacyHidePatrolled->setDefault(
true );
294 $hideMyself = $authorship->getFilter(
'hidemyself' );
295 $hideMyself->setDefault( $user->getBoolOption(
'watchlisthideown' ) );
298 $hideCategorization = $changeType->getFilter(
'hidecategorization' );
299 if ( $hideCategorization !==
null ) {
301 $hideCategorization->setDefault( $user->getBoolOption(
'watchlisthidecategorization' ) );
315 static $compatibilityMap = [
316 'hideMinor' =>
'hideminor',
317 'hideBots' =>
'hidebots',
318 'hideAnons' =>
'hideanons',
319 'hideLiu' =>
'hideliu',
320 'hidePatrolled' =>
'hidepatrolled',
321 'hideOwn' =>
'hidemyself',
325 foreach ( $compatibilityMap as $from => $to ) {
326 if ( isset( $params[$from] ) ) {
327 $params[$to] = $params[$from];
328 unset( $params[$from] );
332 if ( $this->
getRequest()->getVal(
'action' ) ==
'submit' ) {
333 $allBooleansFalse = [];
342 $allBooleansFalse[ $filter->getName() ] =
false;
345 $params += $allBooleansFalse;
351 $opts->fetchValuesFromRequest( $request );
359 protected function doMainQuery( $tables, $fields, $conds, $query_options,
365 $rcQuery = RecentChange::getQueryInfo();
366 $tables = array_merge( $tables, $rcQuery[
'tables'], [
'watchlist' ] );
367 $fields = array_merge( $rcQuery[
'fields'], $fields );
369 $join_conds = array_merge(
374 'wl_user' => $user->getId(),
375 'wl_namespace=rc_namespace',
384 if ( $this->isWatchlistExpiryEnabled ) {
385 $tables[] =
'watchlist_expiry';
386 $fields[] =
'we_expiry';
387 $join_conds[
'watchlist_expiry'] = [
'LEFT JOIN',
'wl_id = we_item' ];
388 $conds[] =
'we_expiry IS NULL OR we_expiry > ' .
$dbr->addQuotes(
$dbr->timestamp() );
392 $fields[] =
'page_latest';
393 $join_conds[
'page'] = [
'LEFT JOIN',
'rc_cur_id=page_id' ];
395 $fields[] =
'wl_notificationtimestamp';
399 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
400 if ( !$permissionManager->userHasRight( $user,
'deletedhistory' ) ) {
402 } elseif ( !$permissionManager->userHasAnyRight( $user,
'suppressrevision',
'viewsuppressed' ) ) {
408 $conds[] =
$dbr->makeList( [
410 $dbr->bitAnd(
'rc_deleted', $bitmask ) .
" != $bitmask",
414 $tagFilter = $opts[
'tagfilter'] !==
'' ? explode(
'|', $opts[
'tagfilter'] ) : [];
424 $this->
runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds, $opts );
431 'ORDER BY' =>
'rc_timestamp DESC',
432 'LIMIT' => $opts[
'limit']
434 if ( in_array(
'DISTINCT', $query_options ) ) {
440 $orderByAndLimit[
'ORDER BY'] =
'rc_timestamp DESC, rc_id DESC';
441 $orderByAndLimit[
'GROUP BY'] =
'rc_timestamp, rc_id';
445 $query_options = array_merge( $orderByAndLimit, $query_options );
446 $query_options[
'MAX_EXECUTION_TIME'] = $this->
getConfig()->get(
'MaxExecutionTimeForExpensiveQueries' );
472 $wlToken = $user->getTokenFromOption(
'watchlisttoken' );
475 'action' =>
'feedwatchlist',
477 'wlowner' => $user->getName(),
478 'wltoken' => $wlToken,
495 $lag =
$dbr->getSessionLagStatus()[
'lag'];
497 $output->showLagWarning( $lag );
501 if ( $rows->numRows() == 0 ) {
502 $output->wrapWikiMsg(
503 "<div class='mw-changeslist-empty'>\n$1\n</div>",
'recentchanges-noresult'
508 $dbr->dataSeek( $rows, 0 );
510 $list = ChangesList::newFromContext( $this->
getContext(), $this->filterGroups );
511 $list->setWatchlistDivs();
512 $list->initChangesListRows( $rows );
514 if ( $user->getOption(
'watchlistunwatchlinks' ) ) {
522 $unwatchTooltipMessage =
'tooltip-ca-unwatch';
525 if ( $this->isWatchlistExpiryEnabled ) {
526 $watchedItem = $this->watchStore->getWatchedItem( $this->getUser(), $rc->getTitle() );
527 if ( $watchedItem instanceof WatchedItem && $watchedItem->getExpiry() !== null ) {
528 $diffInDays = $watchedItem->getExpiryInDays();
530 if ( $diffInDays > 0 ) {
531 $unwatchTooltipMessage =
'tooltip-ca-unwatch-expiring';
533 $unwatchTooltipMessage =
'tooltip-ca-unwatch-expiring-hours';
538 return $this->getLinkRenderer()
540 $this->msg(
'watchlist-unwatch' )->text(), [
541 'class' =>
'mw-unwatch-link',
542 'title' => $this->msg( $unwatchTooltipMessage, [ $diffInDays ] )->text()
543 ], [
'action' =>
'unwatch' ] ) .
"\u{00A0}";
547 $dbr->dataSeek( $rows, 0 );
549 $s = $list->beginRecentChangesList();
555 $userShowHiddenCats = $this->
getUser()->getBoolOption(
'showhiddencats' );
557 foreach ( $rows as $obj ) {
564 !$userShowHiddenCats &&
565 $rc->getParam(
'hidden-cat' )
570 $rc->counter = $counter++;
572 if ( $this->
getConfig()->
get(
'ShowUpdatedMarker' ) ) {
578 if ( $this->
getConfig()->
get(
'RCShowWatchingUsers' )
579 && $user->getOption(
'shownumberswatching' )
581 $rcTitleValue =
new TitleValue( (
int)$obj->rc_namespace, $obj->rc_title );
582 $rc->numberofWatchingusers = $this->watchStore->countWatchers( $rcTitleValue );
584 $rc->numberofWatchingusers = 0;
590 $changeLine = $list->recentChangesLine( $rc, $unseen, $counter );
591 if ( $changeLine !==
false ) {
595 $s .= $list->endRecentChangesList();
597 $output->addHTML(
$s );
608 $out = $this->getOutput();
611 $this->msg(
'watchlistfor2', $user->getName() )
613 $this->getLanguage(),
614 $this->getLinkRenderer()
618 $this->setTopText( $opts );
622 $form .= Xml::openElement(
'form', [
625 'id' =>
'mw-watchlist-form'
627 $form .= Html::hidden(
'title', $this->getPageTitle()->getPrefixedText() );
628 $form .= Xml::openElement(
630 [
'id' =>
'mw-watchlist-options',
'class' =>
'cloptions' ]
632 $form .= Xml::element(
633 'legend',
null, $this->msg(
'watchlist-options' )->text()
636 if ( !$this->isStructuredFilterUiEnabled() ) {
637 $form .= $this->makeLegend();
640 $lang = $this->getLanguage();
642 $now =
$lang->userTimeAndDate( $timestamp, $user );
643 $wlInfo = Html::rawElement(
647 'data-params' => json_encode( [
'from' => $timestamp,
'fromFormatted' => $now ] ),
649 $this->msg(
'wlnote' )->numParams( $numRows, round( $opts[
'days'] * 24 ) )->params(
650 $lang->userDate( $timestamp, $user ),
$lang->userTime( $timestamp, $user )
654 $nondefaults = $opts->getChangedValues();
655 $cutofflinks = Html::rawElement(
657 [
'class' =>
'cldays cloption' ],
658 $this->msg(
'wlshowtime' ) .
' ' . $this->cutoffselector( $opts )
663 $namesOfDisplayedFilters = [];
664 foreach ( $this->getLegacyShowHideFilters() as $filterName => $filter ) {
665 $namesOfDisplayedFilters[] = $filterName;
666 $links[] = $this->showHideCheck(
668 $filter->getShowHide(),
670 $opts[ $filterName ],
671 $filter->isFeatureAvailableOnStructuredUi()
675 $hiddenFields = $nondefaults;
676 $hiddenFields[
'action'] =
'submit';
677 unset( $hiddenFields[
'namespace'] );
678 unset( $hiddenFields[
'invert'] );
679 unset( $hiddenFields[
'associated'] );
680 unset( $hiddenFields[
'days'] );
681 foreach ( $namesOfDisplayedFilters as $filterName ) {
682 unset( $hiddenFields[$filterName] );
687 $form .= $cutofflinks;
688 $form .= Html::rawElement(
690 [
'class' =>
'clshowhide' ],
691 $this->msg(
'watchlist-hide' ) .
692 $this->msg(
'colon-separator' )->escaped() .
693 implode(
' ', $links )
695 $form .=
"\n<br />\n";
697 $namespaceForm = Html::namespaceSelector(
699 'selected' => $opts[
'namespace'],
701 'label' => $this->msg(
'namespace' )->text(),
702 'in-user-lang' =>
true,
704 'name' =>
'namespace',
706 'class' =>
'namespaceselector',
709 $hidden = $opts[
'namespace'] ===
'' ?
' mw-input-hidden' :
'';
710 $namespaceForm .=
'<span class="mw-input-with-label' . $hidden .
'">' . Xml::checkLabel(
711 $this->msg(
'invert' )->text(),
715 [
'title' => $this->msg(
'tooltip-invert' )->text() ]
717 $namespaceForm .=
'<span class="mw-input-with-label' . $hidden .
'">' . Xml::checkLabel(
718 $this->msg(
'namespace_association' )->text(),
722 [
'title' => $this->msg(
'tooltip-namespace_association' )->text() ]
724 $form .= Html::rawElement(
726 [
'class' =>
'namespaceForm cloption' ],
730 $form .= Xml::submitButton(
731 $this->msg(
'watchlist-submit' )->text(),
732 [
'class' =>
'cloption-submit' ]
734 foreach ( $hiddenFields as $key => $value ) {
735 $form .= Html::hidden( $key, $value ) .
"\n";
737 $form .= Xml::closeElement(
'fieldset' ) .
"\n";
738 $form .= Xml::closeElement(
'form' ) .
"\n";
741 if ( $this->isStructuredFilterUiEnabled() ) {
742 $rcfilterContainer = Html::element(
745 [
'class' =>
'rcfilters-container mw-rcfilters-container' ]
748 $loadingContainer = Html::rawElement(
750 [
'class' =>
'mw-rcfilters-spinner' ],
753 [
'class' =>
'mw-rcfilters-spinner-bounce' ]
758 $this->getOutput()->addHTML(
762 [
'class' =>
'rcfilters-head mw-rcfilters-head' ],
763 $rcfilterContainer . $form
768 $this->getOutput()->addHTML( $loadingContainer );
770 $this->getOutput()->addHTML( $form );
773 $this->setBottomText( $opts );
777 $selected = (float)$options[
'days'];
778 if ( $selected <= 0 ) {
779 $selected = $this->maxDays;
782 $selectedHours = round( $selected * 24 );
784 $hours = array_unique( array_filter( [
792 24 * (
float)$this->
getUser()->getOption(
'watchlistdays', 0 ),
798 $select =
new XmlSelect(
'days',
'days', (
float)( $selectedHours / 24 ) );
800 foreach ( $hours as $value ) {
802 $name = $this->msg(
'hours' )->numParams( $value )->text();
804 $name = $this->msg(
'days' )->numParams( $value / 24 )->text();
806 $select->addOption( $name, (
float)( $value / 24 ) );
809 return $select->getHTML() .
"\n<br />\n";
817 $numItems = $this->countItems();
818 $showUpdatedMarker = $this->getConfig()->get(
'ShowUpdatedMarker' );
821 $watchlistHeader =
'';
822 if ( $numItems == 0 ) {
823 $watchlistHeader = $this->msg(
'nowatchlist' )->parse();
825 $watchlistHeader .= $this->msg(
'watchlist-details' )->numParams( $numItems )->parse() .
"\n";
826 if ( $this->getConfig()->
get(
'EnotifWatchlist' )
827 && $user->getOption(
'enotifwatchlistpages' )
829 $watchlistHeader .= $this->msg(
'wlheader-enotif' )->parse() .
"\n";
831 if ( $showUpdatedMarker ) {
832 $watchlistHeader .= $this->msg(
833 $this->isStructuredFilterUiEnabled() ?
834 'rcfilters-watchlist-showupdated' :
835 'wlheader-showupdated'
839 $form .= Html::rawElement(
841 [
'class' =>
'watchlistDetails' ],
845 if ( $numItems > 0 && $showUpdatedMarker ) {
846 $form .= Xml::openElement(
'form', [
'method' =>
'post',
847 'action' => $this->getPageTitle()->getLocalURL(),
848 'id' =>
'mw-watchlist-resetbutton' ] ) .
"\n" .
849 Xml::submitButton( $this->msg(
'enotif_reset' )->text(),
850 [
'name' =>
'mw-watchlist-reset-submit' ] ) .
"\n" .
851 Html::hidden(
'token', $user->getEditToken() ) .
"\n" .
852 Html::hidden(
'reset',
'all' ) .
"\n";
853 foreach ( $nondefaults as $key => $value ) {
854 $form .= Html::hidden( $key, $value ) .
"\n";
856 $form .= Xml::closeElement(
'form' ) .
"\n";
859 $this->getOutput()->addHTML( $form );
862 protected function showHideCheck( $options, $message, $name, $value, $inStructuredUi ) {
863 $options[$name] = 1 - (int)$value;
865 $attribs = [
'class' =>
'mw-input-with-label clshowhideoption cloption' ];
866 if ( $inStructuredUi ) {
867 $attribs[
'data-feature-in-structured-ui' ] =
true;
870 return Html::rawElement(
874 Html::check( $name, (
int)$value, [
'id' => $name ] ) . Html::rawElement(
876 $attribs + [
'for' => $name ],
878 $this->msg( $message,
'<nowiki/>' )->parse()
891 $count = $this->watchStore->countWatchedItems( $this->
getUser() );
892 return floor( $count / 2 );
900 $firstUnseen = $this->getLatestNotificationTimestamp( $rc );
902 return ( $firstUnseen ===
null || $firstUnseen > $rc->
getAttribute(
'rc_timestamp' ) );
910 return $this->watchStore->getLatestNotificationTimestamp(
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
If the group is active, any unchecked filters will translate to hide parameters in the URL.
Special page which uses a ChangesList to show query results.
runMainQueryHook(&$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts)
getFilterGroup( $groupName)
Gets a specified ChangesListFilterGroup by name.
isStructuredFilterUiEnabled()
Check whether the structured filter UI is enabled.
areFiltersInConflict()
Check if filters are in conflict and guaranteed to return no results.
registerFilterGroup(ChangesListFilterGroup $group)
Register a structured changes list filter group.
getLegacyShowHideFilters()
getOptions()
Get the current FormOptions for this request.
makeLegend()
Return the legend displayed within the fieldset.
Represents a filter group with multiple string options.
const NONE
Signifies that no options in the group are selected, meaning the group has no effect.
Similar to FauxRequest, but only fakes URL parameters and method (POST or GET) and use the base reque...
Utility class for creating new RC entries.
getAttribute( $name)
Get an attribute value.
static buildTools( $lang, LinkRenderer $linkRenderer=null)
Build a set of links for convenient navigation between watchlist viewing and editing modes.
const EDIT_CLEAR
Editing modes.
static getMode( $request, $par)
Determine whether we are editing the watchlist, and if so, what kind of editing operation.
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,...
addFeedLinks( $params)
Adds RSS/atom links.
getContext()
Gets the context this SpecialPage is executed in.
getConfig()
Shortcut to get main config object.
getRequest()
Get the WebRequest being used for this instance.
getPageTitle( $subpage=false)
Get a self-referential title object.
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
A special page that lists last changes made to the wiki, limited to user-defined list of titles.
__construct( $page='Watchlist', $restriction='viewmywatchlist')
bool $isWatchlistExpiryEnabled
Watchlist Expiry flag.
outputFeedLinks()
Output feed links.
doHeader( $opts, $numRows)
Set the text to be displayed above the changes.
static $limitPreferenceName
showHideCheck( $options, $message, $name, $value, $inStructuredUi)
getSubpagesForPrefixSearch()
Return an array of subpages that this special page will accept.
WatchedItemStore $watchStore
static $daysPreferenceName
fetchOptionsFromRequest( $opts)
Fetch values for a FormOptions object from the WebRequest associated with this instance.
static checkStructuredFilterUiEnabled( $user)
Static method to check whether StructuredFilter UI is enabled for the given user.1....
doMainQuery( $tables, $fields, $conds, $query_options, $join_conds, FormOptions $opts)
Process the query.bool|IResultWrapper Result or false
static $savedQueriesPreferenceName
getDB()
Return a IDatabase object for reading.
transformFilterDefinition(array $filterDefinition)
Transforms filter definition to prepare it for constructor.See overrides of this method as well....
countItems()
Count the number of paired items on a user's watchlist.
cutoffselector( $options)
setTopText(FormOptions $opts)
Send the text to be displayed before the options.
execute( $subpage)
Main execution point.
registerFilters()
Register all filters and their groups (including those from hooks), plus handle conflicts and default...
getLatestNotificationTimestamp(RecentChange $rc)
static $collapsedPreferenceName
outputChangesList( $rows, $opts)
Build and output the actual changes list.
isChangeEffectivelySeen(RecentChange $rc)
doesWrites()
Indicates whether this special page may perform database writes.
Represents a page (or page fragment) title within MediaWiki.
Storage layer class for WatchedItems.
Class for generating HTML <select> or <datalist> elements.
Interface for configuration instances.
if(!isset( $args[0])) $lang