Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
43.85% covered (danger)
43.85%
57 / 130
28.57% covered (danger)
28.57%
2 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
EventMapper
43.85% covered (danger)
43.85%
57 / 130
28.57% covered (danger)
28.57%
2 / 7
135.67
0.00% covered (danger)
0.00%
0 / 1
 insert
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
2.00
 fetchById
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
6.02
 toggleDeleted
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
 fetchByPage
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
4
 fetchIdsByPage
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 fetchUnreadByUserAndPage
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
6
 deleteOrphanedEvents
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
56
1<?php
2
3namespace MediaWiki\Extension\Notifications\Mapper;
4
5use InvalidArgumentException;
6use MediaWiki\Extension\Notifications\Model\Event;
7use MediaWiki\User\User;
8
9/**
10 * Database mapper for Event model, which is an immutable class, there should
11 * not be any update to it
12 */
13class EventMapper extends AbstractMapper {
14
15    /**
16     * Insert an event record
17     *
18     * @param Event $event
19     * @return int
20     */
21    public function insert( Event $event ) {
22        $dbw = $this->dbFactory->getEchoDb( DB_PRIMARY );
23
24        $row = $event->toDbArray();
25
26        $dbw->newInsertQueryBuilder()
27            ->insertInto( 'echo_event' )
28            ->row( $row )
29            ->caller( __METHOD__ )
30            ->execute();
31
32        $id = $dbw->insertId();
33
34        $listeners = $this->getMethodListeners( __FUNCTION__ );
35        foreach ( $listeners as $listener ) {
36            $dbw->onTransactionCommitOrIdle( $listener, __METHOD__ );
37        }
38
39        return $id;
40    }
41
42    /**
43     * Create an Event by id
44     *
45     * @param int $id
46     * @param bool $fromPrimary
47     * @return Event|false False if it wouldn't load/unserialize
48     */
49    public function fetchById( $id, $fromPrimary = false ) {
50        $db = $fromPrimary ? $this->dbFactory->getEchoDb( DB_PRIMARY ) : $this->dbFactory->getEchoDb( DB_REPLICA );
51
52        $row = $db->newSelectQueryBuilder()
53            ->select( Event::selectFields() )
54            ->from( 'echo_event' )
55            ->where( [ 'event_id' => $id ] )
56            ->caller( __METHOD__ )
57            ->fetchRow();
58
59        // If the row was not found, fall back on the primary database if it makes sense to do so
60        if ( !$row && !$fromPrimary && $this->dbFactory->canRetryPrimary() ) {
61            return $this->fetchById( $id, true );
62        } elseif ( !$row ) {
63            throw new InvalidArgumentException( "No Event found with ID: $id" );
64        }
65
66        return Event::newFromRow( $row );
67    }
68
69    /**
70     * @param int[] $eventIds
71     * @param bool $deleted
72     */
73    public function toggleDeleted( array $eventIds, $deleted ) {
74        $dbw = $this->dbFactory->getEchoDb( DB_PRIMARY );
75
76        $selectDeleted = $deleted ? 0 : 1;
77        $setDeleted = $deleted ? 1 : 0;
78        $dbw->newUpdateQueryBuilder()
79            ->update( 'echo_event' )
80            ->set( [
81                'event_deleted' => $setDeleted,
82            ] )
83            ->where( [
84                'event_deleted' => $selectDeleted,
85                'event_id' => $eventIds,
86            ] )
87            ->caller( __METHOD__ )
88            ->execute();
89    }
90
91    /**
92     * Fetch events associated with a page
93     *
94     * @param int $pageId
95     * @return Event[] Events
96     */
97    public function fetchByPage( $pageId ) {
98        $events = [];
99        $seenEventIds = [];
100        $dbr = $this->dbFactory->getEchoDb( DB_REPLICA );
101
102        // From echo_event
103        $res = $dbr->newSelectQueryBuilder()
104            ->select( Event::selectFields() )
105            ->from( 'echo_event' )
106            ->where( [ 'event_page_id' => $pageId ] )
107            ->caller( __METHOD__ )
108            ->fetchResultSet();
109        foreach ( $res as $row ) {
110            $event = Event::newFromRow( $row );
111            $events[] = $event;
112            $seenEventIds[] = $event->getId();
113        }
114
115        // From echo_target_page
116        $conds = [ 'etp_page' => $pageId ];
117        if ( $seenEventIds ) {
118            // Some events have both a title and target page(s).
119            // Skip the events that were already found in the echo_event table (the query above).
120            $conds[] = $dbr->expr( 'event_id', '!=', $seenEventIds );
121        }
122        $res = $dbr->newSelectQueryBuilder()
123            ->select( Event::selectFields() )
124            ->distinct()
125            ->from( 'echo_event' )
126            ->join( 'echo_target_page', null, 'event_id=etp_event' )
127            ->where( $conds )
128            ->caller( __METHOD__ )
129            ->fetchResultSet();
130        foreach ( $res as $row ) {
131            $events[] = Event::newFromRow( $row );
132        }
133
134        return $events;
135    }
136
137    /**
138     * Fetch event IDs associated with a page
139     *
140     * @param int $pageId
141     * @return int[] Event IDs
142     */
143    public function fetchIdsByPage( $pageId ) {
144        $events = $this->fetchByPage( $pageId );
145        $eventIds = array_map(
146            static function ( Event $event ) {
147                return $event->getId();
148            },
149            $events
150        );
151        return $eventIds;
152    }
153
154    /**
155     * Fetch events unread by a user and associated with a page
156     *
157     * @param User $user
158     * @param int $pageId
159     * @return Event[]
160     */
161    public function fetchUnreadByUserAndPage( User $user, $pageId ) {
162        $dbr = $this->dbFactory->getEchoDb( DB_REPLICA );
163        $fields = array_merge( Event::selectFields(), [ 'notification_timestamp' ] );
164
165        $res = $dbr->newSelectQueryBuilder()
166            ->select( $fields )
167            ->from( 'echo_event' )
168            ->join( 'echo_notification', null, 'notification_event=event_id' )
169            ->join( 'echo_target_page', null, 'etp_event=event_id' )
170            ->where( [
171                'event_deleted' => 0,
172                'notification_user' => $user->getId(),
173                'notification_read_timestamp' => null,
174                'etp_page' => $pageId,
175            ] )
176            ->caller( __METHOD__ )
177            ->fetchResultSet();
178
179        $data = [];
180        foreach ( $res as $row ) {
181            $data[] = Event::newFromRow( $row );
182        }
183
184        return $data;
185    }
186
187    /**
188     * Find out which of the given event IDs are orphaned, and delete them.
189     *
190     * An event is orphaned if it is not referred to by any rows in the echo_notification or
191     * echo_email_batch tables. If $ignoreUserId is set, rows for that user are not considered when
192     * determining orphanhood; if $ignoreUserTable is set, this only applies to that table.
193     * Use this when you've just recently deleted rows related to this user on the primary database, so that
194     * this function won't refuse to delete recently-orphaned events because it still sees the
195     * recently-deleted rows on the replica.
196     *
197     * @param array $eventIds Event IDs to check to see if they have become orphaned
198     * @param int|null $ignoreUserId Allow events to be deleted if the only referring rows
199     *  have this user ID
200     * @param string|null $ignoreUserTable Restrict $ignoreUserId to this table only
201     *  ('echo_notification' or 'echo_email_batch')
202     */
203    public function deleteOrphanedEvents( array $eventIds, $ignoreUserId = null, $ignoreUserTable = null ) {
204        $dbw = $this->dbFactory->getEchoDb( DB_PRIMARY );
205        $dbr = $this->dbFactory->getEchoDb( DB_REPLICA );
206
207        $notifJoinConds = [];
208        $emailJoinConds = [];
209        if ( $ignoreUserId !== null ) {
210            if ( $ignoreUserTable === null || $ignoreUserTable === 'echo_notification' ) {
211                $notifJoinConds[] = $dbr->expr( 'notification_user', '!=', $ignoreUserId );
212            }
213            if ( $ignoreUserTable === null || $ignoreUserTable === 'echo_email_batch' ) {
214                $emailJoinConds[] = $dbr->expr( 'eeb_user_id', '!=', $ignoreUserId );
215            }
216        }
217        $orphanedEventIds = $dbr->newSelectQueryBuilder()
218            ->select( 'event_id' )
219            ->from( 'echo_event' )
220            ->leftJoin( 'echo_notification', null, array_merge(
221                [ 'notification_event=event_id' ],
222                $notifJoinConds
223            ) )
224            ->leftJoin( 'echo_email_batch', null, array_merge(
225                [ 'eeb_event_id=event_id' ],
226                $emailJoinConds
227            ) )
228            ->where( [
229                'event_id' => $eventIds,
230                'notification_timestamp' => null,
231                'eeb_user_id' => null
232            ] )
233            ->caller( __METHOD__ )
234            ->fetchFieldValues();
235        if ( $orphanedEventIds ) {
236            $dbw->newDeleteQueryBuilder()
237                ->deleteFrom( 'echo_event' )
238                ->where( [ 'event_id' => $orphanedEventIds ] )
239                ->caller( __METHOD__ )
240                ->execute();
241            $dbw->newDeleteQueryBuilder()
242                ->deleteFrom( 'echo_target_page' )
243                ->where( [ 'etp_event' => $orphanedEventIds ] )
244                ->caller( __METHOD__ )
245                ->execute();
246        }
247    }
248
249}