MediaWiki master
WatchlistManager.php
Go to the documentation of this file.
1<?php
2
24
40use StatusValue;
44
51
55 public const OPTION_ENOTIF = 'isEnotifEnabled';
56
58 private $isEnotifEnabled;
59
61 private $hookRunner;
62
64 private $readOnlyMode;
65
67 private $revisionLookup;
68
70 private $talkPageNotificationManager;
71
73 private $watchedItemStore;
74
76 private $userFactory;
77
79 private $nsInfo;
80
82 private $wikiPageFactory;
83
100 private $notificationTimestampCache = [];
101
113 public function __construct(
114 array $options,
115 HookContainer $hookContainer,
116 ReadOnlyMode $readOnlyMode,
117 RevisionLookup $revisionLookup,
118 TalkPageNotificationManager $talkPageNotificationManager,
119 WatchedItemStoreInterface $watchedItemStore,
120 UserFactory $userFactory,
121 NamespaceInfo $nsInfo,
122 WikiPageFactory $wikiPageFactory
123 ) {
124 $this->isEnotifEnabled = $options[ self::OPTION_ENOTIF ];
125 $this->hookRunner = new HookRunner( $hookContainer );
126 $this->readOnlyMode = $readOnlyMode;
127 $this->revisionLookup = $revisionLookup;
128 $this->talkPageNotificationManager = $talkPageNotificationManager;
129 $this->watchedItemStore = $watchedItemStore;
130 $this->userFactory = $userFactory;
131 $this->nsInfo = $nsInfo;
132 $this->wikiPageFactory = $wikiPageFactory;
133 }
134
144 public function clearAllUserNotifications( $performer ) {
145 if ( $this->readOnlyMode->isReadOnly() ) {
146 // Cannot change anything in read only
147 return;
148 }
149
150 if ( !$performer instanceof Authority ) {
151 $performer = $this->userFactory->newFromUserIdentity( $performer );
152 }
153
154 $user = $performer->getUser();
155
156 // NOTE: Has to be before `editmywatchlist` user right check, to ensure
157 // anonymous / temporary users have their talk page notifications cleared (T345031).
158 if ( !$this->isEnotifEnabled ) {
159 $this->talkPageNotificationManager->removeUserHasNewMessages( $user );
160 return;
161 }
162
163 if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
164 // User isn't allowed to edit the watchlist
165 return;
166 }
167
168 if ( !$user->isRegistered() ) {
169 return;
170 }
171
172 $this->watchedItemStore->resetAllNotificationTimestampsForUser( $user );
173
174 // We also need to clear here the "you have new message" notification for the own
175 // user_talk page; it's cleared one page view later in WikiPage::doViewUpdates().
176 }
177
192 $performer,
193 $title,
194 int $oldid = 0,
195 RevisionRecord $oldRev = null
196 ) {
197 if ( $this->readOnlyMode->isReadOnly() ) {
198 // Cannot change anything in read only
199 return;
200 }
201
202 if ( !$performer instanceof Authority ) {
203 $performer = $this->userFactory->newFromUserIdentity( $performer );
204 }
205
206 $userIdentity = $performer->getUser();
207 $userTalkPage = (
208 $title->getNamespace() === NS_USER_TALK &&
209 $title->getDBkey() === strtr( $userIdentity->getName(), ' ', '_' )
210 );
211
212 if ( $userTalkPage ) {
213 if ( !$oldid ) {
214 $oldRev = null;
215 } elseif ( !$oldRev ) {
216 $oldRev = $this->revisionLookup->getRevisionById( $oldid );
217 }
218 // NOTE: Has to be called before isAllowed() check, to ensure users with no watchlist
219 // access (by default, temporary and anonymous users) can clear their talk page
220 // notification (T345031).
221 $this->talkPageNotificationManager->clearForPageView( $userIdentity, $oldRev );
222 }
223
224 if ( !$this->isEnotifEnabled ) {
225 return;
226 }
227
228 if ( !$userIdentity->isRegistered() ) {
229 // Nothing else to do
230 return;
231 }
232
233 // NOTE: Has to be checked after the TalkPageNotificationManager::clearForPageView call, to
234 // ensure users with no watchlist access (by default, temporary and anonymous users) can
235 // clear their talk page notification (T345031).
236 if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
237 // User isn't allowed to edit the watchlist
238 return;
239 }
240
241 // Only update the timestamp if the page is being watched.
242 // The query to find out if it is watched is cached both in memcached and per-invocation,
243 // and when it does have to be executed, it can be on a replica DB
244 // If this is the user's newtalk page, we always update the timestamp
245 $force = $userTalkPage ? 'force' : '';
246 $this->watchedItemStore->resetNotificationTimestamp( $userIdentity, $title, $force, $oldid );
247 }
248
256 public function getTitleNotificationTimestamp( UserIdentity $user, $title ) {
257 if ( !$user->isRegistered() ) {
258 return false;
259 }
260
261 $cacheKey = 'u' . $user->getId() . '-' .
262 $title->getNamespace() . ':' . $title->getDBkey();
263
264 // avoid isset here, as it'll return false for null entries
265 if ( array_key_exists( $cacheKey, $this->notificationTimestampCache ) ) {
266 return $this->notificationTimestampCache[ $cacheKey ];
267 }
268
269 $watchedItem = $this->watchedItemStore->getWatchedItem( $user, $title );
270 if ( $watchedItem ) {
271 $timestamp = $watchedItem->getNotificationTimestamp();
272 } else {
273 $timestamp = false;
274 }
275
276 $this->notificationTimestampCache[ $cacheKey ] = $timestamp;
277 return $timestamp;
278 }
279
285 public function isWatchable( PageReference $target ): bool {
286 if ( !$this->nsInfo->isWatchable( $target->getNamespace() ) ) {
287 return false;
288 }
289
290 if ( $target instanceof PageIdentity && !$target->canExist() ) {
291 // Catch "improper" Title instances
292 return false;
293 }
294
295 return true;
296 }
297
305 public function isWatchedIgnoringRights( UserIdentity $userIdentity, PageIdentity $target ): bool {
306 if ( $this->isWatchable( $target ) ) {
307 return $this->watchedItemStore->isWatched( $userIdentity, $target );
308 }
309 return false;
310 }
311
320 public function isWatched( Authority $performer, PageIdentity $target ): bool {
321 if ( $performer->isAllowed( 'viewmywatchlist' ) ) {
322 return $this->isWatchedIgnoringRights( $performer->getUser(), $target );
323 }
324 return false;
325 }
326
334 public function isTempWatchedIgnoringRights( UserIdentity $userIdentity, PageIdentity $target ): bool {
335 if ( $this->isWatchable( $target ) ) {
336 return $this->watchedItemStore->isTempWatched( $userIdentity, $target );
337 }
338 return false;
339 }
340
349 public function isTempWatched( Authority $performer, PageIdentity $target ): bool {
350 if ( $performer->isAllowed( 'viewmywatchlist' ) ) {
351 return $this->isTempWatchedIgnoringRights( $performer->getUser(), $target );
352 }
353 return false;
354 }
355
365 public function addWatchIgnoringRights(
366 UserIdentity $userIdentity,
367 PageIdentity $target,
368 ?string $expiry = null
369 ): StatusValue {
370 if ( !$this->isWatchable( $target ) ) {
371 return StatusValue::newFatal( 'watchlistnotwatchable' );
372 }
373
374 $wikiPage = $this->wikiPageFactory->newFromTitle( $target );
375 $title = $wikiPage->getTitle();
376
377 // TODO: update hooks to take Authority
378 $status = Status::newFatal( 'hookaborted' );
379 $user = $this->userFactory->newFromUserIdentity( $userIdentity );
380 if ( $this->hookRunner->onWatchArticle( $user, $wikiPage, $status, $expiry ) ) {
381 $status = StatusValue::newGood();
382 $this->watchedItemStore->addWatch( $userIdentity, $this->nsInfo->getSubjectPage( $title ), $expiry );
383 if ( $this->nsInfo->canHaveTalkPage( $title ) ) {
384 $this->watchedItemStore->addWatch( $userIdentity, $this->nsInfo->getTalkPage( $title ), $expiry );
385 }
386 $this->hookRunner->onWatchArticleComplete( $user, $wikiPage );
387 }
388
389 // eventually user_touched should be factored out of User and this should be replaced
390 $user->invalidateCache();
391
392 return $status;
393 }
394
405 public function addWatch(
406 Authority $performer,
407 PageIdentity $target,
408 ?string $expiry = null
409 ): StatusValue {
410 if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
411 // TODO: this function should be moved out of User
412 return User::newFatalPermissionDeniedStatus( 'editmywatchlist' );
413 }
414
415 return $this->addWatchIgnoringRights( $performer->getUser(), $target, $expiry );
416 }
417
426 UserIdentity $userIdentity,
427 PageIdentity $target
428 ): StatusValue {
429 if ( !$this->isWatchable( $target ) ) {
430 return StatusValue::newFatal( 'watchlistnotwatchable' );
431 }
432
433 $wikiPage = $this->wikiPageFactory->newFromTitle( $target );
434 $title = $wikiPage->getTitle();
435
436 // TODO: update hooks to take Authority
437 $status = Status::newFatal( 'hookaborted' );
438 $user = $this->userFactory->newFromUserIdentity( $userIdentity );
439 if ( $this->hookRunner->onUnwatchArticle( $user, $wikiPage, $status ) ) {
440 $status = StatusValue::newGood();
441 $this->watchedItemStore->removeWatch( $userIdentity, $this->nsInfo->getSubjectPage( $title ) );
442 if ( $this->nsInfo->canHaveTalkPage( $title ) ) {
443 $this->watchedItemStore->removeWatch( $userIdentity, $this->nsInfo->getTalkPage( $title ) );
444 }
445 $this->hookRunner->onUnwatchArticleComplete( $user, $wikiPage );
446 }
447
448 // eventually user_touched should be factored out of User and this should be replaced
449 $user->invalidateCache();
450
451 return $status;
452 }
453
462 public function removeWatch(
463 Authority $performer,
464 PageIdentity $target
465 ): StatusValue {
466 if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
467 // TODO: this function should be moved out of User
468 return User::newFatalPermissionDeniedStatus( 'editmywatchlist' );
469 }
470
471 return $this->removeWatchIgnoringRights( $performer->getUser(), $target );
472 }
473
487 public function setWatch(
488 bool $watch,
489 Authority $performer,
490 PageIdentity $target,
491 string $expiry = null
492 ): StatusValue {
493 // User must be registered, and either changing the watch state or at least the expiry.
494 if ( !$performer->getUser()->isRegistered() ) {
495 return StatusValue::newGood();
496 }
497
498 // Only call addWatchIgnoringRights() or removeWatch() if there's been a change in the watched status.
499 $oldWatchedItem = $this->watchedItemStore->getWatchedItem( $performer->getUser(), $target );
500 $changingWatchStatus = (bool)$oldWatchedItem !== $watch;
501 if ( $oldWatchedItem && $expiry !== null ) {
502 // If there's an old watched item, a non-null change to the expiry requires an UPDATE.
503 $oldWatchPeriod = $oldWatchedItem->getExpiry() ?? 'infinity';
504 $changingWatchStatus = $changingWatchStatus ||
505 $oldWatchPeriod !== ExpiryDef::normalizeExpiry( $expiry, TS_MW );
506 }
507
508 if ( $changingWatchStatus ) {
509 // If the user doesn't have 'editmywatchlist', we still want to
510 // allow them to add but not remove items via edits and such.
511 if ( $watch ) {
512 return $this->addWatchIgnoringRights( $performer->getUser(), $target, $expiry );
513 } else {
514 return $this->removeWatch( $performer, $target );
515 }
516 }
517
518 return StatusValue::newGood();
519 }
520}
getUser()
const NS_USER_TALK
Definition Defines.php:67
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:81
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Service for creating WikiPage objects.
Page revision base class.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Creates User objects.
internal since 1.36
Definition User.php:93
isTempWatchedIgnoringRights(UserIdentity $userIdentity, PageIdentity $target)
Check if the article is temporarily watched by the user.
__construct(array $options, HookContainer $hookContainer, ReadOnlyMode $readOnlyMode, RevisionLookup $revisionLookup, TalkPageNotificationManager $talkPageNotificationManager, WatchedItemStoreInterface $watchedItemStore, UserFactory $userFactory, NamespaceInfo $nsInfo, WikiPageFactory $wikiPageFactory)
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.
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.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
static newGood( $value=null)
Factory function for good results.
Type definition for expiry timestamps.
Definition ExpiryDef.php:17
Determine whether a site is currently in read-only mode.
Represents the target of a wiki link.
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 with the current execution context,...
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.
isRegistered()
This must be equivalent to getId() != 0 and is provided for code readability.
getId( $wikiId=self::LOCAL)