MediaWiki REL1_34
SpecialWatchlist.php
Go to the documentation of this file.
1<?php
27
35 protected static $savedQueriesPreferenceName = 'rcfilters-wl-saved-queries';
36 protected static $daysPreferenceName = 'watchlistdays';
37 protected static $limitPreferenceName = 'wllimit';
38 protected static $collapsedPreferenceName = 'rcfilters-wl-collapsed';
39
41 private $maxDays;
43 private $watchStore;
44
45 public function __construct( $page = 'Watchlist', $restriction = 'viewmywatchlist' ) {
46 parent::__construct( $page, $restriction );
47
48 $this->maxDays = $this->getConfig()->get( 'RCMaxAge' ) / ( 3600 * 24 );
49 $this->watchStore = MediaWikiServices::getInstance()->getWatchedItemStore();
50 }
51
52 public function doesWrites() {
53 return true;
54 }
55
61 function execute( $subpage ) {
62 // Anons don't get a watchlist
63 $this->requireLogin( 'watchlistanontext' );
64
65 $output = $this->getOutput();
66 $request = $this->getRequest();
67 $this->addHelpLink( 'Help:Watching pages' );
68 $output->addModuleStyles( [ 'mediawiki.special' ] );
69 $output->addModules( [
70 'mediawiki.special.recentchanges',
71 'mediawiki.special.watchlist',
72 ] );
73
74 $mode = SpecialEditWatchlist::getMode( $request, $subpage );
75 if ( $mode !== false ) {
76 if ( $mode === SpecialEditWatchlist::EDIT_RAW ) {
77 $title = SpecialPage::getTitleFor( 'EditWatchlist', 'raw' );
78 } elseif ( $mode === SpecialEditWatchlist::EDIT_CLEAR ) {
79 $title = SpecialPage::getTitleFor( 'EditWatchlist', 'clear' );
80 } else {
81 $title = SpecialPage::getTitleFor( 'EditWatchlist' );
82 }
83
84 $output->redirect( $title->getLocalURL() );
85
86 return;
87 }
88
89 $this->checkPermissions();
90
91 $user = $this->getUser();
92 $opts = $this->getOptions();
93
94 $config = $this->getConfig();
95 if ( ( $config->get( 'EnotifWatchlist' ) || $config->get( 'ShowUpdatedMarker' ) )
96 && $request->getVal( 'reset' )
97 && $request->wasPosted()
98 && $user->matchEditToken( $request->getVal( 'token' ) )
99 ) {
100 $user->clearAllNotifications();
101 $output->redirect( $this->getPageTitle()->getFullURL( $opts->getChangedValues() ) );
102
103 return;
104 }
105
106 parent::execute( $subpage );
107
108 if ( $this->isStructuredFilterUiEnabled() ) {
109 $output->addModuleStyles( [ 'mediawiki.rcfilters.highlightCircles.seenunseen.styles' ] );
110 }
111 }
112
116 public static function checkStructuredFilterUiEnabled( $user ) {
117 if ( $user instanceof Config ) {
118 wfDeprecated( __METHOD__ . ' with Config argument', '1.34' );
119 $user = func_get_arg( 1 );
120 }
121 return !$user->getOption( 'wlenhancedfilters-disable' );
122 }
123
130 public function getSubpagesForPrefixSearch() {
131 return [
132 'clear',
133 'edit',
134 'raw',
135 ];
136 }
137
141 protected function transformFilterDefinition( array $filterDefinition ) {
142 if ( isset( $filterDefinition['showHideSuffix'] ) ) {
143 $filterDefinition['showHide'] = 'wl' . $filterDefinition['showHideSuffix'];
144 }
145
146 return $filterDefinition;
147 }
148
153 protected function registerFilters() {
154 parent::registerFilters();
155
156 // legacy 'extended' filter
158 'name' => 'extended-group',
159 'filters' => [
160 [
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(
171 [
172 'rc_this_oldid=page_latest',
173 'rc_type' => $nonRevisionTypes,
174 ],
175 LIST_OR
176 );
177 }
178 },
179 ]
180 ],
181
182 ] ) );
183
184 if ( $this->isStructuredFilterUiEnabled() ) {
185 $this->getFilterGroup( 'lastRevision' )
186 ->getFilter( 'hidepreviousrevisions' )
187 ->setDefault( !$this->getUser()->getBoolOption( 'extendwatchlist' ) );
188 }
189
191 'name' => 'watchlistactivity',
192 'title' => 'rcfilters-filtergroup-watchlistactivity',
193 'class' => ChangesListStringOptionsFilterGroup::class,
194 'priority' => 3,
195 'isFullCoverage' => true,
196 'filters' => [
197 [
198 'name' => 'unseen',
199 'label' => 'rcfilters-filter-watchlistactivity-unseen-label',
200 'description' => 'rcfilters-filter-watchlistactivity-unseen-description',
201 'cssClassSuffix' => 'watchedunseen',
202 'isRowApplicableCallable' => function ( $ctx, RecentChange $rc ) {
203 return !$this->isChangeEffectivelySeen( $rc );
204 },
205 ],
206 [
207 'name' => 'seen',
208 'label' => 'rcfilters-filter-watchlistactivity-seen-label',
209 'description' => 'rcfilters-filter-watchlistactivity-seen-description',
210 'cssClassSuffix' => 'watchedseen',
211 'isRowApplicableCallable' => function ( $ctx, RecentChange $rc ) {
212 return $this->isChangeEffectivelySeen( $rc );
213 }
214 ],
215 ],
217 'queryCallable' => function (
218 $specialPageClassName,
219 $context,
221 &$tables,
222 &$fields,
223 &$conds,
224 &$query_options,
225 &$join_conds,
226 $selectedValues
227 ) {
228 if ( $selectedValues === [ 'seen' ] ) {
229 $conds[] = $dbr->makeList( [
230 'wl_notificationtimestamp IS NULL',
231 'rc_timestamp < wl_notificationtimestamp'
232 ], LIST_OR );
233 } elseif ( $selectedValues === [ 'unseen' ] ) {
234 $conds[] = $dbr->makeList( [
235 'wl_notificationtimestamp IS NOT NULL',
236 'rc_timestamp >= wl_notificationtimestamp'
237 ], LIST_AND );
238 }
239 }
240 ] ) );
241
242 $user = $this->getUser();
243
244 $significance = $this->getFilterGroup( 'significance' );
245 $hideMinor = $significance->getFilter( 'hideminor' );
246 $hideMinor->setDefault( $user->getBoolOption( 'watchlisthideminor' ) );
247
248 $automated = $this->getFilterGroup( 'automated' );
249 $hideBots = $automated->getFilter( 'hidebots' );
250 $hideBots->setDefault( $user->getBoolOption( 'watchlisthidebots' ) );
251
252 $registration = $this->getFilterGroup( 'registration' );
253 $hideAnons = $registration->getFilter( 'hideanons' );
254 $hideAnons->setDefault( $user->getBoolOption( 'watchlisthideanons' ) );
255 $hideLiu = $registration->getFilter( 'hideliu' );
256 $hideLiu->setDefault( $user->getBoolOption( 'watchlisthideliu' ) );
257
258 // Selecting both hideanons and hideliu on watchlist preferances
259 // gives mutually exclusive filters, so those are ignored
260 if ( $user->getBoolOption( 'watchlisthideanons' ) &&
261 !$user->getBoolOption( 'watchlisthideliu' )
262 ) {
263 $this->getFilterGroup( 'userExpLevel' )
264 ->setDefault( 'registered' );
265 }
266
267 if ( $user->getBoolOption( 'watchlisthideliu' ) &&
268 !$user->getBoolOption( 'watchlisthideanons' )
269 ) {
270 $this->getFilterGroup( 'userExpLevel' )
271 ->setDefault( 'unregistered' );
272 }
273
274 $reviewStatus = $this->getFilterGroup( 'reviewStatus' );
275 if ( $reviewStatus !== null ) {
276 // Conditional on feature being available and rights
277 if ( $user->getBoolOption( 'watchlisthidepatrolled' ) ) {
278 $reviewStatus->setDefault( 'unpatrolled' );
279 $legacyReviewStatus = $this->getFilterGroup( 'legacyReviewStatus' );
280 $legacyHidePatrolled = $legacyReviewStatus->getFilter( 'hidepatrolled' );
281 $legacyHidePatrolled->setDefault( true );
282 }
283 }
284
285 $authorship = $this->getFilterGroup( 'authorship' );
286 $hideMyself = $authorship->getFilter( 'hidemyself' );
287 $hideMyself->setDefault( $user->getBoolOption( 'watchlisthideown' ) );
288
289 $changeType = $this->getFilterGroup( 'changeType' );
290 $hideCategorization = $changeType->getFilter( 'hidecategorization' );
291 if ( $hideCategorization !== null ) {
292 // Conditional on feature being available
293 $hideCategorization->setDefault( $user->getBoolOption( 'watchlisthidecategorization' ) );
294 }
295 }
296
306 protected function fetchOptionsFromRequest( $opts ) {
307 static $compatibilityMap = [
308 'hideMinor' => 'hideminor',
309 'hideBots' => 'hidebots',
310 'hideAnons' => 'hideanons',
311 'hideLiu' => 'hideliu',
312 'hidePatrolled' => 'hidepatrolled',
313 'hideOwn' => 'hidemyself',
314 ];
315
316 $params = $this->getRequest()->getValues();
317 foreach ( $compatibilityMap as $from => $to ) {
318 if ( isset( $params[$from] ) ) {
319 $params[$to] = $params[$from];
320 unset( $params[$from] );
321 }
322 }
323
324 if ( $this->getRequest()->getVal( 'action' ) == 'submit' ) {
325 $allBooleansFalse = [];
326
327 // If the user submitted the form, start with a baseline of "all
328 // booleans are false", then change the ones they checked. This
329 // means we ignore the defaults.
330
331 // This is how we handle the fact that HTML forms don't submit
332 // unchecked boxes.
333 foreach ( $this->getLegacyShowHideFilters() as $filter ) {
334 $allBooleansFalse[ $filter->getName() ] = false;
335 }
336
337 $params = $params + $allBooleansFalse;
338 }
339
340 // Not the prettiest way to achieve this… FormOptions internally depends on data sanitization
341 // methods defined on WebRequest and removing this dependency would cause some code duplication.
342 $request = new DerivativeRequest( $this->getRequest(), $params );
343 $opts->fetchValuesFromRequest( $request );
344
345 return $opts;
346 }
347
351 protected function doMainQuery( $tables, $fields, $conds, $query_options,
352 $join_conds, FormOptions $opts
353 ) {
354 $dbr = $this->getDB();
355 $user = $this->getUser();
356
357 $rcQuery = RecentChange::getQueryInfo();
358 $tables = array_merge( $tables, $rcQuery['tables'], [ 'watchlist' ] );
359 $fields = array_merge( $rcQuery['fields'], $fields );
360
361 $join_conds = array_merge(
362 [
363 'watchlist' => [
364 'JOIN',
365 [
366 'wl_user' => $user->getId(),
367 'wl_namespace=rc_namespace',
368 'wl_title=rc_title'
369 ],
370 ],
371 ],
372 $rcQuery['joins'],
373 $join_conds
374 );
375
376 $tables[] = 'page';
377 $fields[] = 'page_latest';
378 $join_conds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
379
380 $fields[] = 'wl_notificationtimestamp';
381
382 // Log entries with DELETED_ACTION must not show up unless the user has
383 // the necessary rights.
384 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
385 if ( !$permissionManager->userHasRight( $user, 'deletedhistory' ) ) {
386 $bitmask = LogPage::DELETED_ACTION;
387 } elseif ( !$permissionManager->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' ) ) {
389 } else {
390 $bitmask = 0;
391 }
392 if ( $bitmask ) {
393 $conds[] = $dbr->makeList( [
394 'rc_type != ' . RC_LOG,
395 $dbr->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask",
396 ], LIST_OR );
397 }
398
399 $tagFilter = $opts['tagfilter'] ? explode( '|', $opts['tagfilter'] ) : [];
401 $tables,
402 $fields,
403 $conds,
404 $join_conds,
405 $query_options,
406 $tagFilter
407 );
408
409 $this->runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds, $opts );
410
411 if ( $this->areFiltersInConflict() ) {
412 return false;
413 }
414
415 $orderByAndLimit = [
416 'ORDER BY' => 'rc_timestamp DESC',
417 'LIMIT' => $opts['limit']
418 ];
419 if ( in_array( 'DISTINCT', $query_options ) ) {
420 // ChangeTags::modifyDisplayQuery() adds DISTINCT when filtering on multiple tags.
421 // In order to prevent DISTINCT from causing query performance problems,
422 // we have to GROUP BY the primary key. This in turn requires us to add
423 // the primary key to the end of the ORDER BY, and the old ORDER BY to the
424 // start of the GROUP BY
425 $orderByAndLimit['ORDER BY'] = 'rc_timestamp DESC, rc_id DESC';
426 $orderByAndLimit['GROUP BY'] = 'rc_timestamp, rc_id';
427 }
428 // array_merge() is used intentionally here so that hooks can, should
429 // they so desire, override the ORDER BY / LIMIT condition(s)
430 $query_options = array_merge( $orderByAndLimit, $query_options );
431
432 return $dbr->select(
433 $tables,
434 $fields,
435 $conds,
436 __METHOD__,
437 $query_options,
438 $join_conds
439 );
440 }
441
447 protected function getDB() {
448 return wfGetDB( DB_REPLICA, 'watchlist' );
449 }
450
454 public function outputFeedLinks() {
455 $user = $this->getUser();
456 $wlToken = $user->getTokenFromOption( 'watchlisttoken' );
457 if ( $wlToken ) {
458 $this->addFeedLinks( [
459 'action' => 'feedwatchlist',
460 'allrev' => 1,
461 'wlowner' => $user->getName(),
462 'wltoken' => $wlToken,
463 ] );
464 }
465 }
466
473 public function outputChangesList( $rows, $opts ) {
474 $dbr = $this->getDB();
475 $user = $this->getUser();
476 $output = $this->getOutput();
477 $services = MediaWikiServices::getInstance();
478
479 # Show a message about replica DB lag, if applicable
480 $lag = $dbr->getSessionLagStatus()['lag'];
481 if ( $lag > 0 ) {
482 $output->showLagWarning( $lag );
483 }
484
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'
489 );
490 return;
491 }
492
493 $dbr->dataSeek( $rows, 0 );
494
495 $list = ChangesList::newFromContext( $this->getContext(), $this->filterGroups );
496 $list->setWatchlistDivs();
497 $list->initChangesListRows( $rows );
498 if ( $user->getOption( 'watchlistunwatchlinks' ) ) {
499 $list->setChangeLinePrefixer( function ( RecentChange $rc, ChangesList $cl, $grouped ) {
500 // Don't show unwatch link if the line is a grouped log entry using EnhancedChangesList,
501 // since EnhancedChangesList groups log entries by performer rather than by target article
502 if ( $rc->mAttribs['rc_type'] == RC_LOG && $cl instanceof EnhancedChangesList &&
503 $grouped ) {
504 return '';
505 } else {
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}";
512 }
513 } );
514 }
515 $dbr->dataSeek( $rows, 0 );
516
517 if ( $this->getConfig()->get( 'RCShowWatchingUsers' )
518 && $user->getOption( 'shownumberswatching' )
519 ) {
520 $watchedItemStore = $services->getWatchedItemStore();
521 }
522
523 $s = $list->beginRecentChangesList();
524
525 if ( $this->isStructuredFilterUiEnabled() ) {
526 $s .= $this->makeLegend();
527 }
528
529 $userShowHiddenCats = $this->getUser()->getBoolOption( 'showhiddencats' );
530 $counter = 1;
531 foreach ( $rows as $obj ) {
532 # Make RC entry
533 $rc = RecentChange::newFromRow( $obj );
534
535 # Skip CatWatch entries for hidden cats based on user preference
536 if (
537 $rc->getAttribute( 'rc_type' ) == RC_CATEGORIZE &&
538 !$userShowHiddenCats &&
539 $rc->getParam( 'hidden-cat' )
540 ) {
541 continue;
542 }
543
544 $rc->counter = $counter++;
545
546 if ( $this->getConfig()->get( 'ShowUpdatedMarker' ) ) {
547 $unseen = !$this->isChangeEffectivelySeen( $rc );
548 } else {
549 $unseen = false;
550 }
551
552 if ( isset( $watchedItemStore ) ) {
553 $rcTitleValue = new TitleValue( (int)$obj->rc_namespace, $obj->rc_title );
554 $rc->numberofWatchingusers = $watchedItemStore->countWatchers( $rcTitleValue );
555 } else {
556 $rc->numberofWatchingusers = 0;
557 }
558
559 // XXX: this treats pages with no unseen changes as "not on the watchlist" since
560 // everything is on the watchlist and it is an easy way to make pages with unseen
561 // changes appear bold. @TODO: clean this up.
562 $changeLine = $list->recentChangesLine( $rc, $unseen, $counter );
563 if ( $changeLine !== false ) {
564 $s .= $changeLine;
565 }
566 }
567 $s .= $list->endRecentChangesList();
568
569 $output->addHTML( $s );
570 }
571
578 public function doHeader( $opts, $numRows ) {
579 $user = $this->getUser();
580 $out = $this->getOutput();
581
582 $out->addSubtitle(
583 $this->msg( 'watchlistfor2', $user->getName() )
585 $this->getLanguage(),
586 $this->getLinkRenderer()
587 ) )
588 );
589
590 $this->setTopText( $opts );
591
592 $form = '';
593
594 $form .= Xml::openElement( 'form', [
595 'method' => 'get',
596 'action' => wfScript(),
597 'id' => 'mw-watchlist-form'
598 ] );
599 $form .= Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() );
600 $form .= Xml::openElement(
601 'fieldset',
602 [ 'id' => 'mw-watchlist-options', 'class' => 'cloptions' ]
603 );
604 $form .= Xml::element(
605 'legend', null, $this->msg( 'watchlist-options' )->text()
606 );
607
608 if ( !$this->isStructuredFilterUiEnabled() ) {
609 $form .= $this->makeLegend();
610 }
611
612 $lang = $this->getLanguage();
613 $timestamp = wfTimestampNow();
614 $now = $lang->userTimeAndDate( $timestamp, $user );
615 $wlInfo = Html::rawElement(
616 'span',
617 [
618 'class' => 'wlinfo',
619 'data-params' => json_encode( [ 'from' => $timestamp, 'fromFormatted' => $now ] ),
620 ],
621 $this->msg( 'wlnote' )->numParams( $numRows, round( $opts['days'] * 24 ) )->params(
622 $lang->userDate( $timestamp, $user ), $lang->userTime( $timestamp, $user )
623 )->parse()
624 ) . "<br />\n";
625
626 $nondefaults = $opts->getChangedValues();
627 $cutofflinks = Html::rawElement(
628 'span',
629 [ 'class' => 'cldays cloption' ],
630 $this->msg( 'wlshowtime' ) . ' ' . $this->cutoffselector( $opts )
631 );
632
633 # Spit out some control panel links
634 $links = [];
635 $namesOfDisplayedFilters = [];
636 foreach ( $this->getLegacyShowHideFilters() as $filterName => $filter ) {
637 $namesOfDisplayedFilters[] = $filterName;
638 $links[] = $this->showHideCheck(
639 $nondefaults,
640 $filter->getShowHide(),
641 $filterName,
642 $opts[ $filterName ],
643 $filter->isFeatureAvailableOnStructuredUi( $this )
644 );
645 }
646
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] );
655 }
656
657 # Namespace filter and put the whole form together.
658 $form .= $wlInfo;
659 $form .= $cutofflinks;
660 $form .= Html::rawElement(
661 'span',
662 [ 'class' => 'clshowhide' ],
663 $this->msg( 'watchlist-hide' ) .
664 $this->msg( 'colon-separator' )->escaped() .
665 implode( ' ', $links )
666 );
667 $form .= "\n<br />\n";
668
669 $namespaceForm = Html::namespaceSelector(
670 [
671 'selected' => $opts['namespace'],
672 'all' => '',
673 'label' => $this->msg( 'namespace' )->text(),
674 'in-user-lang' => true,
675 ], [
676 'name' => 'namespace',
677 'id' => 'namespace',
678 'class' => 'namespaceselector',
679 ]
680 ) . "\n";
681 $hidden = $opts['namespace'] === '' ? ' mw-input-hidden' : '';
682 $namespaceForm .= '<span class="mw-input-with-label' . $hidden . '">' . Xml::checkLabel(
683 $this->msg( 'invert' )->text(),
684 'invert',
685 'nsinvert',
686 $opts['invert'],
687 [ 'title' => $this->msg( 'tooltip-invert' )->text() ]
688 ) . "</span>\n";
689 $namespaceForm .= '<span class="mw-input-with-label' . $hidden . '">' . Xml::checkLabel(
690 $this->msg( 'namespace_association' )->text(),
691 'associated',
692 'nsassociated',
693 $opts['associated'],
694 [ 'title' => $this->msg( 'tooltip-namespace_association' )->text() ]
695 ) . "</span>\n";
696 $form .= Html::rawElement(
697 'span',
698 [ 'class' => 'namespaceForm cloption' ],
699 $namespaceForm
700 );
701
702 $form .= Xml::submitButton(
703 $this->msg( 'watchlist-submit' )->text(),
704 [ 'class' => 'cloption-submit' ]
705 ) . "\n";
706 foreach ( $hiddenFields as $key => $value ) {
707 $form .= Html::hidden( $key, $value ) . "\n";
708 }
709 $form .= Xml::closeElement( 'fieldset' ) . "\n";
710 $form .= Xml::closeElement( 'form' ) . "\n";
711
712 // Insert a placeholder for RCFilters
713 if ( $this->isStructuredFilterUiEnabled() ) {
714 $rcfilterContainer = Html::element(
715 'div',
716 // TODO: Remove deprecated rcfilters-container class
717 [ 'class' => 'rcfilters-container mw-rcfilters-container' ]
718 );
719
720 $loadingContainer = Html::rawElement(
721 'div',
722 [ 'class' => 'mw-rcfilters-spinner' ],
723 Html::element(
724 'div',
725 [ 'class' => 'mw-rcfilters-spinner-bounce' ]
726 )
727 );
728
729 // Wrap both with rcfilters-head
730 $this->getOutput()->addHTML(
731 Html::rawElement(
732 'div',
733 // TODO: Remove deprecated rcfilters-head class
734 [ 'class' => 'rcfilters-head mw-rcfilters-head' ],
735 $rcfilterContainer . $form
736 )
737 );
738
739 // Add spinner
740 $this->getOutput()->addHTML( $loadingContainer );
741 } else {
742 $this->getOutput()->addHTML( $form );
743 }
744
745 $this->setBottomText( $opts );
746 }
747
748 function cutoffselector( $options ) {
749 $selected = (float)$options['days'];
750 if ( $selected <= 0 ) {
751 $selected = $this->maxDays;
752 }
753
754 $selectedHours = round( $selected * 24 );
755
756 $hours = array_unique( array_filter( [
757 1,
758 2,
759 6,
760 12,
761 24,
762 72,
763 168,
764 24 * (float)$this->getUser()->getOption( 'watchlistdays', 0 ),
765 24 * $this->maxDays,
766 $selectedHours
767 ] ) );
768 asort( $hours );
769
770 $select = new XmlSelect( 'days', 'days', (float)( $selectedHours / 24 ) );
771
772 foreach ( $hours as $value ) {
773 if ( $value < 24 ) {
774 $name = $this->msg( 'hours' )->numParams( $value )->text();
775 } else {
776 $name = $this->msg( 'days' )->numParams( $value / 24 )->text();
777 }
778 $select->addOption( $name, (float)( $value / 24 ) );
779 }
780
781 return $select->getHTML() . "\n<br />\n";
782 }
783
784 function setTopText( FormOptions $opts ) {
785 $nondefaults = $opts->getChangedValues();
786 $form = '';
787 $user = $this->getUser();
788
789 $numItems = $this->countItems();
790 $showUpdatedMarker = $this->getConfig()->get( 'ShowUpdatedMarker' );
791
792 // Show watchlist header
793 $watchlistHeader = '';
794 if ( $numItems == 0 ) {
795 $watchlistHeader = $this->msg( 'nowatchlist' )->parse();
796 } else {
797 $watchlistHeader .= $this->msg( 'watchlist-details' )->numParams( $numItems )->parse() . "\n";
798 if ( $this->getConfig()->get( 'EnotifWatchlist' )
799 && $user->getOption( 'enotifwatchlistpages' )
800 ) {
801 $watchlistHeader .= $this->msg( 'wlheader-enotif' )->parse() . "\n";
802 }
803 if ( $showUpdatedMarker ) {
804 $watchlistHeader .= $this->msg(
806 'rcfilters-watchlist-showupdated' :
807 'wlheader-showupdated'
808 )->parse() . "\n";
809 }
810 }
811 $form .= Html::rawElement(
812 'div',
813 [ 'class' => 'watchlistDetails' ],
814 $watchlistHeader
815 );
816
817 if ( $numItems > 0 && $showUpdatedMarker ) {
818 $form .= Xml::openElement( 'form', [ 'method' => 'post',
819 'action' => $this->getPageTitle()->getLocalURL(),
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";
827 }
828 $form .= Xml::closeElement( 'form' ) . "\n";
829 }
830
831 $this->getOutput()->addHTML( $form );
832 }
833
834 protected function showHideCheck( $options, $message, $name, $value, $inStructuredUi ) {
835 $options[$name] = 1 - (int)$value;
836
837 $attribs = [ 'class' => 'mw-input-with-label clshowhideoption cloption' ];
838 if ( $inStructuredUi ) {
839 $attribs[ 'data-feature-in-structured-ui' ] = true;
840 }
841
842 return Html::rawElement(
843 'span',
844 $attribs,
845 // not using Html::checkLabel because that would escape the contents
846 Html::check( $name, (int)$value, [ 'id' => $name ] ) . Html::rawElement(
847 'label',
848 $attribs + [ 'for' => $name ],
849 // <nowiki/> at beginning to avoid messages with "$1 ..." being parsed as pre tags
850 $this->msg( $message, '<nowiki/>' )->parse()
851 )
852 );
853 }
854
862 protected function countItems() {
863 $store = MediaWikiServices::getInstance()->getWatchedItemStore();
864 $count = $store->countWatchedItems( $this->getUser() );
865 return floor( $count / 2 );
866 }
867
872 protected function isChangeEffectivelySeen( RecentChange $rc ) {
873 $firstUnseen = $this->getLatestNotificationTimestamp( $rc );
874
875 return ( $firstUnseen === null || $firstUnseen > $rc->getAttribute( 'rc_timestamp' ) );
876 }
877
883 return $this->watchStore->getLatestNotificationTimestamp(
884 $rc->getAttribute( 'wl_notificationtimestamp' ),
885 $this->getUser(),
886 $rc->getTitle()
887 );
888 }
889}
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.
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='')
Applies all tags-related changes to a query.
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.
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...
Helper class to keep track of options when mixing links and form elements.
getChangedValues()
Return options modified as an array ( name => value )
const DELETED_RESTRICTED
Definition LogPage.php:37
const DELETED_ACTION
Definition LogPage.php:34
MediaWikiServices is the service locator for the application scope of MediaWiki.
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.
showHideCheck( $options, $message, $name, $value, $inStructuredUi)
getSubpagesForPrefixSearch()
Return an array of subpages that this special page will accept.
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.
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)
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.
Definition XmlSelect.php:26
const LIST_OR
Definition Defines.php:51
const RC_LOG
Definition Defines.php:133
const LIST_AND
Definition Defines.php:48
const RC_CATEGORIZE
Definition Defines.php:135
Interface for configuration instances.
Definition Config.php:28
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
Result wrapper for grabbing data queried from an IDatabase object.
$context
Definition load.php:45
$filter
const DB_REPLICA
Definition defines.php:25
if(!isset( $args[0])) $lang