Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
98.00% |
49 / 50 |
|
75.00% |
3 / 4 |
CRAP | |
0.00% |
0 / 1 |
SubscriptionManager | |
98.00% |
49 / 50 |
|
75.00% |
3 / 4 |
10 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
create | |
95.65% |
22 / 23 |
|
0.00% |
0 / 1 |
5 | |||
getSubscriptionsForUser | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
2 | |||
delete | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Notifications\Push; |
4 | |
5 | use MediaWiki\Extension\Notifications\Mapper\AbstractMapper; |
6 | use MediaWiki\Storage\NameTableStore; |
7 | use Wikimedia\Rdbms\DBError; |
8 | use Wikimedia\Rdbms\IDatabase; |
9 | |
10 | class SubscriptionManager extends AbstractMapper { |
11 | |
12 | /** @var IDatabase */ |
13 | private $dbw; |
14 | |
15 | /** @var IDatabase */ |
16 | private $dbr; |
17 | |
18 | /** @var NameTableStore */ |
19 | private $pushProviderStore; |
20 | |
21 | /** @var NameTableStore */ |
22 | private $pushTopicStore; |
23 | |
24 | /** @var int */ |
25 | private $maxSubscriptionsPerUser; |
26 | |
27 | /** |
28 | * @param IDatabase $dbw primary DB connection (for writes) |
29 | * @param IDatabase $dbr replica DB connection (for reads) |
30 | * @param NameTableStore $pushProviderStore |
31 | * @param NameTableStore $pushTopicStore |
32 | * @param int $maxSubscriptionsPerUser |
33 | */ |
34 | public function __construct( |
35 | IDatabase $dbw, |
36 | IDatabase $dbr, |
37 | NameTableStore $pushProviderStore, |
38 | NameTableStore $pushTopicStore, |
39 | int $maxSubscriptionsPerUser |
40 | ) { |
41 | parent::__construct(); |
42 | $this->dbw = $dbw; |
43 | $this->dbr = $dbr; |
44 | $this->pushProviderStore = $pushProviderStore; |
45 | $this->pushTopicStore = $pushTopicStore; |
46 | $this->maxSubscriptionsPerUser = $maxSubscriptionsPerUser; |
47 | } |
48 | |
49 | /** |
50 | * Store push subscription information for a central user ID. |
51 | * @param string $provider Provider name string (validated by presence in the PARAM_TYPE array) |
52 | * @param string $token Subscriber token provided by the push provider |
53 | * @param int $centralId |
54 | * @param string|null $topic APNS topic string |
55 | * @return bool true if the subscription was created; false if the token already exists |
56 | */ |
57 | public function create( string $provider, string $token, int $centralId, ?string $topic = null ): bool { |
58 | $subscriptions = $this->getSubscriptionsForUser( $centralId ); |
59 | if ( count( $subscriptions ) >= $this->maxSubscriptionsPerUser ) { |
60 | // If we exceed the number of subscriptions for this user, then delete the oldest subscription |
61 | // before inserting the new one, making it behave like a circular buffer. |
62 | // (Find the oldest subscription by iterating, since their order in the DB is not guaranteed.) |
63 | $oldest = $subscriptions[0]; |
64 | foreach ( $subscriptions as $subscription ) { |
65 | if ( $subscription->getUpdated() < $oldest->getUpdated() ) { |
66 | $oldest = $subscription; |
67 | } |
68 | } |
69 | $this->delete( [ $oldest->getToken() ], $centralId ); |
70 | } |
71 | $topicId = $topic ? $this->pushTopicStore->acquireId( $topic ) : null; |
72 | $this->dbw->newInsertQueryBuilder() |
73 | ->insertInto( 'echo_push_subscription' ) |
74 | ->ignore() |
75 | ->row( [ |
76 | 'eps_user' => $centralId, |
77 | 'eps_provider' => $this->pushProviderStore->acquireId( $provider ), |
78 | 'eps_token' => $token, |
79 | 'eps_token_sha256' => hash( 'sha256', $token ), |
80 | 'eps_data' => null, |
81 | 'eps_topic' => $topicId, |
82 | 'eps_updated' => $this->dbw->timestamp() |
83 | ] ) |
84 | ->caller( __METHOD__ ) |
85 | ->execute(); |
86 | return (bool)$this->dbw->affectedRows(); |
87 | } |
88 | |
89 | /** |
90 | * Get full data for all registered subscriptions for a user (by central ID). |
91 | * @param int $centralId |
92 | * @return array array of Subscription objects |
93 | */ |
94 | public function getSubscriptionsForUser( int $centralId ) { |
95 | $res = $this->dbr->newSelectQueryBuilder() |
96 | ->select( '*' ) |
97 | ->from( 'echo_push_subscription' ) |
98 | ->join( 'echo_push_provider', null, 'eps_provider = epp_id' ) |
99 | ->leftJoin( 'echo_push_topic', null, 'eps_topic = ept_id' ) |
100 | ->where( [ 'eps_user' => $centralId ] ) |
101 | ->caller( __METHOD__ ) |
102 | ->fetchResultSet(); |
103 | $result = []; |
104 | foreach ( $res as $row ) { |
105 | $result[] = Subscription::newFromRow( $row ); |
106 | } |
107 | return $result; |
108 | } |
109 | |
110 | /** |
111 | * Delete one or more push subscriptions by token. Unless the current user is a push |
112 | * subscription manager, the query will also include the current central user ID as a condition. |
113 | * @param array $tokens Delete the subscription with these tokens |
114 | * @param int|null $centralId |
115 | * @return int number of rows deleted |
116 | * @throws DBError |
117 | */ |
118 | public function delete( array $tokens, int $centralId = null ): int { |
119 | $cond = [ 'eps_token' => $tokens ]; |
120 | if ( $centralId ) { |
121 | $cond['eps_user'] = $centralId; |
122 | } |
123 | $this->dbw->newDeleteQueryBuilder() |
124 | ->deleteFrom( 'echo_push_subscription' ) |
125 | ->where( $cond ) |
126 | ->caller( __METHOD__ ) |
127 | ->execute(); |
128 | return $this->dbw->affectedRows(); |
129 | } |
130 | |
131 | } |