MediaWiki REL1_37
WatchlistManager.php
Go to the documentation of this file.
1<?php
2
24
39use ReadOnlyMode;
40use Status;
41use StatusValue;
42use TitleValue;
43use User;
46
53
57 public const CONSTRUCTOR_OPTIONS = [
58 'UseEnotif',
59 'ShowUpdatedMarker',
60 ];
61
63 private $options;
64
66 private $hookRunner;
67
70
73
76
79
81 private $userFactory;
82
84 private $nsInfo;
85
88
106
118 public function __construct(
120 HookContainer $hookContainer,
128 ) {
129 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
130 $this->options = $options;
131 $this->hookRunner = new HookRunner( $hookContainer );
132 $this->readOnlyMode = $readOnlyMode;
133 $this->revisionLookup = $revisionLookup;
134 $this->talkPageNotificationManager = $talkPageNotificationManager;
135 $this->watchedItemStore = $watchedItemStore;
136 $this->userFactory = $userFactory;
137 $this->nsInfo = $nsInfo;
138 $this->wikiPageFactory = $wikiPageFactory;
139 }
140
150 public function clearAllUserNotifications( $performer ) {
151 if ( $this->readOnlyMode->isReadOnly() ) {
152 // Cannot change anything in read only
153 return;
154 }
155
156 if ( !$performer instanceof Authority ) {
157 $performer = $this->userFactory->newFromUserIdentity( $performer );
158 }
159
160 if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
161 // User isn't allowed to edit the watchlist
162 return;
163 }
164
165 $user = $performer->getUser();
166
167 if ( !$this->options->get( 'UseEnotif' ) &&
168 !$this->options->get( 'ShowUpdatedMarker' )
169 ) {
170 $this->talkPageNotificationManager->removeUserHasNewMessages( $user );
171 return;
172 }
173
174 $userId = $user->getId();
175 if ( !$userId ) {
176 return;
177 }
178
179 $this->watchedItemStore->resetAllNotificationTimestampsForUser( $user );
180
181 // We also need to clear here the "you have new message" notification for the own
182 // user_talk page; it's cleared one page view later in WikiPage::doViewUpdates().
183 }
184
197 $performer,
198 $title,
199 int $oldid = 0
200 ) {
201 if ( $this->readOnlyMode->isReadOnly() ) {
202 // Cannot change anything in read only
203 return;
204 }
205
206 if ( !$performer instanceof Authority ) {
207 $performer = $this->userFactory->newFromUserIdentity( $performer );
208 }
209
210 if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
211 // User isn't allowed to edit the watchlist
212 return;
213 }
214
215 $userIdentity = $performer->getUser();
216 $userTalkPage = (
217 $title->getNamespace() === NS_USER_TALK &&
218 $title->getDBkey() === strtr( $userIdentity->getName(), ' ', '_' )
219 );
220
221 if ( $userTalkPage ) {
222 // If we're working on user's talk page, we should update the talk page message indicator
223 if ( !$this->hookRunner->onUserClearNewTalkNotification(
224 $userIdentity,
225 $oldid
226 ) ) {
227 return;
228 }
229
230 // Try to update the DB post-send and only if needed...
233 DeferredUpdates::addCallableUpdate( static function () use (
234 $userIdentity,
235 $oldid,
238 ) {
239 if ( !$talkPageNotificationManager->userHasNewMessages( $userIdentity ) ) {
240 // no notifications to clear
241 return;
242 }
243 // Delete the last notifications (they stack up)
245
246 // If there is a new, unseen, revision, use its timestamp
247 if ( !$oldid ) {
248 return;
249 }
250
252 $oldid,
253 RevisionLookup::READ_LATEST
254 );
255 if ( !$oldRev ) {
256 return;
257 }
258
259 $newRev = $revisionLookup->getNextRevision( $oldRev );
260 if ( $newRev ) {
262 $userIdentity,
263 $newRev
264 );
265 }
266 } );
267 }
268
269 if ( !$this->options->get( 'UseEnotif' ) &&
270 !$this->options->get( 'ShowUpdatedMarker' )
271 ) {
272 return;
273 }
274
275 if ( !$userIdentity->isRegistered() ) {
276 // Nothing else to do
277 return;
278 }
279
280 // Only update the timestamp if the page is being watched.
281 // The query to find out if it is watched is cached both in memcached and per-invocation,
282 // and when it does have to be executed, it can be on a replica DB
283 // If this is the user's newtalk page, we always update the timestamp
284 $force = $userTalkPage ? 'force' : '';
285 $this->watchedItemStore->resetNotificationTimestamp( $userIdentity, $title, $force, $oldid );
286 }
287
296 $userId = $user->getId();
297
298 if ( !$userId ) {
299 return false;
300 }
301
302 $cacheKey = 'u' . (string)$userId . '-' .
303 (string)$title->getNamespace() . ':' . $title->getDBkey();
304
305 // avoid isset here, as it'll return false for null entries
306 if ( array_key_exists( $cacheKey, $this->notificationTimestampCache ) ) {
307 return $this->notificationTimestampCache[ $cacheKey ];
308 }
309
310 $watchedItem = $this->watchedItemStore->getWatchedItem( $user, $title );
311 if ( $watchedItem ) {
312 $timestamp = $watchedItem->getNotificationTimestamp();
313 } else {
314 $timestamp = false;
315 }
316
317 $this->notificationTimestampCache[ $cacheKey ] = $timestamp;
318 return $timestamp;
319 }
320
326 public function isWatchable( PageReference $target ): bool {
327 if ( !$this->nsInfo->isWatchable( $target->getNamespace() ) ) {
328 return false;
329 }
330
331 if ( $target instanceof PageIdentity && !$target->canExist() ) {
332 // Catch "improper" Title instances
333 return false;
334 }
335
336 return true;
337 }
338
346 public function isWatchedIgnoringRights( UserIdentity $userIdentity, PageIdentity $target ): bool {
347 if ( $this->isWatchable( $target ) ) {
348 return $this->watchedItemStore->isWatched( $userIdentity, $target );
349 }
350 return false;
351 }
352
361 public function isWatched( Authority $performer, PageIdentity $target ): bool {
362 if ( $performer->isAllowed( 'viewmywatchlist' ) ) {
363 return $this->isWatchedIgnoringRights( $performer->getUser(), $target );
364 }
365 return false;
366 }
367
375 public function isTempWatchedIgnoringRights( UserIdentity $userIdentity, PageIdentity $target ): bool {
376 if ( $this->isWatchable( $target ) ) {
377 return $this->watchedItemStore->isTempWatched( $userIdentity, $target );
378 }
379 return false;
380 }
381
390 public function isTempWatched( Authority $performer, PageIdentity $target ): bool {
391 if ( $performer->isAllowed( 'viewmywatchlist' ) ) {
392 return $this->isTempWatchedIgnoringRights( $performer->getUser(), $target );
393 }
394 return false;
395 }
396
406 public function addWatchIgnoringRights(
407 UserIdentity $userIdentity,
408 PageIdentity $target,
409 ?string $expiry = null
410 ): StatusValue {
411 if ( !$this->isWatchable( $target ) ) {
412 return StatusValue::newFatal( 'watchlistnotwatchable' );
413 }
414
415 $wikiPage = $this->wikiPageFactory->newFromTitle( $target );
416 $title = $wikiPage->getTitle();
417
418 // TODO: update hooks to take Authority
419 $status = Status::newFatal( 'hookaborted' );
420 $user = $this->userFactory->newFromUserIdentity( $userIdentity );
421 if ( $this->hookRunner->onWatchArticle( $user, $wikiPage, $status, $expiry ) ) {
422 $status = StatusValue::newGood();
423 $this->watchedItemStore->addWatch( $userIdentity, $this->nsInfo->getSubjectPage( $title ), $expiry );
424 if ( $this->nsInfo->canHaveTalkPage( $title ) ) {
425 $this->watchedItemStore->addWatch( $userIdentity, $this->nsInfo->getTalkPage( $title ), $expiry );
426 }
427 $this->hookRunner->onWatchArticleComplete( $user, $wikiPage );
428 }
429
430 // eventually user_touched should be factored out of User and this should be replaced
431 $user->invalidateCache();
432
433 return $status;
434 }
435
446 public function addWatch(
447 Authority $performer,
448 PageIdentity $target,
449 ?string $expiry = null
450 ): StatusValue {
451 if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
452 // TODO: this function should be moved out of User
453 return User::newFatalPermissionDeniedStatus( 'editmywatchlist' );
454 }
455
456 return $this->addWatchIgnoringRights( $performer->getUser(), $target, $expiry );
457 }
458
467 UserIdentity $userIdentity,
468 PageIdentity $target
469 ): StatusValue {
470 if ( !$this->isWatchable( $target ) ) {
471 return StatusValue::newFatal( 'watchlistnotwatchable' );
472 }
473
474 $wikiPage = $this->wikiPageFactory->newFromTitle( $target );
475 $title = $wikiPage->getTitle();
476
477 // TODO: update hooks to take Authority
478 $status = Status::newFatal( 'hookaborted' );
479 $user = $this->userFactory->newFromUserIdentity( $userIdentity );
480 if ( $this->hookRunner->onUnwatchArticle( $user, $wikiPage, $status ) ) {
481 $status = StatusValue::newGood();
482 $this->watchedItemStore->removeWatch( $userIdentity, $this->nsInfo->getSubjectPage( $title ) );
483 if ( $this->nsInfo->canHaveTalkPage( $title ) ) {
484 $this->watchedItemStore->removeWatch( $userIdentity, $this->nsInfo->getTalkPage( $title ) );
485 }
486 $this->hookRunner->onUnwatchArticleComplete( $user, $wikiPage );
487 }
488
489 // eventually user_touched should be factored out of User and this should be replaced
490 $user->invalidateCache();
491
492 return $status;
493 }
494
503 public function removeWatch(
504 Authority $performer,
505 PageIdentity $target
506 ): StatusValue {
507 if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
508 // TODO: this function should be moved out of User
509 return User::newFatalPermissionDeniedStatus( 'editmywatchlist' );
510 }
511
512 return $this->removeWatchIgnoringRights( $performer->getUser(), $target );
513 }
514
528 public function setWatch(
529 bool $watch,
530 Authority $performer,
531 PageIdentity $target,
532 string $expiry = null
533 ): StatusValue {
534 // User must be registered, and either changing the watch state or at least the expiry.
535 if ( !$performer->getUser()->isRegistered() ) {
536 return StatusValue::newGood();
537 }
538
539 // Only call addWatchhIgnoringRights() or removeWatch() if there's been a change in the watched status.
540 $link = TitleValue::newFromPage( $target );
541 $oldWatchedItem = $this->watchedItemStore->getWatchedItem( $performer->getUser(), $link );
542 $changingWatchStatus = (bool)$oldWatchedItem !== $watch;
543 if ( $oldWatchedItem && $expiry !== null ) {
544 // If there's an old watched item, a non-null change to the expiry requires an UPDATE.
545 $oldWatchPeriod = $oldWatchedItem->getExpiry() ?? 'infinity';
546 $changingWatchStatus = $changingWatchStatus ||
547 $oldWatchPeriod !== ExpiryDef::normalizeExpiry( $expiry, TS_MW );
548 }
549
550 if ( $changingWatchStatus ) {
551 // If the user doesn't have 'editmywatchlist', we still want to
552 // allow them to add but not remove items via edits and such.
553 if ( $watch ) {
554 return $this->addWatchIgnoringRights( $performer->getUser(), $target, $expiry );
555 } else {
556 return $this->removeWatch( $performer, $target );
557 }
558 }
559
560 return StatusValue::newGood();
561 }
562}
563
568class_alias( WatchlistManager::class, 'MediaWiki\User\WatchlistNotificationManager' );
const NS_USER_TALK
Definition Defines.php:67
if(ini_get('mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Definition Setup.php:88
Class for managing the deferral of updates within the scope of a PHP script invocation.
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...
setUserHasNewMessages(UserIdentity $user, RevisionRecord $curRev=null)
Update the talk page messages status.
userHasNewMessages(UserIdentity $user)
Check if the user has new messages.
removeUserHasNewMessages(UserIdentity $user)
Remove the new messages status.
Creates User objects.
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.
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...
clearTitleUserNotifications( $performer, $title, int $oldid=0)
Clear the user's notification timestamp for the given title.
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.
static newGood( $value=null)
Factory function for good results.
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.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:69
static newFatalPermissionDeniedStatus( $permission)
Factory function for fatal permission-denied errors.
Definition User.php:4214
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.
getNextRevision(RevisionRecord $rev, $flags=0)
Get next revision for this title.
getRevisionById( $id, $flags=0, PageIdentity $page=null)
Load a page revision from a given revision ID number.
Interface for objects representing user identity.
getId( $wikiId=self::LOCAL)