Go to the documentation of this file.
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,
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 ) {
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()
597 'id' =>
'mw-watchlist-form'
602 [
'id' =>
'mw-watchlist-options',
'class' =>
'cloptions' ]
605 'legend',
null, $this->
msg(
'watchlist-options' )->text()
614 $now =
$lang->userTimeAndDate( $timestamp, $user );
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();
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;
662 [
'class' =>
'clshowhide' ],
663 $this->
msg(
'watchlist-hide' ) .
664 $this->
msg(
'colon-separator' )->escaped() .
665 implode(
' ', $links )
667 $form .=
"\n<br />\n";
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() ]
698 [
'class' =>
'namespaceForm cloption' ],
703 $this->
msg(
'watchlist-submit' )->text(),
704 [
'class' =>
'cloption-submit' ]
706 foreach ( $hiddenFields as $key => $value ) {
717 [
'class' =>
'rcfilters-container mw-rcfilters-container' ]
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'
813 [
'class' =>
'watchlistDetails' ],
817 if ( $numItems > 0 && $showUpdatedMarker ) {
820 'id' =>
'mw-watchlist-resetbutton' ] ) .
"\n" .
822 [
'name' =>
'mw-watchlist-reset-submit' ] ) .
"\n" .
825 foreach ( $nondefaults as $key => $value ) {
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;
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(
Similar to FauxRequest, but only fakes URL parameters and method (POST or GET) and use the base reque...
getPageTitle( $subpage=false)
Get a self-referential title object.
const EDIT_CLEAR
Editing modes.
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new recentchanges object.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
static $savedQueriesPreferenceName
getOutput()
Get the OutputPage being used for this instance.
static checkStructuredFilterUiEnabled( $user)
if(!isset( $args[0])) $lang
makeLegend()
Return the legend displayed within the fieldset.
Utility class for creating new RC entries.
getSubpagesForPrefixSearch()
Return an array of subpages that this special page will accept.
getDB()
Return a IDatabase object for reading.
static check( $name, $checked=false, array $attribs=[])
Convenience function to produce a checkbox (input element with type=checkbox)
Special page which uses a ChangesList to show query results.
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,...
__construct( $page='Watchlist', $restriction='viewmywatchlist')
getLanguage()
Shortcut to get user's language.
doHeader( $opts, $numRows)
Set the text to be displayed above the changes.
static openElement( $element, $attribs=null)
This opens an XML element.
doMainQuery( $tables, $fields, $conds, $query_options, $join_conds, FormOptions $opts)
Process the query.Array of tables; see IDatabase::select $table Array of fields; see IDatabase::selec...
Class for generating HTML <select> or <datalist> elements.
static $collapsedPreferenceName
Interface for configuration instances.
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
transformFilterDefinition(array $filterDefinition)
Transforms filter definition to prepare it for constructor.See overrides of this method as well....
getConfig()
Shortcut to get main config object.
addFeedLinks( $params)
Adds RSS/atom links.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
runMainQueryHook(&$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts)
isStructuredFilterUiEnabled()
Check whether the structured filter UI is enabled.
countItems()
Count the number of paired items on a user's watchlist.
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
cutoffselector( $options)
getUser()
Shortcut to get the User executing this instance.
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
showHideCheck( $options, $message, $name, $value, $inStructuredUi)
registerFilters()
Register all filters and their groups (including those from hooks), plus handle conflicts and default...
getContext()
Gets the context this SpecialPage is executed in.
requireLogin( $reasonMsg='exception-nologin-text', $titleMsg='exception-nologin')
If the user is not logged in, throws UserNotLoggedIn error.
A special page that lists last changes made to the wiki, limited to user-defined list of titles.
static newFromContext(IContextSource $context, array $groups=[])
Fetch an appropriate changes list class for the specified context Some users might want to use an enh...
static hidden( $name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
execute( $subpage)
Main execution point.
static getMode( $request, $par)
Determine whether we are editing the watchlist, and if so, what kind of editing operation.
static namespaceSelector(array $params=[], array $selectAttribs=[])
Build a drop-down box for selecting a namespace.
getFilterGroup( $groupName)
Gets a specified ChangesListFilterGroup by name.
getRequest()
Get the WebRequest being used for this instance.
const NONE
Signifies that no options in the group are selected, meaning the group has no effect.
outputFeedLinks()
Output feed links.
getAttribute( $name)
Get an attribute value.
areFiltersInConflict()
Check if filters are in conflict and guaranteed to return no results.
static buildTools( $lang, LinkRenderer $linkRenderer=null)
Build a set of links for convenient navigation between watchlist viewing and editing modes.
doesWrites()
Indicates whether this special page may perform database writes.
If the group is active, any unchecked filters will translate to hide parameters in the URL.
static closeElement( $element)
Shortcut to close an XML element.
outputChangesList( $rows, $opts)
Build and output the actual changes list.
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
static $limitPreferenceName
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
setTopText(FormOptions $opts)
Send the text to be displayed before the options.
fetchOptionsFromRequest( $opts)
Fetch values for a FormOptions object from the WebRequest associated with this instance.
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
setBottomText(FormOptions $opts)
Send the text to be displayed after the options.
registerFilterGroup(ChangesListFilterGroup $group)
Register a structured changes list filter group.
Represents a filter group with multiple string options.
getOptions()
Get the current FormOptions for this request.
static $daysPreferenceName
$watchStore
WatchedItemStore.
getLatestNotificationTimestamp(RecentChange $rc)
getLegacyShowHideFilters()
static submitButton( $value, $attribs=[])
Convenience function to build an HTML submit button When $wgUseMediaWikiUIEverywhere is true it will ...
isChangeEffectivelySeen(RecentChange $rc)
static checkLabel( $label, $name, $id, $checked=false, $attribs=[])
Convenience function to build an HTML checkbox with a label.
Represents a page (or page fragment) title within MediaWiki.