4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License along
15 * with this program; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 * http://www.gnu.org/copyleft/gpl.html
22 namespace MediaWiki\User
;
24 use MediaWiki\Config\ServiceOptions
;
25 use MediaWiki\Revision\RevisionLookup
;
26 use MediaWiki\Revision\RevisionRecord
;
29 use Wikimedia\Rdbms\ILoadBalancer
;
32 * Manages user talk page notifications
35 class TalkPageNotificationManager
{
38 * @internal For use by ServiceWiring
40 public const CONSTRUCTOR_OPTIONS
= [
45 private $userMessagesCache = [];
48 private $disableAnonTalk;
50 /** @var ILoadBalancer */
51 private $loadBalancer;
53 /** @var ReadOnlyMode */
54 private $readOnlyMode;
56 /** @var RevisionLookup */
57 private $revisionLookup;
60 * @param ServiceOptions $serviceOptions
61 * @param ILoadBalancer $loadBalancer
62 * @param ReadOnlyMode $readOnlyMode
63 * @param RevisionLookup $revisionLookup
65 public function __construct(
66 ServiceOptions
$serviceOptions,
67 ILoadBalancer
$loadBalancer,
68 ReadOnlyMode
$readOnlyMode,
69 RevisionLookup
$revisionLookup
71 $serviceOptions->assertRequiredOptions( self
::CONSTRUCTOR_OPTIONS
);
72 $this->disableAnonTalk
= $serviceOptions->get( 'DisableAnonTalk' );
73 $this->loadBalancer
= $loadBalancer;
74 $this->readOnlyMode
= $readOnlyMode;
75 $this->revisionLookup
= $revisionLookup;
79 * Check if the user has new messages.
80 * @param UserIdentity $user
81 * @return bool whether the user has new messages
83 public function userHasNewMessages( UserIdentity
$user ) : bool {
84 $userKey = $this->getCacheKey( $user );
86 // Load the newtalk status if it is unloaded
87 if ( !isset( $this->userMessagesCache
[$userKey] ) ) {
88 if ( $this->isTalkDisabled( $user ) ) {
89 // Anon disabled by configuration.
90 $this->userMessagesCache
[$userKey] = false;
92 $this->userMessagesCache
[$userKey] = $this->dbCheckNewUserMessages( $user );
96 return (bool)$this->userMessagesCache
[$userKey];
100 * Update the talk page messages status.
102 * @param UserIdentity $user
103 * @param RevisionRecord|null $curRev New, as yet unseen revision of the user talk page.
104 * Null is acceptable in case the revision is not known. This will indicate that new messages
105 * exist, but will not affect the latest seen message timestamp
107 public function setUserHasNewMessages(
109 RevisionRecord
$curRev = null
111 if ( $this->isTalkDisabled( $user ) ) {
115 $userKey = $this->getCacheKey( $user );
116 $this->userMessagesCache
[$userKey] = true;
117 $this->dbUpdateNewUserMessages( $user, $curRev );
121 * Remove the new messages status
122 * @param UserIdentity $user
124 public function removeUserHasNewMessages( UserIdentity
$user ) : void
{
125 if ( $this->isTalkDisabled( $user ) ) {
129 $userKey = $this->getCacheKey( $user );
130 $this->userMessagesCache
[$userKey] = false;
132 $this->dbDeleteNewUserMessages( $user );
136 * Returns the timestamp of the latest revision of the user talkpage
137 * that the user has already seen in TS_MW format.
138 * If the user has no new messages, returns null
140 * @param UserIdentity $user
141 * @return string|null
143 public function getLatestSeenMessageTimestamp( UserIdentity
$user ) : ?
string {
144 $userKey = $this->getCacheKey( $user );
145 // Don't use self::userHasNewMessages here to avoid an extra DB query
146 // in case the value is not cached already
147 if ( $this->isTalkDisabled( $user ) ||
148 isset( $this->userMessagesCache
[$userKey] ) && !$this->userMessagesCache
[$userKey]
153 $dbr = $this->loadBalancer
->getConnectionRef( DB_REPLICA
);
154 list( $field, $id ) = $this->getQueryFieldAndId( $user );
155 // Get the "last viewed rev" timestamp from the oldest message notification
156 $timestamp = $dbr->selectField(
158 'MIN(user_last_timestamp)',
163 // TODO: Once passing 'null' as Revision setUserHasNewMessages is removed,
164 // null $timestamp would mean no new messages, so negatives can be cached too.
165 $this->userMessagesCache
[$userKey] = true;
167 return $timestamp !== null ? MWTimestamp
::convert( TS_MW
, $timestamp ) : null;
171 * Remove the cached newtalk status for the given user
172 * @internal There should be no need to call this other than from User::clearInstanceCache
173 * @param UserIdentity $user
175 public function clearInstanceCache( UserIdentity
$user ) : void
{
176 $userKey = $this->getCacheKey( $user );
177 $this->userMessagesCache
[$userKey] = null;
181 * Check whether the talk page is disabled for a user
182 * @param UserIdentity $user
185 private function isTalkDisabled( UserIdentity
$user ) : bool {
186 return !$user->isRegistered() && $this->disableAnonTalk
;
190 * Internal uncached check for new messages
191 * @param UserIdentity $user
192 * @return bool True if the user has new messages
194 private function dbCheckNewUserMessages( UserIdentity
$user ) : bool {
195 $dbr = $this->loadBalancer
->getConnectionRef( DB_REPLICA
);
196 list( $field, $id ) = $this->getQueryFieldAndId( $user );
197 $ok = $dbr->selectField(
207 * Add or update the new messages flag
208 * @param UserIdentity $user
209 * @param RevisionRecord|null $curRev New, as yet unseen revision of the
210 * user talk page. Ignored if null.
211 * @return bool True if successful, false otherwise
213 private function dbUpdateNewUserMessages(
215 RevisionRecord
$curRev = null
217 if ( $this->readOnlyMode
->isReadOnly() ) {
222 $prevRev = $this->revisionLookup
->getPreviousRevision( $curRev );
223 $ts = $prevRev ?
$prevRev->getTimestamp() : null;
228 // Mark the user as having new messages since this revision
229 $dbw = $this->loadBalancer
->getConnectionRef( DB_MASTER
);
230 list( $field, $id ) = $this->getQueryFieldAndId( $user );
235 'user_last_timestamp' => $dbw->timestampOrNull( $ts )
240 return (bool)$dbw->affectedRows();
244 * Clear the new messages flag for the given user
245 * @param UserIdentity $user
246 * @return bool True if successful, false otherwise
248 private function dbDeleteNewUserMessages( UserIdentity
$user ) : bool {
249 if ( $this->readOnlyMode
->isReadOnly() ) {
252 $dbw = $this->loadBalancer
->getConnectionRef( DB_MASTER
);
253 list( $field, $id ) = $this->getQueryFieldAndId( $user );
259 return (bool)$dbw->affectedRows();
263 * Get the field name and id for the user_newtalk table query
264 * @param UserIdentity $user
265 * @return array ( string $field, string|int $id )
267 private function getQueryFieldAndId( UserIdentity
$user ) : array {
268 if ( $user->isRegistered() ) {
270 $id = $user->getId();
273 $id = $user->getName();
275 return [ $field, $id ];
279 * Gets a unique key for various caches.
280 * @param UserIdentity $user
283 private function getCacheKey( UserIdentity
$user ) : string {
284 return $user->isRegistered() ?
"u:{$user->getId()}" : "anon:{$user->getName()}";