MediaWiki  REL1_31
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 
39  private $maxDays;
40 
41  public function __construct( $page = 'Watchlist', $restriction = 'viewmywatchlist' ) {
42  parent::__construct( $page, $restriction );
43 
44  $this->maxDays = $this->getConfig()->get( 'RCMaxAge' ) / ( 3600 * 24 );
45  }
46 
47  public function doesWrites() {
48  return true;
49  }
50 
56  function execute( $subpage ) {
57  // Anons don't get a watchlist
58  $this->requireLogin( 'watchlistanontext' );
59 
60  $output = $this->getOutput();
61  $request = $this->getRequest();
62  $this->addHelpLink( 'Help:Watching pages' );
63  $output->addModules( [
64  'mediawiki.special.changeslist.visitedstatus',
65  'mediawiki.special.watchlist',
66  ] );
67  $output->addModuleStyles( [ 'mediawiki.special.watchlist.styles' ] );
68 
69  $mode = SpecialEditWatchlist::getMode( $request, $subpage );
70  if ( $mode !== false ) {
71  if ( $mode === SpecialEditWatchlist::EDIT_RAW ) {
72  $title = SpecialPage::getTitleFor( 'EditWatchlist', 'raw' );
73  } elseif ( $mode === SpecialEditWatchlist::EDIT_CLEAR ) {
74  $title = SpecialPage::getTitleFor( 'EditWatchlist', 'clear' );
75  } else {
76  $title = SpecialPage::getTitleFor( 'EditWatchlist' );
77  }
78 
79  $output->redirect( $title->getLocalURL() );
80 
81  return;
82  }
83 
84  $this->checkPermissions();
85 
86  $user = $this->getUser();
87  $opts = $this->getOptions();
88 
89  $config = $this->getConfig();
90  if ( ( $config->get( 'EnotifWatchlist' ) || $config->get( 'ShowUpdatedMarker' ) )
91  && $request->getVal( 'reset' )
92  && $request->wasPosted()
93  && $user->matchEditToken( $request->getVal( 'token' ) )
94  ) {
95  $user->clearAllNotifications();
96  $output->redirect( $this->getPageTitle()->getFullURL( $opts->getChangedValues() ) );
97 
98  return;
99  }
100 
101  parent::execute( $subpage );
102 
103  if ( $this->isStructuredFilterUiEnabled() ) {
104  $output->addModuleStyles( [ 'mediawiki.rcfilters.highlightCircles.seenunseen.styles' ] );
105 
106  $output->addJsConfigVars(
107  'wgStructuredChangeFiltersEditWatchlistUrl',
108  SpecialPage::getTitleFor( 'EditWatchlist' )->getLocalURL()
109  );
110  }
111  }
112 
113  public static function checkStructuredFilterUiEnabled( Config $config, User $user ) {
114  return (
115  $config->get( 'StructuredChangeFiltersOnWatchlist' ) &&
116  $user->getOption( 'rcenhancedfilters' )
117  );
118  }
119 
126  public function getSubpagesForPrefixSearch() {
127  return [
128  'clear',
129  'edit',
130  'raw',
131  ];
132  }
133 
137  protected function transformFilterDefinition( array $filterDefinition ) {
138  if ( isset( $filterDefinition['showHideSuffix'] ) ) {
139  $filterDefinition['showHide'] = 'wl' . $filterDefinition['showHideSuffix'];
140  }
141 
142  return $filterDefinition;
143  }
144 
148  protected function registerFilters() {
149  parent::registerFilters();
150 
151  // legacy 'extended' filter
153  'name' => 'extended-group',
154  'filters' => [
155  [
156  'name' => 'extended',
157  'isReplacedInStructuredUi' => true,
158  'activeValue' => false,
159  'default' => $this->getUser()->getBoolOption( 'extendwatchlist' ),
160  'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables,
161  &$fields, &$conds, &$query_options, &$join_conds ) {
162  $nonRevisionTypes = [ RC_LOG ];
163  Hooks::run( 'SpecialWatchlistGetNonRevisionTypes', [ &$nonRevisionTypes ] );
164  if ( $nonRevisionTypes ) {
165  $conds[] = $dbr->makeList(
166  [
167  'rc_this_oldid=page_latest',
168  'rc_type' => $nonRevisionTypes,
169  ],
170  LIST_OR
171  );
172  }
173  },
174  ]
175  ],
176 
177  ] ) );
178 
179  if ( $this->isStructuredFilterUiEnabled() ) {
180  $this->getFilterGroup( 'lastRevision' )
181  ->getFilter( 'hidepreviousrevisions' )
182  ->setDefault( !$this->getUser()->getBoolOption( 'extendwatchlist' ) );
183  }
184 
186  'name' => 'watchlistactivity',
187  'title' => 'rcfilters-filtergroup-watchlistactivity',
189  'priority' => 3,
190  'isFullCoverage' => true,
191  'filters' => [
192  [
193  'name' => 'unseen',
194  'label' => 'rcfilters-filter-watchlistactivity-unseen-label',
195  'description' => 'rcfilters-filter-watchlistactivity-unseen-description',
196  'cssClassSuffix' => 'watchedunseen',
197  'isRowApplicableCallable' => function ( $ctx, $rc ) {
198  $changeTs = $rc->getAttribute( 'rc_timestamp' );
199  $lastVisitTs = $rc->getAttribute( 'wl_notificationtimestamp' );
200  return $lastVisitTs !== null && $changeTs >= $lastVisitTs;
201  },
202  ],
203  [
204  'name' => 'seen',
205  'label' => 'rcfilters-filter-watchlistactivity-seen-label',
206  'description' => 'rcfilters-filter-watchlistactivity-seen-description',
207  'cssClassSuffix' => 'watchedseen',
208  'isRowApplicableCallable' => function ( $ctx, $rc ) {
209  $changeTs = $rc->getAttribute( 'rc_timestamp' );
210  $lastVisitTs = $rc->getAttribute( 'wl_notificationtimestamp' );
211  return $lastVisitTs === null || $changeTs < $lastVisitTs;
212  }
213  ],
214  ],
216  'queryCallable' => function ( $specialPageClassName, $context, $dbr,
217  &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedValues ) {
218  if ( $selectedValues === [ 'seen' ] ) {
219  $conds[] = $dbr->makeList( [
220  'wl_notificationtimestamp IS NULL',
221  'rc_timestamp < wl_notificationtimestamp'
222  ], LIST_OR );
223  } elseif ( $selectedValues === [ 'unseen' ] ) {
224  $conds[] = $dbr->makeList( [
225  'wl_notificationtimestamp IS NOT NULL',
226  'rc_timestamp >= wl_notificationtimestamp'
227  ], LIST_AND );
228  }
229  }
230  ] ) );
231 
232  $user = $this->getUser();
233 
234  $significance = $this->getFilterGroup( 'significance' );
235  $hideMinor = $significance->getFilter( 'hideminor' );
236  $hideMinor->setDefault( $user->getBoolOption( 'watchlisthideminor' ) );
237 
238  $automated = $this->getFilterGroup( 'automated' );
239  $hideBots = $automated->getFilter( 'hidebots' );
240  $hideBots->setDefault( $user->getBoolOption( 'watchlisthidebots' ) );
241 
242  $registration = $this->getFilterGroup( 'registration' );
243  $hideAnons = $registration->getFilter( 'hideanons' );
244  $hideAnons->setDefault( $user->getBoolOption( 'watchlisthideanons' ) );
245  $hideLiu = $registration->getFilter( 'hideliu' );
246  $hideLiu->setDefault( $user->getBoolOption( 'watchlisthideliu' ) );
247 
248  // Selecting both hideanons and hideliu on watchlist preferances
249  // gives mutually exclusive filters, so those are ignored
250  if ( $user->getBoolOption( 'watchlisthideanons' ) &&
251  !$user->getBoolOption( 'watchlisthideliu' )
252  ) {
253  $this->getFilterGroup( 'userExpLevel' )
254  ->setDefault( 'registered' );
255  }
256 
257  if ( $user->getBoolOption( 'watchlisthideliu' ) &&
258  !$user->getBoolOption( 'watchlisthideanons' )
259  ) {
260  $this->getFilterGroup( 'userExpLevel' )
261  ->setDefault( 'unregistered' );
262  }
263 
264  $reviewStatus = $this->getFilterGroup( 'reviewStatus' );
265  if ( $reviewStatus !== null ) {
266  // Conditional on feature being available and rights
267  if ( $user->getBoolOption( 'watchlisthidepatrolled' ) ) {
268  $reviewStatus->setDefault( 'unpatrolled' );
269  $legacyReviewStatus = $this->getFilterGroup( 'legacyReviewStatus' );
270  $legacyHidePatrolled = $legacyReviewStatus->getFilter( 'hidepatrolled' );
271  $legacyHidePatrolled->setDefault( true );
272  }
273  }
274 
275  $authorship = $this->getFilterGroup( 'authorship' );
276  $hideMyself = $authorship->getFilter( 'hidemyself' );
277  $hideMyself->setDefault( $user->getBoolOption( 'watchlisthideown' ) );
278 
279  $changeType = $this->getFilterGroup( 'changeType' );
280  $hideCategorization = $changeType->getFilter( 'hidecategorization' );
281  if ( $hideCategorization !== null ) {
282  // Conditional on feature being available
283  $hideCategorization->setDefault( $user->getBoolOption( 'watchlisthidecategorization' ) );
284  }
285  }
286 
292  protected function getCustomFilters() {
293  if ( $this->customFilters === null ) {
294  $this->customFilters = parent::getCustomFilters();
295  Hooks::run( 'SpecialWatchlistFilters', [ $this, &$this->customFilters ], '1.23' );
296  }
297 
298  return $this->customFilters;
299  }
300 
310  protected function fetchOptionsFromRequest( $opts ) {
311  static $compatibilityMap = [
312  'hideMinor' => 'hideminor',
313  'hideBots' => 'hidebots',
314  'hideAnons' => 'hideanons',
315  'hideLiu' => 'hideliu',
316  'hidePatrolled' => 'hidepatrolled',
317  'hideOwn' => 'hidemyself',
318  ];
319 
320  $params = $this->getRequest()->getValues();
321  foreach ( $compatibilityMap as $from => $to ) {
322  if ( isset( $params[$from] ) ) {
323  $params[$to] = $params[$from];
324  unset( $params[$from] );
325  }
326  }
327 
328  if ( $this->getRequest()->getVal( 'action' ) == 'submit' ) {
329  $allBooleansFalse = [];
330 
331  // If the user submitted the form, start with a baseline of "all
332  // booleans are false", then change the ones they checked. This
333  // means we ignore the defaults.
334 
335  // This is how we handle the fact that HTML forms don't submit
336  // unchecked boxes.
337  foreach ( $this->getLegacyShowHideFilters() as $filter ) {
338  $allBooleansFalse[ $filter->getName() ] = false;
339  }
340 
341  $params = $params + $allBooleansFalse;
342  }
343 
344  // Not the prettiest way to achieve this… FormOptions internally depends on data sanitization
345  // methods defined on WebRequest and removing this dependency would cause some code duplication.
346  $request = new DerivativeRequest( $this->getRequest(), $params );
347  $opts->fetchValuesFromRequest( $request );
348 
349  return $opts;
350  }
351 
355  protected function doMainQuery( $tables, $fields, $conds, $query_options,
356  $join_conds, FormOptions $opts
357  ) {
358  $dbr = $this->getDB();
359  $user = $this->getUser();
360 
361  $rcQuery = RecentChange::getQueryInfo();
362  $tables = array_merge( $tables, $rcQuery['tables'], [ 'watchlist' ] );
363  $fields = array_merge( $rcQuery['fields'], $fields );
364 
365  $join_conds = array_merge(
366  [
367  'watchlist' => [
368  'INNER JOIN',
369  [
370  'wl_user' => $user->getId(),
371  'wl_namespace=rc_namespace',
372  'wl_title=rc_title'
373  ],
374  ],
375  ],
376  $rcQuery['joins'],
377  $join_conds
378  );
379 
380  $tables[] = 'page';
381  $fields[] = 'page_latest';
382  $join_conds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
383 
384  $fields[] = 'wl_notificationtimestamp';
385 
386  // Log entries with DELETED_ACTION must not show up unless the user has
387  // the necessary rights.
388  if ( !$user->isAllowed( 'deletedhistory' ) ) {
389  $bitmask = LogPage::DELETED_ACTION;
390  } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
392  } else {
393  $bitmask = 0;
394  }
395  if ( $bitmask ) {
396  $conds[] = $dbr->makeList( [
397  'rc_type != ' . RC_LOG,
398  $dbr->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask",
399  ], LIST_OR );
400  }
401 
402  $tagFilter = $opts['tagfilter'] ? explode( '|', $opts['tagfilter'] ) : [];
404  $tables,
405  $fields,
406  $conds,
407  $join_conds,
408  $query_options,
409  $tagFilter
410  );
411 
412  $this->runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds, $opts );
413 
414  if ( $this->areFiltersInConflict() ) {
415  return false;
416  }
417 
418  $orderByAndLimit = [
419  'ORDER BY' => 'rc_timestamp DESC',
420  'LIMIT' => $opts['limit']
421  ];
422  if ( in_array( 'DISTINCT', $query_options ) ) {
423  // ChangeTags::modifyDisplayQuery() adds DISTINCT when filtering on multiple tags.
424  // In order to prevent DISTINCT from causing query performance problems,
425  // we have to GROUP BY the primary key. This in turn requires us to add
426  // the primary key to the end of the ORDER BY, and the old ORDER BY to the
427  // start of the GROUP BY
428  $orderByAndLimit['ORDER BY'] = 'rc_timestamp DESC, rc_id DESC';
429  $orderByAndLimit['GROUP BY'] = 'rc_timestamp, rc_id';
430  }
431  // array_merge() is used intentionally here so that hooks can, should
432  // they so desire, override the ORDER BY / LIMIT condition(s)
433  $query_options = array_merge( $orderByAndLimit, $query_options );
434 
435  return $dbr->select(
436  $tables,
437  $fields,
438  $conds,
439  __METHOD__,
440  $query_options,
441  $join_conds
442  );
443  }
444 
445  protected function runMainQueryHook( &$tables, &$fields, &$conds, &$query_options,
446  &$join_conds, $opts
447  ) {
448  return parent::runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds, $opts )
449  && Hooks::run(
450  'SpecialWatchlistQuery',
451  [ &$conds, &$tables, &$join_conds, &$fields, $opts ],
452  '1.23'
453  );
454  }
455 
461  protected function getDB() {
462  return wfGetDB( DB_REPLICA, 'watchlist' );
463  }
464 
468  public function outputFeedLinks() {
469  $user = $this->getUser();
470  $wlToken = $user->getTokenFromOption( 'watchlisttoken' );
471  if ( $wlToken ) {
472  $this->addFeedLinks( [
473  'action' => 'feedwatchlist',
474  'allrev' => 1,
475  'wlowner' => $user->getName(),
476  'wltoken' => $wlToken,
477  ] );
478  }
479  }
480 
487  public function outputChangesList( $rows, $opts ) {
488  $dbr = $this->getDB();
489  $user = $this->getUser();
490  $output = $this->getOutput();
491 
492  # Show a message about replica DB lag, if applicable
493  $lag = MediaWikiServices::getInstance()->getDBLoadBalancer()->safeGetLag( $dbr );
494  if ( $lag > 0 ) {
495  $output->showLagWarning( $lag );
496  }
497 
498  # If no rows to display, show message before try to render the list
499  if ( $rows->numRows() == 0 ) {
500  $output->wrapWikiMsg(
501  "<div class='mw-changeslist-empty'>\n$1\n</div>", 'recentchanges-noresult'
502  );
503  return;
504  }
505 
506  $dbr->dataSeek( $rows, 0 );
507 
508  $list = ChangesList::newFromContext( $this->getContext(), $this->filterGroups );
509  $list->setWatchlistDivs();
510  $list->initChangesListRows( $rows );
511  if ( $user->getOption( 'watchlistunwatchlinks' ) ) {
512  $list->setChangeLinePrefixer( function ( RecentChange $rc, ChangesList $cl, $grouped ) {
513  // Don't show unwatch link if the line is a grouped log entry using EnhancedChangesList,
514  // since EnhancedChangesList groups log entries by performer rather than by target article
515  if ( $rc->mAttribs['rc_type'] == RC_LOG && $cl instanceof EnhancedChangesList &&
516  $grouped ) {
517  return '';
518  } else {
519  return $this->getLinkRenderer()
520  ->makeKnownLink( $rc->getTitle(),
521  $this->msg( 'watchlist-unwatch' )->text(), [
522  'class' => 'mw-unwatch-link',
523  'title' => $this->msg( 'tooltip-ca-unwatch' )->text()
524  ], [ 'action' => 'unwatch' ] ) . '&#160;';
525  }
526  } );
527  }
528  $dbr->dataSeek( $rows, 0 );
529 
530  if ( $this->getConfig()->get( 'RCShowWatchingUsers' )
531  && $user->getOption( 'shownumberswatching' )
532  ) {
533  $watchedItemStore = MediaWikiServices::getInstance()->getWatchedItemStore();
534  }
535 
536  $s = $list->beginRecentChangesList();
537 
538  if ( $this->isStructuredFilterUiEnabled() ) {
539  $s .= $this->makeLegend();
540  }
541 
542  $userShowHiddenCats = $this->getUser()->getBoolOption( 'showhiddencats' );
543  $counter = 1;
544  foreach ( $rows as $obj ) {
545  # Make RC entry
546  $rc = RecentChange::newFromRow( $obj );
547 
548  # Skip CatWatch entries for hidden cats based on user preference
549  if (
550  $rc->getAttribute( 'rc_type' ) == RC_CATEGORIZE &&
551  !$userShowHiddenCats &&
552  $rc->getParam( 'hidden-cat' )
553  ) {
554  continue;
555  }
556 
557  $rc->counter = $counter++;
558 
559  if ( $this->getConfig()->get( 'ShowUpdatedMarker' ) ) {
560  $updated = $obj->wl_notificationtimestamp;
561  } else {
562  $updated = false;
563  }
564 
565  if ( isset( $watchedItemStore ) ) {
566  $rcTitleValue = new TitleValue( (int)$obj->rc_namespace, $obj->rc_title );
567  $rc->numberofWatchingusers = $watchedItemStore->countWatchers( $rcTitleValue );
568  } else {
569  $rc->numberofWatchingusers = 0;
570  }
571 
572  $changeLine = $list->recentChangesLine( $rc, $updated, $counter );
573  if ( $changeLine !== false ) {
574  $s .= $changeLine;
575  }
576  }
577  $s .= $list->endRecentChangesList();
578 
579  $output->addHTML( $s );
580  }
581 
588  public function doHeader( $opts, $numRows ) {
589  $user = $this->getUser();
590  $out = $this->getOutput();
591 
592  $out->addSubtitle(
593  $this->msg( 'watchlistfor2', $user->getName() )
595  $this->getLanguage(),
596  $this->getLinkRenderer()
597  ) )
598  );
599 
600  $this->setTopText( $opts );
601 
602  $form = '';
603 
604  $form .= Xml::openElement( 'form', [
605  'method' => 'get',
606  'action' => wfScript(),
607  'id' => 'mw-watchlist-form'
608  ] );
609  $form .= Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() );
610  $form .= Xml::openElement(
611  'fieldset',
612  [ 'id' => 'mw-watchlist-options', 'class' => 'cloptions' ]
613  );
614  $form .= Xml::element(
615  'legend', null, $this->msg( 'watchlist-options' )->text()
616  );
617 
618  if ( !$this->isStructuredFilterUiEnabled() ) {
619  $form .= $this->makeLegend();
620  }
621 
622  $lang = $this->getLanguage();
623  $timestamp = wfTimestampNow();
624  $wlInfo = Html::rawElement(
625  'span',
626  [
627  'class' => 'wlinfo',
628  'data-params' => json_encode( [ 'from' => $timestamp ] ),
629  ],
630  $this->msg( 'wlnote' )->numParams( $numRows, round( $opts['days'] * 24 ) )->params(
631  $lang->userDate( $timestamp, $user ), $lang->userTime( $timestamp, $user )
632  )->parse()
633  ) . "<br />\n";
634 
635  $nondefaults = $opts->getChangedValues();
636  $cutofflinks = Html::rawElement(
637  'span',
638  [ 'class' => 'cldays cloption' ],
639  $this->msg( 'wlshowtime' ) . ' ' . $this->cutoffselector( $opts )
640  );
641 
642  # Spit out some control panel links
643  $links = [];
644  $namesOfDisplayedFilters = [];
645  foreach ( $this->getLegacyShowHideFilters() as $filterName => $filter ) {
646  $namesOfDisplayedFilters[] = $filterName;
647  $links[] = $this->showHideCheck(
648  $nondefaults,
649  $filter->getShowHide(),
650  $filterName,
651  $opts[ $filterName ],
652  $filter->isFeatureAvailableOnStructuredUi( $this )
653  );
654  }
655 
656  $hiddenFields = $nondefaults;
657  $hiddenFields['action'] = 'submit';
658  unset( $hiddenFields['namespace'] );
659  unset( $hiddenFields['invert'] );
660  unset( $hiddenFields['associated'] );
661  unset( $hiddenFields['days'] );
662  foreach ( $namesOfDisplayedFilters as $filterName ) {
663  unset( $hiddenFields[$filterName] );
664  }
665 
666  # Namespace filter and put the whole form together.
667  $form .= $wlInfo;
668  $form .= $cutofflinks;
669  $form .= Html::rawElement(
670  'span',
671  [ 'class' => 'clshowhide' ],
672  $this->msg( 'watchlist-hide' ) .
673  $this->msg( 'colon-separator' )->escaped() .
674  implode( ' ', $links )
675  );
676  $form .= "\n<br />\n";
677 
678  $namespaceForm = Html::namespaceSelector(
679  [
680  'selected' => $opts['namespace'],
681  'all' => '',
682  'label' => $this->msg( 'namespace' )->text()
683  ], [
684  'name' => 'namespace',
685  'id' => 'namespace',
686  'class' => 'namespaceselector',
687  ]
688  ) . "\n";
689  $namespaceForm .= '<span class="mw-input-with-label">' . Xml::checkLabel(
690  $this->msg( 'invert' )->text(),
691  'invert',
692  'nsinvert',
693  $opts['invert'],
694  [ 'title' => $this->msg( 'tooltip-invert' )->text() ]
695  ) . "</span>\n";
696  $namespaceForm .= '<span class="mw-input-with-label">' . Xml::checkLabel(
697  $this->msg( 'namespace_association' )->text(),
698  'associated',
699  'nsassociated',
700  $opts['associated'],
701  [ 'title' => $this->msg( 'tooltip-namespace_association' )->text() ]
702  ) . "</span>\n";
703  $form .= Html::rawElement(
704  'span',
705  [ 'class' => 'namespaceForm cloption' ],
706  $namespaceForm
707  );
708 
709  $form .= Xml::submitButton(
710  $this->msg( 'watchlist-submit' )->text(),
711  [ 'class' => 'cloption-submit' ]
712  ) . "\n";
713  foreach ( $hiddenFields as $key => $value ) {
714  $form .= Html::hidden( $key, $value ) . "\n";
715  }
716  $form .= Xml::closeElement( 'fieldset' ) . "\n";
717  $form .= Xml::closeElement( 'form' ) . "\n";
718 
719  // Insert a placeholder for RCFilters
720  if ( $this->isStructuredFilterUiEnabled() ) {
721  $rcfilterContainer = Html::element(
722  'div',
723  [ 'class' => 'rcfilters-container' ]
724  );
725 
726  $loadingContainer = Html::rawElement(
727  'div',
728  [ 'class' => 'rcfilters-spinner' ],
730  'div',
731  [ 'class' => 'rcfilters-spinner-bounce' ]
732  )
733  );
734 
735  // Wrap both with rcfilters-head
736  $this->getOutput()->addHTML(
738  'div',
739  [ 'class' => 'rcfilters-head' ],
740  $rcfilterContainer . $form
741  )
742  );
743 
744  // Add spinner
745  $this->getOutput()->addHTML( $loadingContainer );
746  } else {
747  $this->getOutput()->addHTML( $form );
748  }
749 
750  $this->setBottomText( $opts );
751  }
752 
753  function cutoffselector( $options ) {
754  $selected = (float)$options['days'];
755  if ( $selected <= 0 ) {
756  $selected = $this->maxDays;
757  }
758 
759  $selectedHours = round( $selected * 24 );
760 
761  $hours = array_unique( array_filter( [
762  1,
763  2,
764  6,
765  12,
766  24,
767  72,
768  168,
769  24 * (float)$this->getUser()->getOption( 'watchlistdays', 0 ),
770  24 * $this->maxDays,
771  $selectedHours
772  ] ) );
773  asort( $hours );
774 
775  $select = new XmlSelect( 'days', 'days', (float)( $selectedHours / 24 ) );
776 
777  foreach ( $hours as $value ) {
778  if ( $value < 24 ) {
779  $name = $this->msg( 'hours' )->numParams( $value )->text();
780  } else {
781  $name = $this->msg( 'days' )->numParams( $value / 24 )->text();
782  }
783  $select->addOption( $name, (float)( $value / 24 ) );
784  }
785 
786  return $select->getHTML() . "\n<br />\n";
787  }
788 
789  function setTopText( FormOptions $opts ) {
790  $nondefaults = $opts->getChangedValues();
791  $form = '';
792  $user = $this->getUser();
793 
794  $numItems = $this->countItems();
795  $showUpdatedMarker = $this->getConfig()->get( 'ShowUpdatedMarker' );
796 
797  // Show watchlist header
798  $watchlistHeader = '';
799  if ( $numItems == 0 ) {
800  $watchlistHeader = $this->msg( 'nowatchlist' )->parse();
801  } else {
802  $watchlistHeader .= $this->msg( 'watchlist-details' )->numParams( $numItems )->parse() . "\n";
803  if ( $this->getConfig()->get( 'EnotifWatchlist' )
804  && $user->getOption( 'enotifwatchlistpages' )
805  ) {
806  $watchlistHeader .= $this->msg( 'wlheader-enotif' )->parse() . "\n";
807  }
808  if ( $showUpdatedMarker ) {
809  $watchlistHeader .= $this->msg(
810  $this->isStructuredFilterUiEnabled() ?
811  'rcfilters-watchlist-showupdated' :
812  'wlheader-showupdated'
813  )->parse() . "\n";
814  }
815  }
816  $form .= Html::rawElement(
817  'div',
818  [ 'class' => 'watchlistDetails' ],
819  $watchlistHeader
820  );
821 
822  if ( $numItems > 0 && $showUpdatedMarker ) {
823  $form .= Xml::openElement( 'form', [ 'method' => 'post',
824  'action' => $this->getPageTitle()->getLocalURL(),
825  'id' => 'mw-watchlist-resetbutton' ] ) . "\n" .
826  Xml::submitButton( $this->msg( 'enotif_reset' )->text(),
827  [ 'name' => 'mw-watchlist-reset-submit' ] ) . "\n" .
828  Html::hidden( 'token', $user->getEditToken() ) . "\n" .
829  Html::hidden( 'reset', 'all' ) . "\n";
830  foreach ( $nondefaults as $key => $value ) {
831  $form .= Html::hidden( $key, $value ) . "\n";
832  }
833  $form .= Xml::closeElement( 'form' ) . "\n";
834  }
835 
836  $this->getOutput()->addHTML( $form );
837  }
838 
839  protected function showHideCheck( $options, $message, $name, $value, $inStructuredUi ) {
840  $options[$name] = 1 - (int)$value;
841 
842  $attribs = [ 'class' => 'mw-input-with-label clshowhideoption cloption' ];
843  if ( $inStructuredUi ) {
844  $attribs[ 'data-feature-in-structured-ui' ] = true;
845  }
846 
847  return Html::rawElement(
848  'span',
849  $attribs,
850  // not using Html::checkLabel because that would escape the contents
851  Html::check( $name, (int)$value, [ 'id' => $name ] ) . Html::rawElement(
852  'label',
853  $attribs + [ 'for' => $name ],
854  // <nowiki/> at beginning to avoid messages with "$1 ..." being parsed as pre tags
855  $this->msg( $message, '<nowiki/>' )->parse()
856  )
857  );
858  }
859 
867  protected function countItems() {
868  $store = MediaWikiServices::getInstance()->getWatchedItemStore();
869  $count = $store->countWatchedItems( $this->getUser() );
870  return floor( $count / 2 );
871  }
872 }
$user
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a account $user
Definition: hooks.txt:247
DerivativeRequest
Similar to FauxRequest, but only fakes URL parameters and method (POST or GET) and use the base reque...
Definition: DerivativeRequest.php:34
SpecialPage\getPageTitle
getPageTitle( $subpage=false)
Get a self-referential title object.
Definition: SpecialPage.php:675
SpecialEditWatchlist\EDIT_CLEAR
const EDIT_CLEAR
Editing modes.
Definition: SpecialEditWatchlist.php:46
RecentChange\getQueryInfo
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new recentchanges object.
Definition: RecentChange.php:268
SpecialEditWatchlist\EDIT_RAW
const EDIT_RAW
Definition: SpecialEditWatchlist.php:47
SpecialPage\msg
msg( $key)
Wrapper around wfMessage that sets the current context.
Definition: SpecialPage.php:793
use
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
Definition: APACHE-LICENSE-2.0.txt:10
$rows
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction $rows
Definition: hooks.txt:2783
SpecialWatchlist\$savedQueriesPreferenceName
static $savedQueriesPreferenceName
Definition: SpecialWatchlist.php:35
SpecialPage\getOutput
getOutput()
Get the OutputPage being used for this instance.
Definition: SpecialPage.php:722
array
the array() calling protocol came about after MediaWiki 1.4rc1.
$output
static configuration should be added through ResourceLoaderGetConfigVars instead can be used to get the real title after the basic globals have been set but before ordinary actions take place $output
Definition: hooks.txt:2255
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:33
ChangesListSpecialPage\makeLegend
makeLegend()
Return the legend displayed within the fieldset.
Definition: ChangesListSpecialPage.php:1673
text
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add text
Definition: design.txt:18
RecentChange
Utility class for creating new RC entries.
Definition: RecentChange.php:68
SpecialWatchlist\getSubpagesForPrefixSearch
getSubpagesForPrefixSearch()
Return an array of subpages that this special page will accept.
Definition: SpecialWatchlist.php:126
SpecialWatchlist\getDB
getDB()
Return a IDatabase object for reading.
Definition: SpecialWatchlist.php:461
Html\check
static check( $name, $checked=false, array $attribs=[])
Convenience function to produce a checkbox (input element with type=checkbox)
Definition: Html.php:666
RC_LOG
const RC_LOG
Definition: Defines.php:154
ChangesListSpecialPage
Special page which uses a ChangesList to show query results.
Definition: ChangesListSpecialPage.php:35
$out
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output $out
Definition: hooks.txt:864
SpecialPage\checkPermissions
checkPermissions()
Checks if userCanExecute, and if not throws a PermissionsError.
Definition: SpecialPage.php:306
$params
$params
Definition: styleTest.css.php:40
$s
$s
Definition: mergeMessageFileList.php:187
SpecialPage\getTitleFor
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,...
Definition: SpecialPage.php:82
SpecialWatchlist\__construct
__construct( $page='Watchlist', $restriction='viewmywatchlist')
Definition: SpecialWatchlist.php:41
SpecialPage\getLanguage
getLanguage()
Shortcut to get user's language.
Definition: SpecialPage.php:752
SpecialWatchlist\doHeader
doHeader( $opts, $numRows)
Set the text to be displayed above the changes.
Definition: SpecialWatchlist.php:588
Xml\openElement
static openElement( $element, $attribs=null)
This opens an XML element.
Definition: Xml.php:109
php
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:37
LIST_AND
const LIST_AND
Definition: Defines.php:53
SpecialWatchlist\doMainQuery
doMainQuery( $tables, $fields, $conds, $query_options, $join_conds, FormOptions $opts)
@inheritDoc
Definition: SpecialWatchlist.php:355
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
XmlSelect
Class for generating HTML <select> or <datalist> elements.
Definition: XmlSelect.php:26
$dbr
$dbr
Definition: testCompression.php:50
SpecialWatchlist\$maxDays
$maxDays
Definition: SpecialWatchlist.php:39
ChangeTags\modifyDisplayQuery
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='')
Applies all tags-related changes to a query.
Definition: ChangeTags.php:723
Config
Interface for configuration instances.
Definition: Config.php:28
SpecialPage\addHelpLink
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Definition: SpecialPage.php:832
LIST_OR
const LIST_OR
Definition: Defines.php:56
SpecialWatchlist\transformFilterDefinition
transformFilterDefinition(array $filterDefinition)
@inheritDoc
Definition: SpecialWatchlist.php:137
SpecialPage\getConfig
getConfig()
Shortcut to get main config object.
Definition: SpecialPage.php:761
SpecialPage\addFeedLinks
addFeedLinks( $params)
Adds RSS/atom links.
Definition: SpecialPage.php:814
wfScript
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
Definition: GlobalFunctions.php:2890
Wikimedia\Rdbms\IResultWrapper
Result wrapper for grabbing data queried from an IDatabase object.
Definition: IResultWrapper.php:24
Config\get
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2812
ChangesListSpecialPage\isStructuredFilterUiEnabled
isStructuredFilterUiEnabled()
Check whether the structured filter UI is enabled.
Definition: ChangesListSpecialPage.php:1873
SpecialWatchlist\countItems
countItems()
Count the number of paired items on a user's watchlist.
Definition: SpecialWatchlist.php:867
Xml\element
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition: Xml.php:39
RecentChange\newFromRow
static newFromRow( $row)
Definition: RecentChange.php:124
SpecialWatchlist\cutoffselector
cutoffselector( $options)
Definition: SpecialWatchlist.php:753
$title
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:964
SpecialPage\getUser
getUser()
Shortcut to get the User executing this instance.
Definition: SpecialPage.php:732
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
wfTimestampNow
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
Definition: GlobalFunctions.php:2009
SpecialWatchlist\showHideCheck
showHideCheck( $options, $message, $name, $value, $inStructuredUi)
Definition: SpecialWatchlist.php:839
SpecialWatchlist\registerFilters
registerFilters()
@inheritDoc
Definition: SpecialWatchlist.php:148
LogPage\DELETED_ACTION
const DELETED_ACTION
Definition: LogPage.php:32
SpecialPage\getContext
getContext()
Gets the context this SpecialPage is executed in.
Definition: SpecialPage.php:695
SpecialPage\requireLogin
requireLogin( $reasonMsg='exception-nologin-text', $titleMsg='exception-nologin')
If the user is not logged in, throws UserNotLoggedIn error.
Definition: SpecialPage.php:336
SpecialWatchlist
A special page that lists last changes made to the wiki, limited to user-defined list of titles.
Definition: SpecialWatchlist.php:34
$options
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition: hooks.txt:2001
ChangesList\newFromContext
static newFromContext(IContextSource $context, array $groups=[])
Fetch an appropriate changes list class for the specified context Some users might want to use an enh...
Definition: ChangesList.php:88
execute
$batch execute()
Html\hidden
static hidden( $name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:774
$attribs
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses after processing & $attribs
Definition: hooks.txt:2014
EnhancedChangesList
Definition: EnhancedChangesList.php:23
SpecialWatchlist\execute
execute( $subpage)
Main execution point.
Definition: SpecialWatchlist.php:56
SpecialEditWatchlist\getMode
static getMode( $request, $par)
Determine whether we are editing the watchlist, and if so, what kind of editing operation.
Definition: SpecialEditWatchlist.php:709
$value
$value
Definition: styleTest.css.php:45
Html\namespaceSelector
static namespaceSelector(array $params=[], array $selectAttribs=[])
Build a drop-down box for selecting a namespace.
Definition: Html.php:862
ChangesListSpecialPage\getFilterGroup
getFilterGroup( $groupName)
Gets a specified ChangesListFilterGroup by name.
Definition: ChangesListSpecialPage.php:1197
SpecialPage\getRequest
getRequest()
Get the WebRequest being used for this instance.
Definition: SpecialPage.php:712
ChangesListStringOptionsFilterGroup\NONE
const NONE
Signifies that no options in the group are selected, meaning the group has no effect.
Definition: ChangesListStringOptionsFilterGroup.php:59
SpecialWatchlist\outputFeedLinks
outputFeedLinks()
Output feed links.
Definition: SpecialWatchlist.php:468
ChangesListSpecialPage\areFiltersInConflict
areFiltersInConflict()
Check if filters are in conflict and guaranteed to return no results.
Definition: ChangesListSpecialPage.php:578
SpecialEditWatchlist\buildTools
static buildTools( $lang, LinkRenderer $linkRenderer=null)
Build a set of links for convenient navigation between watchlist viewing and editing modes.
Definition: SpecialEditWatchlist.php:735
SpecialWatchlist\doesWrites
doesWrites()
Indicates whether this special page may perform database writes.
Definition: SpecialWatchlist.php:47
$name
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:302
ChangesListBooleanFilterGroup
If the group is active, any unchecked filters will translate to hide parameters in the URL.
Definition: ChangesListBooleanFilterGroup.php:12
Xml\closeElement
static closeElement( $element)
Shortcut to close an XML element.
Definition: Xml.php:118
SpecialWatchlist\outputChangesList
outputChangesList( $rows, $opts)
Build and output the actual changes list.
Definition: SpecialWatchlist.php:487
$tables
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist & $tables
Definition: hooks.txt:1015
as
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:22
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:209
SpecialWatchlist\$limitPreferenceName
static $limitPreferenceName
Definition: SpecialWatchlist.php:37
SpecialWatchlist\runMainQueryHook
runMainQueryHook(&$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts)
Definition: SpecialWatchlist.php:445
LogPage\DELETED_RESTRICTED
const DELETED_RESTRICTED
Definition: LogPage.php:35
ChangesList
Definition: ChangesList.php:28
SpecialWatchlist\getCustomFilters
getCustomFilters()
Get all custom filters.
Definition: SpecialWatchlist.php:292
SpecialWatchlist\checkStructuredFilterUiEnabled
static checkStructuredFilterUiEnabled(Config $config, User $user)
Static method to check whether StructuredFilter UI is enabled for the given user.
Definition: SpecialWatchlist.php:113
class
you have access to all of the normal MediaWiki so you can get a DB use the etc For full docs on the Maintenance class
Definition: maintenance.txt:56
RC_CATEGORIZE
const RC_CATEGORIZE
Definition: Defines.php:156
FormOptions
Helper class to keep track of options when mixing links and form elements.
Definition: FormOptions.php:35
$request
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction you ll probably need to make sure the header is varied on $request
Definition: hooks.txt:2806
Html\element
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:231
MediaWikiServices
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency MediaWikiServices
Definition: injection.txt:25
FormOptions\getChangedValues
getChangedValues()
Return options modified as an array ( name => value )
Definition: FormOptions.php:306
SpecialWatchlist\setTopText
setTopText(FormOptions $opts)
Send the text to be displayed before the options.
Definition: SpecialWatchlist.php:789
SpecialWatchlist\fetchOptionsFromRequest
fetchOptionsFromRequest( $opts)
Fetch values for a FormOptions object from the WebRequest associated with this instance.
Definition: SpecialWatchlist.php:310
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:53
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:203
ChangesListSpecialPage\setBottomText
setBottomText(FormOptions $opts)
Send the text to be displayed after the options.
Definition: ChangesListSpecialPage.php:1651
ChangesListSpecialPage\registerFilterGroup
registerFilterGroup(ChangesListFilterGroup $group)
Register a structured changes list filter group.
Definition: ChangesListSpecialPage.php:1175
ChangesListStringOptionsFilterGroup
Represents a filter group with multiple string options.
Definition: ChangesListStringOptionsFilterGroup.php:37
ChangesListSpecialPage\getOptions
getOptions()
Get the current FormOptions for this request.
Definition: ChangesListSpecialPage.php:937
SpecialWatchlist\$daysPreferenceName
static $daysPreferenceName
Definition: SpecialWatchlist.php:36
$context
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction you ll probably need to make sure the header is varied on and they can depend only on the ResourceLoaderContext $context
Definition: hooks.txt:2811
ChangesListSpecialPage\$customFilters
array $customFilters
Definition: ChangesListSpecialPage.php:67
ChangesListSpecialPage\getLegacyShowHideFilters
getLegacyShowHideFilters()
Definition: ChangesListSpecialPage.php:1097
Xml\submitButton
static submitButton( $value, $attribs=[])
Convenience function to build an HTML submit button When $wgUseMediaWikiUIEverywhere is true it will ...
Definition: Xml.php:460
Xml\checkLabel
static checkLabel( $label, $name, $id, $checked=false, $attribs=[])
Convenience function to build an HTML checkbox with a label.
Definition: Xml.php:420
TitleValue
Represents a page (or page fragment) title within MediaWiki.
Definition: TitleValue.php:35