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