45 public function __construct( $page =
'Watchlist', $restriction =
'viewmywatchlist' ) {
46 parent::__construct( $page, $restriction );
48 $this->maxDays = $this->
getConfig()->get(
'RCMaxAge' ) / ( 3600 * 24 );
49 $this->watchStore = MediaWikiServices::getInstance()->getWatchedItemStore();
68 $output->addModuleStyles( [
'mediawiki.special' ] );
69 $output->addModules( [
70 'mediawiki.special.recentchanges',
71 'mediawiki.special.watchlist',
75 if ( $mode !==
false ) {
84 $output->redirect(
$title->getLocalURL() );
95 if ( ( $config->get(
'EnotifWatchlist' ) || $config->get(
'ShowUpdatedMarker' ) )
96 && $request->getVal(
'reset' )
97 && $request->wasPosted()
98 && $user->matchEditToken( $request->getVal(
'token' ) )
100 $user->clearAllNotifications();
101 $output->redirect( $this->
getPageTitle()->getFullURL( $opts->getChangedValues() ) );
106 parent::execute( $subpage );
109 $output->addModuleStyles( [
'mediawiki.rcfilters.highlightCircles.seenunseen.styles' ] );
117 if ( $user instanceof
Config ) {
118 wfDeprecated( __METHOD__ .
' with Config argument',
'1.34' );
119 $user = func_get_arg( 1 );
121 return !$user->getOption(
'wlenhancedfilters-disable' );
142 if ( isset( $filterDefinition[
'showHideSuffix'] ) ) {
143 $filterDefinition[
'showHide'] =
'wl' . $filterDefinition[
'showHideSuffix'];
146 return $filterDefinition;
154 parent::registerFilters();
158 'name' =>
'extended-group',
161 'name' =>
'extended',
162 'isReplacedInStructuredUi' =>
true,
163 'activeValue' =>
false,
164 'default' => $this->
getUser()->getBoolOption(
'extendwatchlist' ),
165 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables,
166 &$fields, &$conds, &$query_options, &$join_conds ) {
167 $nonRevisionTypes = [
RC_LOG ];
168 Hooks::run(
'SpecialWatchlistGetNonRevisionTypes', [ &$nonRevisionTypes ] );
169 if ( $nonRevisionTypes ) {
170 $conds[] =
$dbr->makeList(
172 'rc_this_oldid=page_latest',
173 'rc_type' => $nonRevisionTypes,
186 ->getFilter(
'hidepreviousrevisions' )
187 ->setDefault( !$this->
getUser()->getBoolOption(
'extendwatchlist' ) );
191 'name' =>
'watchlistactivity',
192 'title' =>
'rcfilters-filtergroup-watchlistactivity',
193 'class' => ChangesListStringOptionsFilterGroup::class,
195 'isFullCoverage' =>
true,
199 'label' =>
'rcfilters-filter-watchlistactivity-unseen-label',
200 'description' =>
'rcfilters-filter-watchlistactivity-unseen-description',
201 'cssClassSuffix' =>
'watchedunseen',
202 'isRowApplicableCallable' =>
function ( $ctx,
RecentChange $rc ) {
208 'label' =>
'rcfilters-filter-watchlistactivity-seen-label',
209 'description' =>
'rcfilters-filter-watchlistactivity-seen-description',
210 'cssClassSuffix' =>
'watchedseen',
211 'isRowApplicableCallable' =>
function ( $ctx,
RecentChange $rc ) {
217 'queryCallable' =>
function (
218 $specialPageClassName,
228 if ( $selectedValues === [
'seen' ] ) {
229 $conds[] =
$dbr->makeList( [
230 'wl_notificationtimestamp IS NULL',
231 'rc_timestamp < wl_notificationtimestamp'
233 } elseif ( $selectedValues === [
'unseen' ] ) {
234 $conds[] =
$dbr->makeList( [
235 'wl_notificationtimestamp IS NOT NULL',
236 'rc_timestamp >= wl_notificationtimestamp'
245 $hideMinor = $significance->getFilter(
'hideminor' );
246 $hideMinor->setDefault( $user->getBoolOption(
'watchlisthideminor' ) );
249 $hideBots = $automated->getFilter(
'hidebots' );
250 $hideBots->setDefault( $user->getBoolOption(
'watchlisthidebots' ) );
253 $hideAnons = $registration->getFilter(
'hideanons' );
254 $hideAnons->setDefault( $user->getBoolOption(
'watchlisthideanons' ) );
255 $hideLiu = $registration->getFilter(
'hideliu' );
256 $hideLiu->setDefault( $user->getBoolOption(
'watchlisthideliu' ) );
260 if ( $user->getBoolOption(
'watchlisthideanons' ) &&
261 !$user->getBoolOption(
'watchlisthideliu' )
264 ->setDefault(
'registered' );
267 if ( $user->getBoolOption(
'watchlisthideliu' ) &&
268 !$user->getBoolOption(
'watchlisthideanons' )
271 ->setDefault(
'unregistered' );
275 if ( $reviewStatus !==
null ) {
277 if ( $user->getBoolOption(
'watchlisthidepatrolled' ) ) {
278 $reviewStatus->setDefault(
'unpatrolled' );
279 $legacyReviewStatus = $this->
getFilterGroup(
'legacyReviewStatus' );
280 $legacyHidePatrolled = $legacyReviewStatus->getFilter(
'hidepatrolled' );
281 $legacyHidePatrolled->setDefault(
true );
286 $hideMyself = $authorship->getFilter(
'hidemyself' );
287 $hideMyself->setDefault( $user->getBoolOption(
'watchlisthideown' ) );
290 $hideCategorization = $changeType->getFilter(
'hidecategorization' );
291 if ( $hideCategorization !==
null ) {
293 $hideCategorization->setDefault( $user->getBoolOption(
'watchlisthidecategorization' ) );
307 static $compatibilityMap = [
308 'hideMinor' =>
'hideminor',
309 'hideBots' =>
'hidebots',
310 'hideAnons' =>
'hideanons',
311 'hideLiu' =>
'hideliu',
312 'hidePatrolled' =>
'hidepatrolled',
313 'hideOwn' =>
'hidemyself',
317 foreach ( $compatibilityMap as $from => $to ) {
318 if ( isset( $params[$from] ) ) {
319 $params[$to] = $params[$from];
320 unset( $params[$from] );
324 if ( $this->
getRequest()->getVal(
'action' ) ==
'submit' ) {
325 $allBooleansFalse = [];
334 $allBooleansFalse[
$filter->getName() ] =
false;
337 $params = $params + $allBooleansFalse;
343 $opts->fetchValuesFromRequest( $request );
351 protected function doMainQuery( $tables, $fields, $conds, $query_options,
357 $rcQuery = RecentChange::getQueryInfo();
358 $tables = array_merge( $tables, $rcQuery[
'tables'], [
'watchlist' ] );
359 $fields = array_merge( $rcQuery[
'fields'], $fields );
361 $join_conds = array_merge(
366 'wl_user' => $user->getId(),
367 'wl_namespace=rc_namespace',
377 $fields[] =
'page_latest';
378 $join_conds[
'page'] = [
'LEFT JOIN',
'rc_cur_id=page_id' ];
380 $fields[] =
'wl_notificationtimestamp';
384 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
385 if ( !$permissionManager->userHasRight( $user,
'deletedhistory' ) ) {
387 } elseif ( !$permissionManager->userHasAnyRight( $user,
'suppressrevision',
'viewsuppressed' ) ) {
393 $conds[] =
$dbr->makeList( [
395 $dbr->bitAnd(
'rc_deleted', $bitmask ) .
" != $bitmask",
399 $tagFilter = $opts[
'tagfilter'] ? explode(
'|', $opts[
'tagfilter'] ) : [];
409 $this->
runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds, $opts );
416 'ORDER BY' =>
'rc_timestamp DESC',
417 'LIMIT' => $opts[
'limit']
419 if ( in_array(
'DISTINCT', $query_options ) ) {
425 $orderByAndLimit[
'ORDER BY'] =
'rc_timestamp DESC, rc_id DESC';
426 $orderByAndLimit[
'GROUP BY'] =
'rc_timestamp, rc_id';
430 $query_options = array_merge( $orderByAndLimit, $query_options );
456 $wlToken = $user->getTokenFromOption(
'watchlisttoken' );
459 'action' =>
'feedwatchlist',
461 'wlowner' => $user->getName(),
462 'wltoken' => $wlToken,
477 $services = MediaWikiServices::getInstance();
479 # Show a message about replica DB lag, if applicable
480 $lag =
$dbr->getSessionLagStatus()[
'lag'];
482 $output->showLagWarning( $lag );
485 # If no rows to display, show message before try to render the list
486 if ( $rows->numRows() == 0 ) {
487 $output->wrapWikiMsg(
488 "<div class='mw-changeslist-empty'>\n$1\n</div>",
'recentchanges-noresult'
493 $dbr->dataSeek( $rows, 0 );
496 $list->setWatchlistDivs();
497 $list->initChangesListRows( $rows );
498 if ( $user->getOption(
'watchlistunwatchlinks' ) ) {
506 return $this->getLinkRenderer()
507 ->makeKnownLink( $rc->getTitle(),
508 $this->msg(
'watchlist-unwatch' )->text(), [
509 'class' =>
'mw-unwatch-link',
510 'title' => $this->msg(
'tooltip-ca-unwatch' )->text()
511 ], [
'action' =>
'unwatch' ] ) .
"\u{00A0}";
515 $dbr->dataSeek( $rows, 0 );
517 if ( $this->
getConfig()->
get(
'RCShowWatchingUsers' )
518 && $user->getOption(
'shownumberswatching' )
520 $watchedItemStore = $services->getWatchedItemStore();
523 $s = $list->beginRecentChangesList();
529 $userShowHiddenCats = $this->
getUser()->getBoolOption(
'showhiddencats' );
531 foreach ( $rows as $obj ) {
533 $rc = RecentChange::newFromRow( $obj );
535 # Skip CatWatch entries for hidden cats based on user preference
538 !$userShowHiddenCats &&
539 $rc->getParam(
'hidden-cat' )
544 $rc->counter = $counter++;
546 if ( $this->
getConfig()->
get(
'ShowUpdatedMarker' ) ) {
552 if ( isset( $watchedItemStore ) ) {
553 $rcTitleValue =
new TitleValue( (
int)$obj->rc_namespace, $obj->rc_title );
554 $rc->numberofWatchingusers = $watchedItemStore->countWatchers( $rcTitleValue );
556 $rc->numberofWatchingusers = 0;
562 $changeLine = $list->recentChangesLine( $rc, $unseen, $counter );
563 if ( $changeLine !==
false ) {
567 $s .= $list->endRecentChangesList();
569 $output->addHTML(
$s );
583 $this->
msg(
'watchlistfor2', $user->getName() )
585 $this->getLanguage(),
586 $this->getLinkRenderer()
594 $form .= Xml::openElement(
'form', [
597 'id' =>
'mw-watchlist-form'
599 $form .= Html::hidden(
'title', $this->
getPageTitle()->getPrefixedText() );
600 $form .= Xml::openElement(
602 [
'id' =>
'mw-watchlist-options',
'class' =>
'cloptions' ]
604 $form .= Xml::element(
605 'legend',
null, $this->
msg(
'watchlist-options' )->text()
614 $now =
$lang->userTimeAndDate( $timestamp, $user );
615 $wlInfo = Html::rawElement(
619 'data-params' => json_encode( [
'from' => $timestamp,
'fromFormatted' => $now ] ),
621 $this->
msg(
'wlnote' )->numParams( $numRows, round( $opts[
'days'] * 24 ) )->params(
622 $lang->userDate( $timestamp, $user ),
$lang->userTime( $timestamp, $user )
626 $nondefaults = $opts->getChangedValues();
627 $cutofflinks = Html::rawElement(
629 [
'class' =>
'cldays cloption' ],
633 # Spit out some control panel links
635 $namesOfDisplayedFilters = [];
637 $namesOfDisplayedFilters[] = $filterName;
642 $opts[ $filterName ],
643 $filter->isFeatureAvailableOnStructuredUi( $this )
647 $hiddenFields = $nondefaults;
648 $hiddenFields[
'action'] =
'submit';
649 unset( $hiddenFields[
'namespace'] );
650 unset( $hiddenFields[
'invert'] );
651 unset( $hiddenFields[
'associated'] );
652 unset( $hiddenFields[
'days'] );
653 foreach ( $namesOfDisplayedFilters as $filterName ) {
654 unset( $hiddenFields[$filterName] );
657 # Namespace filter and put the whole form together.
659 $form .= $cutofflinks;
660 $form .= Html::rawElement(
662 [
'class' =>
'clshowhide' ],
663 $this->
msg(
'watchlist-hide' ) .
664 $this->
msg(
'colon-separator' )->escaped() .
665 implode(
' ', $links )
667 $form .=
"\n<br />\n";
669 $namespaceForm = Html::namespaceSelector(
671 'selected' => $opts[
'namespace'],
673 'label' => $this->
msg(
'namespace' )->text(),
674 'in-user-lang' =>
true,
676 'name' =>
'namespace',
678 'class' =>
'namespaceselector',
681 $hidden = $opts[
'namespace'] ===
'' ?
' mw-input-hidden' :
'';
682 $namespaceForm .=
'<span class="mw-input-with-label' . $hidden .
'">' . Xml::checkLabel(
683 $this->
msg(
'invert' )->text(),
687 [
'title' => $this->
msg(
'tooltip-invert' )->text() ]
689 $namespaceForm .=
'<span class="mw-input-with-label' . $hidden .
'">' . Xml::checkLabel(
690 $this->
msg(
'namespace_association' )->text(),
694 [
'title' => $this->
msg(
'tooltip-namespace_association' )->text() ]
696 $form .= Html::rawElement(
698 [
'class' =>
'namespaceForm cloption' ],
702 $form .= Xml::submitButton(
703 $this->
msg(
'watchlist-submit' )->text(),
704 [
'class' =>
'cloption-submit' ]
706 foreach ( $hiddenFields as $key => $value ) {
707 $form .= Html::hidden( $key, $value ) .
"\n";
709 $form .= Xml::closeElement(
'fieldset' ) .
"\n";
710 $form .= Xml::closeElement(
'form' ) .
"\n";
714 $rcfilterContainer = Html::element(
717 [
'class' =>
'rcfilters-container mw-rcfilters-container' ]
720 $loadingContainer = Html::rawElement(
722 [
'class' =>
'mw-rcfilters-spinner' ],
725 [
'class' =>
'mw-rcfilters-spinner-bounce' ]
734 [
'class' =>
'rcfilters-head mw-rcfilters-head' ],
735 $rcfilterContainer . $form
740 $this->
getOutput()->addHTML( $loadingContainer );
749 $selected = (float)$options[
'days'];
750 if ( $selected <= 0 ) {
754 $selectedHours = round( $selected * 24 );
756 $hours = array_unique( array_filter( [
764 24 * (
float)$this->
getUser()->getOption(
'watchlistdays', 0 ),
770 $select =
new XmlSelect(
'days',
'days', (
float)( $selectedHours / 24 ) );
772 foreach ( $hours as $value ) {
774 $name = $this->
msg(
'hours' )->numParams( $value )->text();
776 $name = $this->
msg(
'days' )->numParams( $value / 24 )->text();
778 $select->addOption( $name, (
float)( $value / 24 ) );
781 return $select->getHTML() .
"\n<br />\n";
790 $showUpdatedMarker = $this->
getConfig()->get(
'ShowUpdatedMarker' );
793 $watchlistHeader =
'';
794 if ( $numItems == 0 ) {
795 $watchlistHeader = $this->
msg(
'nowatchlist' )->parse();
797 $watchlistHeader .= $this->
msg(
'watchlist-details' )->numParams( $numItems )->parse() .
"\n";
798 if ( $this->
getConfig()->
get(
'EnotifWatchlist' )
799 && $user->getOption(
'enotifwatchlistpages' )
801 $watchlistHeader .= $this->
msg(
'wlheader-enotif' )->parse() .
"\n";
803 if ( $showUpdatedMarker ) {
804 $watchlistHeader .= $this->
msg(
806 'rcfilters-watchlist-showupdated' :
807 'wlheader-showupdated'
811 $form .= Html::rawElement(
813 [
'class' =>
'watchlistDetails' ],
817 if ( $numItems > 0 && $showUpdatedMarker ) {
818 $form .= Xml::openElement(
'form', [
'method' =>
'post',
820 'id' =>
'mw-watchlist-resetbutton' ] ) .
"\n" .
821 Xml::submitButton( $this->
msg(
'enotif_reset' )->text(),
822 [
'name' =>
'mw-watchlist-reset-submit' ] ) .
"\n" .
823 Html::hidden(
'token', $user->getEditToken() ) .
"\n" .
824 Html::hidden(
'reset',
'all' ) .
"\n";
825 foreach ( $nondefaults as $key => $value ) {
826 $form .= Html::hidden( $key, $value ) .
"\n";
828 $form .= Xml::closeElement(
'form' ) .
"\n";
834 protected function showHideCheck( $options, $message, $name, $value, $inStructuredUi ) {
835 $options[$name] = 1 - (int)$value;
837 $attribs = [
'class' =>
'mw-input-with-label clshowhideoption cloption' ];
838 if ( $inStructuredUi ) {
839 $attribs[
'data-feature-in-structured-ui' ] =
true;
842 return Html::rawElement(
846 Html::check( $name, (
int)$value, [
'id' => $name ] ) . Html::rawElement(
848 $attribs + [
'for' => $name ],
850 $this->
msg( $message,
'<nowiki/>' )->parse()
863 $store = MediaWikiServices::getInstance()->getWatchedItemStore();
864 $count = $store->countWatchedItems( $this->
getUser() );
865 return floor( $count / 2 );
875 return ( $firstUnseen ===
null || $firstUnseen > $rc->
getAttribute(
'rc_timestamp' ) );
883 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)
Throws 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.
setBottomText(FormOptions $opts)
Send the text to be displayed after the options.
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.
static newFromContext(IContextSource $context, array $groups=[])
Fetch an appropriate changes list class for the specified context Some users might want to use an enh...
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.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
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.
getLanguage()
Shortcut to get user's language.
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')
outputFeedLinks()
Output feed links.
$watchStore
WatchedItemStore.
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.
static $daysPreferenceName
fetchOptionsFromRequest( $opts)
Fetch values for a FormOptions object from the WebRequest associated with this instance.
static checkStructuredFilterUiEnabled( $user)
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.
Class for generating HTML <select> or <datalist> elements.
Interface for configuration instances.
if(!isset( $args[0])) $lang