Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
73.31% |
173 / 236 |
|
42.11% |
8 / 19 |
CRAP | |
0.00% |
0 / 1 |
NewsletterDb | |
73.31% |
173 / 236 |
|
42.11% |
8 / 19 |
51.48 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addSubscription | |
96.00% |
24 / 25 |
|
0.00% |
0 / 1 |
4 | |||
removeSubscription | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
2 | |||
addPublisher | |
94.12% |
16 / 17 |
|
0.00% |
0 / 1 |
3.00 | |||
removePublisher | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
1 | |||
addNewsletter | |
86.67% |
13 / 15 |
|
0.00% |
0 / 1 |
2.01 | |||
updateDescription | |
88.24% |
15 / 17 |
|
0.00% |
0 / 1 |
2.01 | |||
updateName | |
87.50% |
14 / 16 |
|
0.00% |
0 / 1 |
2.01 | |||
updateMainPage | |
87.50% |
14 / 16 |
|
0.00% |
0 / 1 |
2.01 | |||
deleteNewsletter | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
restoreNewsletter | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
getNewsletter | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
2.00 | |||
getNewsletterFromName | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
getPublishersFromID | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
getNewsletterSubscribersCount | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
getSubscribersFromID | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
newsletterExistsForMainPage | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
getNewsletterFromRow | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
addNewsletterIssue | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Newsletter; |
4 | |
5 | use MediaWiki\MediaWikiServices; |
6 | use MediaWiki\Title\Title; |
7 | use MediaWiki\User\User; |
8 | use stdClass; |
9 | use Wikimedia\Rdbms\DBQueryError; |
10 | use Wikimedia\Rdbms\ILoadBalancer; |
11 | use Wikimedia\Rdbms\IResultWrapper; |
12 | |
13 | /** |
14 | * @license GPL-2.0-or-later |
15 | * @author Addshore |
16 | */ |
17 | class 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 | } |