Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 254 |
|
0.00% |
0 / 13 |
CRAP | |
0.00% |
0 / 1 |
NewMessages | |
0.00% |
0 / 254 |
|
0.00% |
0 / 13 |
1406 | |
0.00% |
0 / 1 |
markThreadAsUnreadByUser | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
markThreadAsReadByUser | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
markAllReadByUser | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
writeUserMessageState | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
2 | |||
getWhereClause | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
2 | |||
getRowsObject | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
2 | |||
writeMessageStateForUpdatedThread | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
30 | |||
getNotifyUsers | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
156 | |||
notifyUsersByMail | |
0.00% |
0 / 67 |
|
0.00% |
0 / 1 |
42 | |||
newUserMessages | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
2 | |||
newMessageCount | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
12 | |||
recacheMessageCount | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
watchedThreadsForUser | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | use MediaWiki\MediaWikiServices; |
4 | use MediaWiki\User\User; |
5 | use MediaWiki\User\UserIdentity; |
6 | use Wikimedia\Rdbms\IExpression; |
7 | use Wikimedia\Rdbms\IResultWrapper; |
8 | use Wikimedia\Rdbms\RawSQLExpression; |
9 | |
10 | class NewMessages { |
11 | public static function markThreadAsUnreadByUser( Thread $thread, UserIdentity $user ) { |
12 | self::writeUserMessageState( $thread, $user ); |
13 | } |
14 | |
15 | /** |
16 | * @param Thread $thread |
17 | * @param UserIdentity $user |
18 | */ |
19 | public static function markThreadAsReadByUser( Thread $thread, UserIdentity $user ) { |
20 | $thread_id = $thread->id(); |
21 | $user_id = $user->getId(); |
22 | |
23 | $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase(); |
24 | |
25 | $dbw->newDeleteQueryBuilder() |
26 | ->deleteFrom( 'user_message_state' ) |
27 | ->where( [ 'ums_user' => $user_id, 'ums_thread' => $thread_id ] ) |
28 | ->caller( __METHOD__ ) |
29 | ->execute(); |
30 | |
31 | self::recacheMessageCount( $user_id ); |
32 | } |
33 | |
34 | /** |
35 | * @param UserIdentity|int $user |
36 | */ |
37 | public static function markAllReadByUser( $user ) { |
38 | if ( is_object( $user ) ) { |
39 | $user_id = $user->getId(); |
40 | } elseif ( is_int( $user ) ) { |
41 | $user_id = $user; |
42 | } else { |
43 | throw new InvalidArgumentException( __METHOD__ . " expected User or integer but got $user" ); |
44 | } |
45 | |
46 | $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase(); |
47 | |
48 | $dbw->newDeleteQueryBuilder() |
49 | ->deleteFrom( 'user_message_state' ) |
50 | ->where( [ 'ums_user' => $user_id ] ) |
51 | ->caller( __METHOD__ ) |
52 | ->execute(); |
53 | |
54 | self::recacheMessageCount( $user_id ); |
55 | } |
56 | |
57 | private static function writeUserMessageState( Thread $thread, UserIdentity $user ) { |
58 | $thread_id = $thread->id(); |
59 | $user_id = $user->getId(); |
60 | |
61 | $conversation = Threads::withId( $thread_id )->topmostThread()->id(); |
62 | |
63 | $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase(); |
64 | $dbw->newReplaceQueryBuilder() |
65 | ->replaceInto( 'user_message_state' ) |
66 | ->uniqueIndexFields( [ 'ums_user', 'ums_thread' ] ) |
67 | ->row( [ |
68 | 'ums_user' => $user_id, |
69 | 'ums_thread' => $thread_id, |
70 | 'ums_read_timestamp' => null, |
71 | 'ums_conversation' => $conversation |
72 | ] ) |
73 | ->caller( __METHOD__ ) |
74 | ->execute(); |
75 | |
76 | self::recacheMessageCount( $user_id ); |
77 | } |
78 | |
79 | /** |
80 | * Get the where clause for an update |
81 | * If the thread is on a user's talkpage, set that user's newtalk. |
82 | * |
83 | * @param Thread $t |
84 | * @return IExpression |
85 | */ |
86 | private static function getWhereClause( $t ) { |
87 | $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase(); |
88 | |
89 | $tpTitle = $t->getTitle(); |
90 | $rootThread = $t->topmostThread()->root()->getTitle(); |
91 | |
92 | // Select any applicable watchlist entries for the thread. |
93 | $talkpageWhere = $dbw->andExpr( [ |
94 | 'wl_namespace' => $tpTitle->getNamespace(), |
95 | 'wl_title' => $tpTitle->getDBkey() |
96 | ] ); |
97 | $rootWhere = $dbw->andExpr( [ |
98 | 'wl_namespace' => $rootThread->getNamespace(), |
99 | 'wl_title' => $rootThread->getDBkey() |
100 | ] ); |
101 | |
102 | return $dbw->orExpr( [ $talkpageWhere, $rootWhere ] ); |
103 | } |
104 | |
105 | /** |
106 | * @param Thread $t |
107 | * @return IResultWrapper |
108 | */ |
109 | private static function getRowsObject( $t ) { |
110 | $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
111 | return $dbr->newSelectQueryBuilder() |
112 | ->select( [ 'wl_user', 'ums_user', 'ums_read_timestamp', 'up_value' ] ) |
113 | ->from( 'watchlist' ) |
114 | ->leftJoin( 'user_message_state', null, [ |
115 | 'ums_user=wl_user', |
116 | 'ums_thread' => $t->id() |
117 | ] ) |
118 | ->leftJoin( 'user_properties', null, [ |
119 | 'up_user=wl_user', |
120 | 'up_property' => 'lqtnotifytalk', |
121 | ] ) |
122 | ->where( self::getWhereClause( $t ) ) |
123 | ->caller( __METHOD__ ) |
124 | ->fetchResultSet(); |
125 | } |
126 | |
127 | /** |
128 | * Write a user_message_state for each user who is watching the thread. |
129 | * If the thread is on a user's talkpage, set that user's newtalk. |
130 | * @param Thread $t |
131 | * @param int $type |
132 | * @param UserIdentity $changeUser |
133 | */ |
134 | public static function writeMessageStateForUpdatedThread( $t, $type, $changeUser ) { |
135 | wfDebugLog( 'LiquidThreads', 'Doing notifications' ); |
136 | |
137 | $usersByCategory = self::getNotifyUsers( $t, $changeUser ); |
138 | $userIds = $usersByCategory['notify']; |
139 | $notifyUsers = $usersByCategory['email']; |
140 | |
141 | // Do the actual updates |
142 | if ( count( $userIds ) ) { |
143 | $insertRows = []; |
144 | foreach ( $userIds as $u ) { |
145 | $insertRows[] = [ |
146 | 'ums_user' => $u, |
147 | 'ums_thread' => $t->id(), |
148 | 'ums_read_timestamp' => null, |
149 | 'ums_conversation' => $t->topmostThread()->id(), |
150 | ]; |
151 | } |
152 | |
153 | $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase(); |
154 | $dbw->newReplaceQueryBuilder() |
155 | ->replaceInto( 'user_message_state' ) |
156 | ->uniqueIndexFields( [ 'ums_user', 'ums_thread' ] ) |
157 | ->rows( $insertRows ) |
158 | ->caller( __METHOD__ ) |
159 | ->execute(); |
160 | } |
161 | |
162 | global $wgLqtEnotif; |
163 | if ( count( $notifyUsers ) && $wgLqtEnotif ) { |
164 | self::notifyUsersByMail( $t, $notifyUsers, wfTimestampNow(), $type ); |
165 | } |
166 | } |
167 | |
168 | /** |
169 | * @param Thread $t |
170 | * @param UserIdentity $changeUser |
171 | * @return array |
172 | */ |
173 | public static function getNotifyUsers( $t, $changeUser ) { |
174 | // Pull users to update the message state for, including whether or not a |
175 | // user_message_state row exists for them, and whether or not to send an email |
176 | // notification. |
177 | $userIds = []; |
178 | $notifyUsers = []; |
179 | $services = MediaWikiServices::getInstance(); |
180 | $userOptionsLookup = $services->getUserOptionsLookup(); |
181 | $res = self::getRowsObject( $t ); |
182 | foreach ( $res as $row ) { |
183 | // Don't notify yourself |
184 | if ( $changeUser->getId() == $row->wl_user ) { |
185 | continue; |
186 | } |
187 | |
188 | if ( !$row->ums_user || $row->ums_read_timestamp ) { |
189 | $userIds[] = $row->wl_user; |
190 | self::recacheMessageCount( $row->wl_user ); |
191 | } |
192 | |
193 | global $wgHiddenPrefs; |
194 | if ( !in_array( 'lqtnotifytalk', $wgHiddenPrefs ) && isset( $row->up_value ) ) { |
195 | $wantsTalkNotification = (bool)$row->wl_user; |
196 | } else { |
197 | $wantsTalkNotification = $userOptionsLookup->getDefaultOption( 'lqtnotifytalk' ); |
198 | } |
199 | |
200 | if ( $wantsTalkNotification ) { |
201 | $notifyUsers[] = $row->wl_user; |
202 | } |
203 | } |
204 | |
205 | // Add user talk notification |
206 | if ( $t->getTitle()->getNamespace() == NS_USER_TALK ) { |
207 | $name = $t->getTitle()->getText(); |
208 | |
209 | $user = User::newFromName( $name ); |
210 | if ( $user && $user->getName() !== $changeUser->getName() ) { |
211 | $services->getTalkPageNotificationManager() |
212 | ->setUserHasNewMessages( $user ); |
213 | |
214 | $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup(); |
215 | $userIds[] = $user->getId(); |
216 | if ( $userOptionsLookup->getOption( $user, 'enotifusertalkpages' ) ) { |
217 | $notifyUsers[] = $user->getId(); |
218 | } |
219 | } |
220 | } |
221 | |
222 | return [ |
223 | 'notify' => $userIds, |
224 | 'email' => $notifyUsers, |
225 | ]; |
226 | } |
227 | |
228 | /** |
229 | * @param Thread $t |
230 | * @param int[] $watching_users |
231 | * @param string $timestamp |
232 | * @param int $type |
233 | */ |
234 | public static function notifyUsersByMail( $t, $watching_users, $timestamp, $type ) { |
235 | $messages = [ |
236 | Threads::CHANGE_REPLY_CREATED => 'lqt-enotif-reply', |
237 | Threads::CHANGE_NEW_THREAD => 'lqt-enotif-newthread', |
238 | ]; |
239 | $subjects = [ |
240 | Threads::CHANGE_REPLY_CREATED => 'lqt-enotif-subject-reply', |
241 | Threads::CHANGE_NEW_THREAD => 'lqt-enotif-subject-newthread', |
242 | ]; |
243 | |
244 | if ( !isset( $messages[$type] ) || !isset( $subjects[$type] ) ) { |
245 | wfDebugLog( 'LiquidThreads', "Email notification failed: type $type unrecognised" ); |
246 | return; |
247 | } else { |
248 | $msgName = $messages[$type]; |
249 | $subjectMsg = $subjects[$type]; |
250 | } |
251 | |
252 | // Send email notification, fetching all the data in one go |
253 | $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
254 | |
255 | $res = $dbr->newSelectQueryBuilder() |
256 | ->select( [ |
257 | 'u.*', |
258 | 'timecorrection' => 'tc_prop.up_value', |
259 | 'language' => 'l_prop.up_value' |
260 | ] ) |
261 | ->from( 'user', 'u' ) |
262 | ->leftJoin( 'user_properties', 'tc_prop', [ |
263 | 'tc_prop.up_user=user_id', |
264 | 'tc_prop.up_property' => 'timecorrection', |
265 | ] ) |
266 | ->leftJoin( 'user_properties', 'l_prop', [ |
267 | 'l_prop.up_user=user_id', |
268 | 'l_prop.up_property' => 'language', |
269 | ] ) |
270 | ->where( [ 'u.user_id' => $watching_users ] ) |
271 | ->caller( __METHOD__ ) |
272 | ->fetchResultSet(); |
273 | |
274 | // Set up one-time data. |
275 | global $wgPasswordSender; |
276 | $link_title = clone $t->getTitle(); |
277 | $link_title->setFragment( '#' . $t->getAnchorName() ); |
278 | $permalink = LqtView::linkInContextCanonicalURL( $t ); |
279 | $talkPage = $t->getTitle()->getPrefixedText(); |
280 | $from = new MailAddress( $wgPasswordSender, wfMessage( 'emailsender' )->text() ); |
281 | $threadSubject = $t->subject(); |
282 | |
283 | // Parse content and strip HTML of post content |
284 | $emailer = MediaWikiServices::getInstance()->getEmailer(); |
285 | $languageFactory = MediaWikiServices::getInstance()->getLanguageFactory(); |
286 | foreach ( $res as $row ) { |
287 | $u = User::newFromRow( $row ); |
288 | |
289 | if ( $row->language ) { |
290 | $langCode = $row->language; |
291 | } else { |
292 | global $wgLanguageCode; |
293 | |
294 | $langCode = $wgLanguageCode; |
295 | } |
296 | |
297 | $lang = $languageFactory->getLanguage( $langCode ); |
298 | |
299 | // Adjust with time correction |
300 | $timeCorrection = $row->timecorrection; |
301 | $adjustedTimestamp = (string)$lang->userAdjust( $timestamp, $timeCorrection ); |
302 | |
303 | $date = $lang->date( $adjustedTimestamp ); |
304 | $time = $lang->time( $adjustedTimestamp ); |
305 | |
306 | $content = $t->root()->getPage()->getContent(); |
307 | $params = [ |
308 | $u->getName(), |
309 | $t->subject(), |
310 | $date, |
311 | $time, |
312 | $talkPage, |
313 | $permalink, |
314 | ( $content instanceof TextContent ) ? $content->getText() : '', |
315 | $t->author()->getName() |
316 | ]; |
317 | |
318 | // Get message in user's own language, bug 20645 |
319 | $msg = wfMessage( $msgName, $params )->inLanguage( $langCode )->text(); |
320 | |
321 | $to = MailAddress::newFromUser( $u ); |
322 | $subject = wfMessage( $subjectMsg, $threadSubject )->inLanguage( $langCode )->text(); |
323 | |
324 | $emailer->send( [ $to ], $from, $subject, $msg ); |
325 | } |
326 | } |
327 | |
328 | public static function newUserMessages( $user ) { |
329 | $talkPage = MediaWikiServices::getInstance()->getWikiPageFactory() |
330 | ->newFromTitle( $user->getUserPage()->getTalkPage() ); |
331 | |
332 | $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
333 | |
334 | $joinConds = [ 'ums_user' => null ]; |
335 | $joinConds[] = $dbr->andExpr( [ |
336 | 'ums_user' => $user->getId(), |
337 | new RawSQLExpression( 'ums_thread=thread_id' ), |
338 | ] ); |
339 | $joinClause = $dbr->orExpr( $joinConds ); |
340 | |
341 | $res = $dbr->newSelectQueryBuilder() |
342 | ->select( '*' ) |
343 | ->from( 'user_message_state' ) |
344 | ->leftJoin( 'thread', null, [ $joinClause ] ) |
345 | ->where( [ |
346 | 'ums_read_timestamp' => null, |
347 | Threads::articleClause( $talkPage ) |
348 | ] ) |
349 | ->caller( __METHOD__ ) |
350 | ->fetchResultSet(); |
351 | |
352 | return Threads::loadFromResult( $res, $dbr ); |
353 | } |
354 | |
355 | /** |
356 | * @param User $user |
357 | * @param int $dbIndex |
358 | * @return int |
359 | */ |
360 | public static function newMessageCount( $user, $dbIndex = DB_REPLICA ) { |
361 | $services = MediaWikiServices::getInstance(); |
362 | $cache = $services->getMainWANObjectCache(); |
363 | $connectionProvider = $services->getConnectionProvider(); |
364 | $fname = __METHOD__; |
365 | |
366 | return (int)$cache->getWithSetCallback( |
367 | $cache->makeKey( 'lqt-new-messages-count', $user->getId() ), |
368 | $cache::TTL_DAY, |
369 | static function () use ( $user, $dbIndex, $fname, $connectionProvider ) { |
370 | if ( $dbIndex === DB_REPLICA ) { |
371 | $db = $connectionProvider->getReplicaDatabase(); |
372 | } else { |
373 | $db = $connectionProvider->getPrimaryDatabase(); |
374 | } |
375 | |
376 | $cond = [ 'ums_user' => $user->getId(), 'ums_read_timestamp' => null ]; |
377 | |
378 | $res = $db->newSelectQueryBuilder() |
379 | ->select( '1' ) |
380 | ->from( 'user_message_state' ) |
381 | ->where( $cond ) |
382 | ->caller( $fname ) |
383 | ->limit( 500 ) |
384 | ->fetchResultSet(); |
385 | $count = $res->numRows(); |
386 | if ( $count >= 500 ) { |
387 | $count = $db->newSelectQueryBuilder() |
388 | ->select( '*' ) |
389 | ->from( 'user_message_state' ) |
390 | ->where( $cond ) |
391 | ->caller( $fname ) |
392 | ->estimateRowCount(); |
393 | } |
394 | |
395 | return $count; |
396 | } |
397 | ); |
398 | } |
399 | |
400 | public static function recacheMessageCount( $uid ) { |
401 | $cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); |
402 | $cache->delete( $cache->makeKey( 'lqt-new-messages-count', $uid ) ); |
403 | User::newFromId( $uid )->clearSharedCache( 'refresh' ); |
404 | } |
405 | |
406 | public static function watchedThreadsForUser( User $user ) { |
407 | $talkPage = MediaWikiServices::getInstance()->getWikiPageFactory() |
408 | ->newFromTitle( $user->getUserPage()->getTalkPage() ); |
409 | |
410 | $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
411 | |
412 | $res = $dbr->newSelectQueryBuilder() |
413 | ->select( '*' ) |
414 | ->from( 'thread' ) |
415 | ->join( 'user_message_state', null, 'ums_thread=thread_id' ) |
416 | ->where( [ |
417 | 'ums_read_timestamp' => null, |
418 | 'ums_user' => $user->getId(), |
419 | 'not (' . Threads::articleClause( $talkPage ) . ')', |
420 | ] ) |
421 | ->caller( __METHOD__ ) |
422 | ->fetchResultSet(); |
423 | |
424 | return Threads::loadFromResult( $res, $dbr ); |
425 | } |
426 | } |