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 TitleValue;
44 use User;
47 
54 
58  public const CONSTRUCTOR_OPTIONS = [
59  'UseEnotif',
61  ];
62 
64  private $options;
65 
67  private $hookRunner;
68 
70  private $readOnlyMode;
71 
73  private $revisionLookup;
74 
77 
80 
82  private $userFactory;
83 
85  private $nsInfo;
86 
89 
107 
119  public function __construct(
121  HookContainer $hookContainer,
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( 'UseEnotif' ) &&
169  !$this->options->get( MainConfigNames::ShowUpdatedMarker )
170  ) {
171  $this->talkPageNotificationManager->removeUserHasNewMessages( $user );
172  return;
173  }
174 
175  $userId = $user->getId();
176  if ( !$userId ) {
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( 'UseEnotif' ) &&
235  !$this->options->get( MainConfigNames::ShowUpdatedMarker )
236  ) {
237  return;
238  }
239 
240  if ( !$userIdentity->isRegistered() ) {
241  // Nothing else to do
242  return;
243  }
244 
245  // Only update the timestamp if the page is being watched.
246  // The query to find out if it is watched is cached both in memcached and per-invocation,
247  // and when it does have to be executed, it can be on a replica DB
248  // If this is the user's newtalk page, we always update the timestamp
249  $force = $userTalkPage ? 'force' : '';
250  $this->watchedItemStore->resetNotificationTimestamp( $userIdentity, $title, $force, $oldid );
251  }
252 
261  $userId = $user->getId();
262 
263  if ( !$userId ) {
264  return false;
265  }
266 
267  $cacheKey = 'u' . (string)$userId . '-' .
268  (string)$title->getNamespace() . ':' . $title->getDBkey();
269 
270  // avoid isset here, as it'll return false for null entries
271  if ( array_key_exists( $cacheKey, $this->notificationTimestampCache ) ) {
272  return $this->notificationTimestampCache[ $cacheKey ];
273  }
274 
275  $watchedItem = $this->watchedItemStore->getWatchedItem( $user, $title );
276  if ( $watchedItem ) {
277  $timestamp = $watchedItem->getNotificationTimestamp();
278  } else {
279  $timestamp = false;
280  }
281 
282  $this->notificationTimestampCache[ $cacheKey ] = $timestamp;
283  return $timestamp;
284  }
285 
291  public function isWatchable( PageReference $target ): bool {
292  if ( !$this->nsInfo->isWatchable( $target->getNamespace() ) ) {
293  return false;
294  }
295 
296  if ( $target instanceof PageIdentity && !$target->canExist() ) {
297  // Catch "improper" Title instances
298  return false;
299  }
300 
301  return true;
302  }
303 
311  public function isWatchedIgnoringRights( UserIdentity $userIdentity, PageIdentity $target ): bool {
312  if ( $this->isWatchable( $target ) ) {
313  return $this->watchedItemStore->isWatched( $userIdentity, $target );
314  }
315  return false;
316  }
317 
326  public function isWatched( Authority $performer, PageIdentity $target ): bool {
327  if ( $performer->isAllowed( 'viewmywatchlist' ) ) {
328  return $this->isWatchedIgnoringRights( $performer->getUser(), $target );
329  }
330  return false;
331  }
332 
340  public function isTempWatchedIgnoringRights( UserIdentity $userIdentity, PageIdentity $target ): bool {
341  if ( $this->isWatchable( $target ) ) {
342  return $this->watchedItemStore->isTempWatched( $userIdentity, $target );
343  }
344  return false;
345  }
346 
355  public function isTempWatched( Authority $performer, PageIdentity $target ): bool {
356  if ( $performer->isAllowed( 'viewmywatchlist' ) ) {
357  return $this->isTempWatchedIgnoringRights( $performer->getUser(), $target );
358  }
359  return false;
360  }
361 
371  public function addWatchIgnoringRights(
372  UserIdentity $userIdentity,
373  PageIdentity $target,
374  ?string $expiry = null
375  ): StatusValue {
376  if ( !$this->isWatchable( $target ) ) {
377  return StatusValue::newFatal( 'watchlistnotwatchable' );
378  }
379 
380  $wikiPage = $this->wikiPageFactory->newFromTitle( $target );
381  $title = $wikiPage->getTitle();
382 
383  // TODO: update hooks to take Authority
384  $status = Status::newFatal( 'hookaborted' );
385  $user = $this->userFactory->newFromUserIdentity( $userIdentity );
386  if ( $this->hookRunner->onWatchArticle( $user, $wikiPage, $status, $expiry ) ) {
387  $status = StatusValue::newGood();
388  $this->watchedItemStore->addWatch( $userIdentity, $this->nsInfo->getSubjectPage( $title ), $expiry );
389  if ( $this->nsInfo->canHaveTalkPage( $title ) ) {
390  $this->watchedItemStore->addWatch( $userIdentity, $this->nsInfo->getTalkPage( $title ), $expiry );
391  }
392  $this->hookRunner->onWatchArticleComplete( $user, $wikiPage );
393  }
394 
395  // eventually user_touched should be factored out of User and this should be replaced
396  $user->invalidateCache();
397 
398  return $status;
399  }
400 
411  public function addWatch(
412  Authority $performer,
413  PageIdentity $target,
414  ?string $expiry = null
415  ): StatusValue {
416  if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
417  // TODO: this function should be moved out of User
418  return User::newFatalPermissionDeniedStatus( 'editmywatchlist' );
419  }
420 
421  return $this->addWatchIgnoringRights( $performer->getUser(), $target, $expiry );
422  }
423 
431  public function removeWatchIgnoringRights(
432  UserIdentity $userIdentity,
433  PageIdentity $target
434  ): StatusValue {
435  if ( !$this->isWatchable( $target ) ) {
436  return StatusValue::newFatal( 'watchlistnotwatchable' );
437  }
438 
439  $wikiPage = $this->wikiPageFactory->newFromTitle( $target );
440  $title = $wikiPage->getTitle();
441 
442  // TODO: update hooks to take Authority
443  $status = Status::newFatal( 'hookaborted' );
444  $user = $this->userFactory->newFromUserIdentity( $userIdentity );
445  if ( $this->hookRunner->onUnwatchArticle( $user, $wikiPage, $status ) ) {
446  $status = StatusValue::newGood();
447  $this->watchedItemStore->removeWatch( $userIdentity, $this->nsInfo->getSubjectPage( $title ) );
448  if ( $this->nsInfo->canHaveTalkPage( $title ) ) {
449  $this->watchedItemStore->removeWatch( $userIdentity, $this->nsInfo->getTalkPage( $title ) );
450  }
451  $this->hookRunner->onUnwatchArticleComplete( $user, $wikiPage );
452  }
453 
454  // eventually user_touched should be factored out of User and this should be replaced
455  $user->invalidateCache();
456 
457  return $status;
458  }
459 
468  public function removeWatch(
469  Authority $performer,
470  PageIdentity $target
471  ): StatusValue {
472  if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
473  // TODO: this function should be moved out of User
474  return User::newFatalPermissionDeniedStatus( 'editmywatchlist' );
475  }
476 
477  return $this->removeWatchIgnoringRights( $performer->getUser(), $target );
478  }
479 
493  public function setWatch(
494  bool $watch,
495  Authority $performer,
496  PageIdentity $target,
497  string $expiry = null
498  ): StatusValue {
499  // User must be registered, and either changing the watch state or at least the expiry.
500  if ( !$performer->getUser()->isRegistered() ) {
501  return StatusValue::newGood();
502  }
503 
504  // Only call addWatchIgnoringRights() or removeWatch() if there's been a change in the watched status.
505  $link = TitleValue::newFromPage( $target );
506  $oldWatchedItem = $this->watchedItemStore->getWatchedItem( $performer->getUser(), $link );
507  $changingWatchStatus = (bool)$oldWatchedItem !== $watch;
508  if ( $oldWatchedItem && $expiry !== null ) {
509  // If there's an old watched item, a non-null change to the expiry requires an UPDATE.
510  $oldWatchPeriod = $oldWatchedItem->getExpiry() ?? 'infinity';
511  $changingWatchStatus = $changingWatchStatus ||
512  $oldWatchPeriod !== ExpiryDef::normalizeExpiry( $expiry, TS_MW );
513  }
514 
515  if ( $changingWatchStatus ) {
516  // If the user doesn't have 'editmywatchlist', we still want to
517  // allow them to add but not remove items via edits and such.
518  if ( $watch ) {
519  return $this->addWatchIgnoringRights( $performer->getUser(), $target, $expiry );
520  } else {
521  return $this->removeWatch( $performer, $target );
522  }
523  }
524 
525  return StatusValue::newGood();
526  }
527 }
528 
533 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:562
A class containing constants representing the names of configuration variables.
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...
WatchedItemStoreInterface $watchedItemStore
isWatchedIgnoringRights(UserIdentity $userIdentity, PageIdentity $target)
Check if the page is watched by the user.
array $notificationTimestampCache
Cache for getTitleNotificationTimestamp.
TalkPageNotificationManager $talkPageNotificationManager
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:43
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:44
Represents a page (or page fragment) title within MediaWiki.
Definition: TitleValue.php:40
static newFromPage(PageReference $page)
Create a TitleValue from a local PageReference.
Definition: TitleValue.php:103
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:68
static newFatalPermissionDeniedStatus( $permission)
Factory function for fatal permission-denied errors.
Definition: User.php:3635
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)