Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 254
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
NewMessages
0.00% covered (danger)
0.00%
0 / 254
0.00% covered (danger)
0.00%
0 / 13
1406
0.00% covered (danger)
0.00%
0 / 1
 markThreadAsUnreadByUser
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 markThreadAsReadByUser
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 markAllReadByUser
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 writeUserMessageState
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
2
 getWhereClause
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 getRowsObject
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
2
 writeMessageStateForUpdatedThread
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
30
 getNotifyUsers
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
156
 notifyUsersByMail
0.00% covered (danger)
0.00%
0 / 67
0.00% covered (danger)
0.00%
0 / 1
42
 newUserMessages
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
2
 newMessageCount
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
12
 recacheMessageCount
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 watchedThreadsForUser
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3use MediaWiki\MediaWikiServices;
4use MediaWiki\User\User;
5use MediaWiki\User\UserIdentity;
6use Wikimedia\Rdbms\IExpression;
7use Wikimedia\Rdbms\IResultWrapper;
8use Wikimedia\Rdbms\RawSQLExpression;
9
10class 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}