Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
80.52% covered (warning)
80.52%
62 / 77
14.29% covered (danger)
14.29%
1 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
UserLocator
81.58% covered (warning)
81.58%
62 / 76
14.29% covered (danger)
14.29%
1 / 7
37.01
0.00% covered (danger)
0.00%
0 / 1
 locateUsersWatchingTitle
90.00% covered (success)
90.00%
18 / 20
0.00% covered (danger)
0.00%
0 / 1
2.00
 locateTalkPageOwner
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
5.07
 locateUserPageOwner
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 locateEventAgent
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 locateArticleCreator
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
6.40
 getArticleAuthorByArticleId
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
3.00
 locateFromEventExtra
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
7.01
1<?php
2
3namespace MediaWiki\Extension\Notifications;
4
5use BatchRowIterator;
6use Iterator;
7use MediaWiki\Extension\Notifications\Iterator\CallbackIterator;
8use MediaWiki\Extension\Notifications\Model\Event;
9use MediaWiki\MediaWikiServices;
10use MediaWiki\User\User;
11use RecursiveIteratorIterator;
12
13class UserLocator {
14    /**
15     * Return all users watching the event title.
16     *
17     * The echo job queue must be enabled to prevent timeouts submitting to
18     * heavily watched pages when this is used.
19     *
20     * @param Event $event
21     * @param int $batchSize
22     * @return User[]|Iterator<User>
23     */
24    public static function locateUsersWatchingTitle( Event $event, $batchSize = 500 ) {
25        $title = $event->getTitle();
26        if ( !$title ) {
27            return [];
28        }
29        $provider = MediaWikiServices::getInstance()->getConnectionProvider();
30        $batchRowIt = new BatchRowIterator(
31            $provider->getReplicaDatabase( false, 'watchlist' ),
32            /* $table = */ 'watchlist',
33            /* $primaryKeys = */ [ 'wl_user' ],
34            $batchSize
35        );
36        $batchRowIt->addConditions( [
37            'wl_namespace' => $title->getNamespace(),
38            'wl_title' => $title->getDBkey(),
39        ] );
40        $batchRowIt->setCaller( __METHOD__ );
41
42        // flatten the result into a stream of rows
43        $recursiveIt = new RecursiveIteratorIterator( $batchRowIt );
44
45        // add callback to convert user id to user objects
46        $echoCallbackIt = new CallbackIterator( $recursiveIt, static function ( $row ) {
47            return User::newFromId( $row->wl_user );
48        } );
49
50        return $echoCallbackIt;
51    }
52
53    /**
54     * If the event occurred on the talk page of a registered
55     * user return that user.
56     *
57     * @param Event $event
58     * @return User[]
59     */
60    public static function locateTalkPageOwner( Event $event ) {
61        $title = $event->getTitle();
62        if ( !$title || $title->getNamespace() !== NS_USER_TALK ) {
63            return [];
64        }
65
66        $user = User::newFromName( $title->getDBkey() );
67        if ( $user && $user->isRegistered() ) {
68            return [ $user->getId() => $user ];
69        }
70
71        return [];
72    }
73
74    /**
75     * If the event occurred on the user page of a registered
76     * user return that user.
77     *
78     * @param Event $event
79     * @return User[]
80     */
81    public static function locateUserPageOwner( Event $event ) {
82        $title = $event->getTitle();
83        if ( !$title || !$title->inNamespace( NS_USER ) ) {
84            return [];
85        }
86
87        $user = User::newFromName( $title->getDBkey() );
88        if ( $user && $user->isRegistered() ) {
89            return [ $user->getId() => $user ];
90        }
91
92        return [];
93    }
94
95    /**
96     * Return the event agent
97     *
98     * @param Event $event
99     * @return User[]
100     */
101    public static function locateEventAgent( Event $event ) {
102        $agent = $event->getAgent();
103        if ( $agent && $agent->isRegistered() ) {
104            return [ $agent->getId() => $agent ];
105        }
106
107        return [];
108    }
109
110    /**
111     * Return the user that created the first revision of the
112     * associated title.
113     *
114     * @param Event $event
115     * @return User[]
116     */
117    public static function locateArticleCreator( Event $event ) {
118        $title = $event->getTitle();
119
120        if ( !$title || $title->getArticleID() <= 0 ) {
121            return [];
122        }
123
124        $user = self::getArticleAuthorByArticleId( $title->getArticleID() );
125        if ( $user ) {
126            // T318523: Don't send page-linked notifications for pages created by bot users.
127            if ( $event->getType() === 'page-linked' && $user->isBot() ) {
128                return [];
129            }
130            return [ $user->getId() => $user ];
131        }
132
133        return [];
134    }
135
136    /**
137     * @param int $articleId
138     * @return User|null
139     */
140    public static function getArticleAuthorByArticleId( int $articleId ): ?User {
141        $services = MediaWikiServices::getInstance();
142        $dbr = $services->getConnectionProvider()->getReplicaDatabase();
143        $revQuery = $services->getRevisionStore()->getQueryInfo();
144        $res = $dbr->newSelectQueryBuilder()
145            ->select( [ 'rev_user' => $revQuery['fields']['rev_user'] ] )
146            ->tables( $revQuery['tables'] )
147            ->where( [ 'rev_page' => $articleId ] )
148            ->orderBy( [ 'rev_timestamp', 'rev_id' ] )
149            ->joinConds( $revQuery['joins'] )
150            ->caller( __METHOD__ )
151            ->fetchRow();
152        if ( !$res || !$res->rev_user ) {
153            return null;
154        }
155
156        return User::newFromId( $res->rev_user );
157    }
158
159    /**
160     * Fetch user ids from the event extra data.  Requires additional
161     * parameter.  Example $wgEchoNotifications parameter:
162     *
163     *   'user-locators' => [ [ 'event-extra', 'mentions' ] ],
164     *
165     * The above will look in the 'mentions' parameter for a user id or
166     * array of user ids.  It will return all these users as notification
167     * targets.
168     *
169     * @param Event $event
170     * @param string[] $keys one or more keys to check for user ids
171     * @return User[]
172     */
173    public static function locateFromEventExtra( Event $event, array $keys ) {
174        $users = [];
175        foreach ( $keys as $key ) {
176            $userIds = $event->getExtraParam( $key );
177            if ( !$userIds ) {
178                continue;
179            }
180            if ( !is_array( $userIds ) ) {
181                $userIds = [ $userIds ];
182            }
183            foreach ( $userIds as $userId ) {
184                // we shouldn't receive User instances, but allow
185                // it for backward compatability
186                if ( $userId instanceof User ) {
187                    if ( !$userId->isRegistered() ) {
188                        continue;
189                    }
190                    $user = $userId;
191                } else {
192                    $user = User::newFromId( $userId );
193                }
194                $users[$user->getId()] = $user;
195            }
196        }
197
198        return $users;
199    }
200}
201
202class_alias( UserLocator::class, 'EchoUserLocator' );