MediaWiki  master
WatchlistManager.php
Go to the documentation of this file.
1 <?php
2 
24 
39 use NamespaceInfo;
40 use ReadOnlyMode;
41 use Status;
42 use StatusValue;
43 use User;
46 
53 
57  public const CONSTRUCTOR_OPTIONS = [
61  ];
62 
64  private $options;
65 
67  private $hookRunner;
68 
70  private $readOnlyMode;
71 
73  private $revisionLookup;
74 
76  private $talkPageNotificationManager;
77 
79  private $watchedItemStore;
80 
82  private $userFactory;
83 
85  private $nsInfo;
86 
88  private $wikiPageFactory;
89 
106  private $notificationTimestampCache = [];
107 
119  public function __construct(
120  ServiceOptions $options,
121  HookContainer $hookContainer,
122  ReadOnlyMode $readOnlyMode,
123  RevisionLookup $revisionLookup,
124  TalkPageNotificationManager $talkPageNotificationManager,
125  WatchedItemStoreInterface $watchedItemStore,
126  UserFactory $userFactory,
127  NamespaceInfo $nsInfo,
128  WikiPageFactory $wikiPageFactory
129  ) {
130  $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
131  $this->options = $options;
132  $this->hookRunner = new HookRunner( $hookContainer );
133  $this->readOnlyMode = $readOnlyMode;
134  $this->revisionLookup = $revisionLookup;
135  $this->talkPageNotificationManager = $talkPageNotificationManager;
136  $this->watchedItemStore = $watchedItemStore;
137  $this->userFactory = $userFactory;
138  $this->nsInfo = $nsInfo;
139  $this->wikiPageFactory = $wikiPageFactory;
140  }
141 
151  public function clearAllUserNotifications( $performer ) {
152  if ( $this->readOnlyMode->isReadOnly() ) {
153  // Cannot change anything in read only
154  return;
155  }
156 
157  if ( !$performer instanceof Authority ) {
158  $performer = $this->userFactory->newFromUserIdentity( $performer );
159  }
160 
161  if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
162  // User isn't allowed to edit the watchlist
163  return;
164  }
165 
166  $user = $performer->getUser();
167 
168  if ( !$this->options->get( MainConfigNames::EnotifUserTalk ) &&
169  !$this->options->get( MainConfigNames::EnotifWatchlist ) &&
170  !$this->options->get( MainConfigNames::ShowUpdatedMarker )
171  ) {
172  $this->talkPageNotificationManager->removeUserHasNewMessages( $user );
173  return;
174  }
175 
176  if ( !$user->isRegistered() ) {
177  return;
178  }
179 
180  $this->watchedItemStore->resetAllNotificationTimestampsForUser( $user );
181 
182  // We also need to clear here the "you have new message" notification for the own
183  // user_talk page; it's cleared one page view later in WikiPage::doViewUpdates().
184  }
185 
200  $performer,
201  $title,
202  int $oldid = 0,
203  RevisionRecord $oldRev = null
204  ) {
205  if ( $this->readOnlyMode->isReadOnly() ) {
206  // Cannot change anything in read only
207  return;
208  }
209 
210  if ( !$performer instanceof Authority ) {
211  $performer = $this->userFactory->newFromUserIdentity( $performer );
212  }
213 
214  if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
215  // User isn't allowed to edit the watchlist
216  return;
217  }
218 
219  $userIdentity = $performer->getUser();
220  $userTalkPage = (
221  $title->getNamespace() === NS_USER_TALK &&
222  $title->getDBkey() === strtr( $userIdentity->getName(), ' ', '_' )
223  );
224 
225  if ( $userTalkPage ) {
226  if ( !$oldid ) {
227  $oldRev = null;
228  } elseif ( !$oldRev ) {
229  $oldRev = $this->revisionLookup->getRevisionById( $oldid );
230  }
231  $this->talkPageNotificationManager->clearForPageView( $userIdentity, $oldRev );
232  }
233 
234  if ( !$this->options->get( MainConfigNames::EnotifUserTalk ) &&
235  !$this->options->get( MainConfigNames::EnotifWatchlist ) &&
236  !$this->options->get( MainConfigNames::ShowUpdatedMarker )
237  ) {
238  return;
239  }
240 
241  if ( !$userIdentity->isRegistered() ) {
242  // Nothing else to do
243  return;
244  }
245 
246  // Only update the timestamp if the page is being watched.
247  // The query to find out if it is watched is cached both in memcached and per-invocation,
248  // and when it does have to be executed, it can be on a replica DB
249  // If this is the user's newtalk page, we always update the timestamp
250  $force = $userTalkPage ? 'force' : '';
251  $this->watchedItemStore->resetNotificationTimestamp( $userIdentity, $title, $force, $oldid );
252  }
253 
262  if ( !$user->isRegistered() ) {
263  return false;
264  }
265 
266  $cacheKey = 'u' . (string)$user->getId() . '-' .
267  (string)$title->getNamespace() . ':' . $title->getDBkey();
268 
269  // avoid isset here, as it'll return false for null entries
270  if ( array_key_exists( $cacheKey, $this->notificationTimestampCache ) ) {
271  return $this->notificationTimestampCache[ $cacheKey ];
272  }
273 
274  $watchedItem = $this->watchedItemStore->getWatchedItem( $user, $title );
275  if ( $watchedItem ) {
276  $timestamp = $watchedItem->getNotificationTimestamp();
277  } else {
278  $timestamp = false;
279  }
280 
281  $this->notificationTimestampCache[ $cacheKey ] = $timestamp;
282  return $timestamp;
283  }
284 
290  public function isWatchable( PageReference $target ): bool {
291  if ( !$this->nsInfo->isWatchable( $target->getNamespace() ) ) {
292  return false;
293  }
294 
295  if ( $target instanceof PageIdentity && !$target->canExist() ) {
296  // Catch "improper" Title instances
297  return false;
298  }
299 
300  return true;
301  }
302 
310  public function isWatchedIgnoringRights( UserIdentity $userIdentity, PageIdentity $target ): bool {
311  if ( $this->isWatchable( $target ) ) {
312  return $this->watchedItemStore->isWatched( $userIdentity, $target );
313  }
314  return false;
315  }
316 
325  public function isWatched( Authority $performer, PageIdentity $target ): bool {
326  if ( $performer->isAllowed( 'viewmywatchlist' ) ) {
327  return $this->isWatchedIgnoringRights( $performer->getUser(), $target );
328  }
329  return false;
330  }
331 
339  public function isTempWatchedIgnoringRights( UserIdentity $userIdentity, PageIdentity $target ): bool {
340  if ( $this->isWatchable( $target ) ) {
341  return $this->watchedItemStore->isTempWatched( $userIdentity, $target );
342  }
343  return false;
344  }
345 
354  public function isTempWatched( Authority $performer, PageIdentity $target ): bool {
355  if ( $performer->isAllowed( 'viewmywatchlist' ) ) {
356  return $this->isTempWatchedIgnoringRights( $performer->getUser(), $target );
357  }
358  return false;
359  }
360 
370  public function addWatchIgnoringRights(
371  UserIdentity $userIdentity,
372  PageIdentity $target,
373  ?string $expiry = null
374  ): StatusValue {
375  if ( !$this->isWatchable( $target ) ) {
376  return StatusValue::newFatal( 'watchlistnotwatchable' );
377  }
378 
379  $wikiPage = $this->wikiPageFactory->newFromTitle( $target );
380  $title = $wikiPage->getTitle();
381 
382  // TODO: update hooks to take Authority
383  $status = Status::newFatal( 'hookaborted' );
384  $user = $this->userFactory->newFromUserIdentity( $userIdentity );
385  if ( $this->hookRunner->onWatchArticle( $user, $wikiPage, $status, $expiry ) ) {
386  $status = StatusValue::newGood();
387  $this->watchedItemStore->addWatch( $userIdentity, $this->nsInfo->getSubjectPage( $title ), $expiry );
388  if ( $this->nsInfo->canHaveTalkPage( $title ) ) {
389  $this->watchedItemStore->addWatch( $userIdentity, $this->nsInfo->getTalkPage( $title ), $expiry );
390  }
391  $this->hookRunner->onWatchArticleComplete( $user, $wikiPage );
392  }
393 
394  // eventually user_touched should be factored out of User and this should be replaced
395  $user->invalidateCache();
396 
397  return $status;
398  }
399 
410  public function addWatch(
411  Authority $performer,
412  PageIdentity $target,
413  ?string $expiry = null
414  ): StatusValue {
415  if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
416  // TODO: this function should be moved out of User
417  return User::newFatalPermissionDeniedStatus( 'editmywatchlist' );
418  }
419 
420  return $this->addWatchIgnoringRights( $performer->getUser(), $target, $expiry );
421  }
422 
430  public function removeWatchIgnoringRights(
431  UserIdentity $userIdentity,
432  PageIdentity $target
433  ): StatusValue {
434  if ( !$this->isWatchable( $target ) ) {
435  return StatusValue::newFatal( 'watchlistnotwatchable' );
436  }
437 
438  $wikiPage = $this->wikiPageFactory->newFromTitle( $target );
439  $title = $wikiPage->getTitle();
440 
441  // TODO: update hooks to take Authority
442  $status = Status::newFatal( 'hookaborted' );
443  $user = $this->userFactory->newFromUserIdentity( $userIdentity );
444  if ( $this->hookRunner->onUnwatchArticle( $user, $wikiPage, $status ) ) {
445  $status = StatusValue::newGood();
446  $this->watchedItemStore->removeWatch( $userIdentity, $this->nsInfo->getSubjectPage( $title ) );
447  if ( $this->nsInfo->canHaveTalkPage( $title ) ) {
448  $this->watchedItemStore->removeWatch( $userIdentity, $this->nsInfo->getTalkPage( $title ) );
449  }
450  $this->hookRunner->onUnwatchArticleComplete( $user, $wikiPage );
451  }
452 
453  // eventually user_touched should be factored out of User and this should be replaced
454  $user->invalidateCache();
455 
456  return $status;
457  }
458 
467  public function removeWatch(
468  Authority $performer,
469  PageIdentity $target
470  ): StatusValue {
471  if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
472  // TODO: this function should be moved out of User
473  return User::newFatalPermissionDeniedStatus( 'editmywatchlist' );
474  }
475 
476  return $this->removeWatchIgnoringRights( $performer->getUser(), $target );
477  }
478 
492  public function setWatch(
493  bool $watch,
494  Authority $performer,
495  PageIdentity $target,
496  string $expiry = null
497  ): StatusValue {
498  // User must be registered, and either changing the watch state or at least the expiry.
499  if ( !$performer->getUser()->isRegistered() ) {
500  return StatusValue::newGood();
501  }
502 
503  // Only call addWatchIgnoringRights() or removeWatch() if there's been a change in the watched status.
504  $oldWatchedItem = $this->watchedItemStore->getWatchedItem( $performer->getUser(), $target );
505  $changingWatchStatus = (bool)$oldWatchedItem !== $watch;
506  if ( $oldWatchedItem && $expiry !== null ) {
507  // If there's an old watched item, a non-null change to the expiry requires an UPDATE.
508  $oldWatchPeriod = $oldWatchedItem->getExpiry() ?? 'infinity';
509  $changingWatchStatus = $changingWatchStatus ||
510  $oldWatchPeriod !== ExpiryDef::normalizeExpiry( $expiry, TS_MW );
511  }
512 
513  if ( $changingWatchStatus ) {
514  // If the user doesn't have 'editmywatchlist', we still want to
515  // allow them to add but not remove items via edits and such.
516  if ( $watch ) {
517  return $this->addWatchIgnoringRights( $performer->getUser(), $target, $expiry );
518  } else {
519  return $this->removeWatch( $performer, $target );
520  }
521  }
522 
523  return StatusValue::newGood();
524  }
525 }
526 
531 class_alias( WatchlistManager::class, 'MediaWiki\User\WatchlistNotificationManager' );
getUser()
const NS_USER_TALK
Definition: Defines.php:67
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Definition: WebStart.php:82
A class for passing options to services.
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:566
A class containing constants representing the names of configuration variables.
const EnotifWatchlist
Name constant for the EnotifWatchlist setting, for use with Config::get()
const EnotifUserTalk
Name constant for the EnotifUserTalk setting, for use with Config::get()
const ShowUpdatedMarker
Name constant for the ShowUpdatedMarker setting, for use with Config::get()
Service for creating WikiPage objects.
Page revision base class.
Creates User objects.
Definition: UserFactory.php:38
isTempWatchedIgnoringRights(UserIdentity $userIdentity, PageIdentity $target)
Check if the article is temporarily watched by the user.
setWatch(bool $watch, Authority $performer, PageIdentity $target, string $expiry=null)
Watch or unwatch a page, calling watch/unwatch hooks as appropriate.
getTitleNotificationTimestamp(UserIdentity $user, $title)
Get the timestamp when this page was updated since the user last saw it.
clearTitleUserNotifications( $performer, $title, int $oldid=0, RevisionRecord $oldRev=null)
Clear the user's notification timestamp for the given title.
clearAllUserNotifications( $performer)
Resets all of the given user's page-change notification timestamps.
removeWatch(Authority $performer, PageIdentity $target)
Stop watching a page if the user has permission to edit their watchlist.
isWatched(Authority $performer, PageIdentity $target)
Check if the page is watched by the user and the user has permission to view their watchlist.
__construct(ServiceOptions $options, HookContainer $hookContainer, ReadOnlyMode $readOnlyMode, RevisionLookup $revisionLookup, TalkPageNotificationManager $talkPageNotificationManager, WatchedItemStoreInterface $watchedItemStore, UserFactory $userFactory, NamespaceInfo $nsInfo, WikiPageFactory $wikiPageFactory)
addWatch(Authority $performer, PageIdentity $target, ?string $expiry=null)
Watch a page if the user has permission to edit their watchlist.
isTempWatched(Authority $performer, PageIdentity $target)
Check if the page is temporarily watched by the user and the user has permission to view their watchl...
isWatchedIgnoringRights(UserIdentity $userIdentity, PageIdentity $target)
Check if the page is watched by the user.
addWatchIgnoringRights(UserIdentity $userIdentity, PageIdentity $target, ?string $expiry=null)
Watch a page.
removeWatchIgnoringRights(UserIdentity $userIdentity, PageIdentity $target)
Stop watching a page.
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
A service class for fetching the wiki's current read-only mode.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: StatusValue.php:46
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:73
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:85
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:44
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:70
static newFatalPermissionDeniedStatus( $permission)
Factory function for fatal permission-denied errors.
Definition: User.php:3400
Type definition for expiry timestamps.
Definition: ExpiryDef.php:17
Interface for objects (potentially) representing an editable wiki page.
canExist()
Checks whether this PageIdentity represents a "proper" page, meaning that it could exist as an editab...
Interface for objects (potentially) representing a page that can be viewable and linked to on a wiki.
This interface represents the authority associated the current execution context, such as a web reque...
Definition: Authority.php:37
getUser()
Returns the performer of the actions associated with this authority.
Service for looking up page revisions.
Interface for objects representing user identity.
getId( $wikiId=self::LOCAL)