Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
73.31% covered (warning)
73.31%
173 / 236
42.11% covered (danger)
42.11%
8 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
NewsletterDb
73.31% covered (warning)
73.31%
173 / 236
42.11% covered (danger)
42.11%
8 / 19
51.48
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addSubscription
96.00% covered (success)
96.00%
24 / 25
0.00% covered (danger)
0.00%
0 / 1
4
 removeSubscription
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
2
 addPublisher
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
3.00
 removePublisher
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 addNewsletter
86.67% covered (warning)
86.67%
13 / 15
0.00% covered (danger)
0.00%
0 / 1
2.01
 updateDescription
88.24% covered (warning)
88.24%
15 / 17
0.00% covered (danger)
0.00%
0 / 1
2.01
 updateName
87.50% covered (warning)
87.50%
14 / 16
0.00% covered (danger)
0.00%
0 / 1
2.01
 updateMainPage
87.50% covered (warning)
87.50%
14 / 16
0.00% covered (danger)
0.00%
0 / 1
2.01
 deleteNewsletter
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 restoreNewsletter
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 getNewsletter
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
2.00
 getNewsletterFromName
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 getPublishersFromID
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 getNewsletterSubscribersCount
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 getSubscribersFromID
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 newsletterExistsForMainPage
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getNewsletterFromRow
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 addNewsletterIssue
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace MediaWiki\Extension\Newsletter;
4
5use MediaWiki\MediaWikiServices;
6use MediaWiki\Title\Title;
7use MediaWiki\User\User;
8use stdClass;
9use Wikimedia\Rdbms\DBQueryError;
10use Wikimedia\Rdbms\ILoadBalancer;
11use Wikimedia\Rdbms\IResultWrapper;
12
13/**
14 * @license GPL-2.0-or-later
15 * @author Addshore
16 */
17class NewsletterDb {
18
19    /**
20     * @var ILoadBalancer
21     */
22    private $lb;
23
24    public function __construct( ILoadBalancer $lb ) {
25        $this->lb = $lb;
26    }
27
28    /**
29     * @param Newsletter $newsletter
30     * @param array $userIds
31     */
32    public function addSubscription( Newsletter $newsletter, array $userIds ): void {
33        if ( !$userIds ) {
34            return;
35        }
36
37        $rowData = [];
38        foreach ( $userIds as $userId ) {
39            $rowData[] = [
40                'nls_newsletter_id' => $newsletter->getId(),
41                'nls_subscriber_id' => $userId
42            ];
43        }
44        $dbw = $this->lb->getConnection( DB_PRIMARY );
45        $dbw->startAtomic( __METHOD__ );
46
47        // Tolerate (silently ignore) if it was already there
48        $dbw->newInsertQueryBuilder()
49            ->insertInto( 'nl_subscriptions' )
50            ->ignore()
51            ->rows( $rowData )
52            ->caller( __METHOD__ )
53            ->execute();
54
55        // But only update the count if there was a change
56        if ( $dbw->affectedRows() ) {
57            $dbw->newUpdateQueryBuilder()
58                ->update( 'nl_newsletters' )
59                // For index reasons, count is negative
60                ->set( [ 'nl_subscriber_count=nl_subscriber_count-' . count( $userIds ) ] )
61                ->where( [ 'nl_id' => $newsletter->getId() ] )
62                ->caller( __METHOD__ )
63                ->execute();
64        }
65
66        $dbw->endAtomic( __METHOD__ );
67    }
68
69    /**
70     * @param Newsletter $newsletter
71     * @param array $userIds
72     */
73    public function removeSubscription( Newsletter $newsletter, array $userIds ): void {
74        $rowData = [
75            'nls_newsletter_id' => $newsletter->getId(),
76            'nls_subscriber_id' => $userIds
77        ];
78
79        $dbw = $this->lb->getConnection( DB_PRIMARY );
80        $dbw->startAtomic( __METHOD__ );
81
82        $dbw->newDeleteQueryBuilder()
83            ->deleteFrom( 'nl_subscriptions' )
84            ->where( $rowData )
85            ->caller( __METHOD__ )
86            ->execute();
87
88        // Delete query succeeds even if the row already gone
89        // But only update the count if there was a change
90        if ( $dbw->affectedRows() ) {
91            $dbw->newUpdateQueryBuilder()
92                ->update( 'nl_newsletters' )
93                // For index reasons, count is negative
94                ->set( [ 'nl_subscriber_count=nl_subscriber_count+' . count( $userIds ) ] )
95                ->where( [ 'nl_id' => $newsletter->getId() ] )
96                ->caller( __METHOD__ )
97                ->execute();
98        }
99
100        $dbw->endAtomic( __METHOD__ );
101    }
102
103    /**
104     * @param Newsletter $newsletter
105     * @param array $userIds
106     * @return bool Success of the action
107     */
108    public function addPublisher( Newsletter $newsletter, array $userIds ): bool {
109        if ( !$userIds ) {
110            return false;
111        }
112
113        $newsletterId = $newsletter->getId();
114        $rowData = [];
115        foreach ( $userIds as $userId ) {
116            $rowData[] = [
117                'nlp_newsletter_id' => $newsletterId,
118                'nlp_publisher_id' => $userId
119            ];
120        }
121
122        $dbw = $this->lb->getConnection( DB_PRIMARY );
123
124        // Let the user action appear success even if the row is already there.
125        $dbw->newInsertQueryBuilder()
126            ->insertInto( 'nl_publishers' )
127            ->ignore()
128            ->rows( $rowData )
129            ->caller( __METHOD__ )
130            ->execute();
131        // Provide a bool that reflects actual creation of a row,
132        // used for decide whether to create a matching MW log entry.
133        return (bool)$dbw->affectedRows();
134    }
135
136    /**
137     * @param Newsletter $newsletter
138     * @param array $userIds
139     * @return bool
140     */
141    public function removePublisher( Newsletter $newsletter, array $userIds ): bool {
142        $rowData = [
143            'nlp_newsletter_id' => $newsletter->getId(),
144            'nlp_publisher_id' => $userIds
145        ];
146
147        $dbw = $this->lb->getConnection( DB_PRIMARY );
148
149        $dbw->newDeleteQueryBuilder()
150            ->deleteFrom( 'nl_publishers' )
151            ->where( $rowData )
152            ->caller( __METHOD__ )
153            ->execute();
154
155        // Delete query succeeds even if the row was already gone.
156        // Provide a bool that reflects actual creation of a row,
157        // used for decide whether to create a matching MW log entry.
158        return (bool)$dbw->affectedRows();
159    }
160
161    /**
162     * @param Newsletter $newsletter
163     * @return int|bool The ID of the newsletter added, or false on failure
164     */
165    public function addNewsletter( Newsletter $newsletter ) {
166        $contLang = MediaWikiServices::getInstance()->getContentLanguage();
167        $rowData = [
168            'nl_name' => $newsletter->getName(),
169            // nl_newsletters.nl_desc is a blob but put some limit
170            // here which is less than the max size for blobs
171            'nl_desc' => $contLang->truncateForDatabase( $newsletter->getDescription(), 600000 ),
172            'nl_main_page_id' => $newsletter->getPageId(),
173        ];
174
175        $dbw = $this->lb->getConnection( DB_PRIMARY );
176        try {
177            $dbw->newInsertQueryBuilder()
178                ->insertInto( 'nl_newsletters' )
179                ->row( $rowData )
180                ->caller( __METHOD__ )
181                ->execute();
182            return $dbw->insertId();
183        } catch ( DBQueryError $ex ) {
184            return false;
185        }
186    }
187
188    /**
189     * @param int $id
190     * @param string $description
191     * @return bool Success of the action
192     */
193    public function updateDescription( int $id, string $description ): bool {
194        $contLang = MediaWikiServices::getInstance()->getContentLanguage();
195
196        $rowData = [
197            // nl_newsletters.nl_desc is a blob but put some limit
198            // here which is less than the max size for blobs
199            'nl_desc' => $contLang->truncateForDatabase( $description, 600000 ),
200        ];
201        $conds = [
202            'nl_id' => $id,
203        ];
204
205        $dbw = $this->lb->getConnection( DB_PRIMARY );
206        try {
207            $dbw->newUpdateQueryBuilder()
208                ->update( 'nl_newsletters' )
209                ->set( $rowData )
210                ->where( $conds )
211                ->caller( __METHOD__ )
212                ->execute();
213        } catch ( DBQueryError $ex ) {
214            return false;
215        }
216
217        return true;
218    }
219
220    /**
221     * @param int $id
222     * @param string $name
223     * @return bool Success of the action
224     */
225    public function updateName( int $id, string $name ): bool {
226        $rowData = [
227            'nl_name' => $name,
228        ];
229        $conds = [
230            'nl_id' => $id,
231        ];
232
233        $dbw = $this->lb->getConnection( DB_PRIMARY );
234        try {
235            $dbw->newUpdateQueryBuilder()
236                ->update( 'nl_newsletters' )
237                ->set( $rowData )
238                ->where( $conds )
239                ->caller( __METHOD__ )
240                ->execute();
241        } catch ( DBQueryError $ex ) {
242            return false;
243        }
244
245        return true;
246    }
247
248    /**
249     * @param int $id
250     * @param int $pageId
251     * @return bool Success of the action
252     */
253    public function updateMainPage( int $id, int $pageId ): bool {
254        $rowData = [
255            'nl_main_page_id' => $pageId,
256        ];
257        $conds = [
258            'nl_id' => $id,
259        ];
260
261        $dbw = $this->lb->getConnection( DB_PRIMARY );
262        try {
263            $dbw->newUpdateQueryBuilder()
264                ->update( 'nl_newsletters' )
265                ->set( $rowData )
266                ->where( $conds )
267                ->caller( __METHOD__ )
268                ->execute();
269        } catch ( DBQueryError $ex ) {
270            return false;
271        }
272
273        return true;
274    }
275
276    /**
277     * @param Newsletter $newsletter
278     */
279    public function deleteNewsletter( Newsletter $newsletter ): void {
280        $dbw = $this->lb->getConnection( DB_PRIMARY );
281        $dbw->newUpdateQueryBuilder()
282            ->update( 'nl_newsletters' )
283            ->set( [ 'nl_active' => 0 ] )
284            ->where( [ 'nl_id' => $newsletter->getId(), 'nl_active' => 1 ] )
285            ->caller( __METHOD__ )
286            ->execute();
287    }
288
289    /**
290     * Set an inactive newsletter to active again
291     *
292     * @param string $newsletterName
293     */
294    public function restoreNewsletter( string $newsletterName ): void {
295        $dbw = $this->lb->getConnection( DB_PRIMARY );
296        $dbw->newUpdateQueryBuilder()
297            ->update( 'nl_newsletters' )
298            ->set( [ 'nl_active' => 1 ] )
299            ->where( [ 'nl_name' => $newsletterName, 'nl_active' => 0 ] )
300            ->caller( __METHOD__ )
301            ->execute();
302    }
303
304    /**
305     * @param int $id
306     * @return Newsletter|null null if no newsletter exists with the provided id
307     */
308    public function getNewsletter( int $id ) {
309        $dbr = $this->lb->getConnection( DB_REPLICA );
310        $res = $dbr->select(
311            'nl_newsletters',
312            [ 'nl_id', 'nl_name', 'nl_desc', 'nl_main_page_id' ],
313            [ 'nl_id' => $id, 'nl_active' => 1 ],
314            __METHOD__
315        );
316
317        if ( $res->numRows() === 0 ) {
318            return null;
319        }
320
321        return $this->getNewsletterFromRow( $res->current() );
322    }
323
324    /**
325     * Fetch the newsletter matching the given name from the DB
326     *
327     * @param string $name
328     * @param bool $active
329     * @return Newsletter|null
330     */
331    public function getNewsletterFromName( string $name, bool $active = true ) {
332        $dbr = $this->lb->getConnection( DB_REPLICA );
333        $res = $dbr->selectRow(
334            'nl_newsletters',
335            [ 'nl_id', 'nl_name', 'nl_desc', 'nl_main_page_id' ],
336            [ 'nl_name' => $name, 'nl_active' => $active ],
337            __METHOD__
338        );
339
340        return $res ? $this->getNewsletterFromRow( $res ) : null;
341    }
342
343    /**
344     * @param int $id
345     * @return int[]
346     */
347    public function getPublishersFromID( int $id ): array {
348        $dbr = $this->lb->getConnection( DB_REPLICA );
349        $result = $dbr->selectFieldValues(
350            'nl_publishers',
351            'nlp_publisher_id',
352            [ 'nlp_newsletter_id' => $id ],
353            __METHOD__
354        );
355
356        return array_map( 'intval', $result );
357    }
358
359    /**
360     * @param int $id
361     * @return int
362     */
363    public function getNewsletterSubscribersCount( int $id ): int {
364        $dbr = $this->lb->getConnection( DB_REPLICA );
365        $result = $dbr->selectField(
366            'nl_newsletters',
367            'nl_subscriber_count',
368            [ 'nl_id' => $id ],
369            __METHOD__
370        );
371
372        // We store nl_subscriber_count as negative numbers so that sorting should work on one
373        // direction
374        return -(int)$result;
375    }
376
377    /**
378     * @param int $id
379     * @return int[]
380     */
381    public function getSubscribersFromID( int $id ): array {
382        $dbr = $this->lb->getConnection( DB_REPLICA );
383        $result = $dbr->selectFieldValues(
384            'nl_subscriptions',
385            'nls_subscriber_id',
386            [ 'nls_newsletter_id' => $id ],
387            __METHOD__
388        );
389
390        return array_map( 'intval', $result );
391    }
392
393    /**
394     * Fetch all newsletter Main Pages
395     *
396     * @param int $mainPageId
397     * @return IResultWrapper
398     */
399    public function newsletterExistsForMainPage( int $mainPageId ) {
400        $dbr = $this->lb->getConnection( DB_REPLICA );
401        return $dbr->select(
402            'nl_newsletters',
403            [ 'nl_main_page_id', 'nl_active' ],
404            [ 'nl_main_page_id' => $mainPageId ],
405            __METHOD__
406        );
407    }
408
409    /**
410     * @param stdClass $row
411     * @return Newsletter
412     */
413    private function getNewsletterFromRow( $row ): Newsletter {
414        return new Newsletter(
415            $row->nl_id,
416            $row->nl_name,
417            $row->nl_desc,
418            $row->nl_main_page_id
419        );
420    }
421
422    /**
423     * @param Newsletter $newsletter
424     * @param Title $title
425     * @param User $publisher
426     * @return bool|int the id of the issue added, false on failure
427     */
428    public function addNewsletterIssue( Newsletter $newsletter, Title $title, User $publisher ) {
429        // Note: the writeDb is used as this is used in the next insert
430        $dbw = $this->lb->getConnection( DB_PRIMARY );
431        $dbw->startAtomic( __METHOD__ );
432
433        $dbw->lockForUpdate( 'nl_newsletters', [ 'nl_id' => $newsletter->getId() ], __METHOD__ );
434        $lastIssueId = (int)$dbw->selectField(
435            'nl_issues',
436            'nli_issue_id',
437            [ 'nli_newsletter_id' => $newsletter->getId() ],
438            __METHOD__,
439            [
440                'ORDER BY' => 'nli_issue_id DESC',
441                'FOR UPDATE'
442            ]
443        );
444        $nextIssueId = $lastIssueId + 1;
445
446        try {
447            $dbw->newInsertQueryBuilder()
448                ->insertInto( 'nl_issues' )
449                ->row( [
450                    'nli_issue_id' => $nextIssueId,
451                    'nli_page_id' => $title->getArticleID(),
452                    'nli_newsletter_id' => $newsletter->getId(),
453                    'nli_publisher_id' => $publisher->getId(),
454                ] )
455                ->caller( __METHOD__ )
456                ->execute();
457            $dbw->endAtomic( __METHOD__ );
458        } catch ( DBQueryError $ex ) {
459            $dbw->rollback( __METHOD__ );
460            return false;
461        }
462
463        return $nextIssueId;
464    }
465
466}