Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
71.60% |
58 / 81 |
|
50.00% |
3 / 6 |
CRAP | |
0.00% |
0 / 1 |
BounceHandlerActions | |
71.60% |
58 / 81 |
|
50.00% |
3 / 6 |
16.87 | |
0.00% |
0 / 1 |
__construct | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
2.01 | |||
handleFailingRecipient | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
3 | |||
createEchoNotification | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
notifyGlobalUser | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
2 | |||
unSubscribeUser | |
64.29% |
18 / 28 |
|
0.00% |
0 / 1 |
4.73 | |||
formatHeaders | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\BounceHandler; |
4 | |
5 | use EchoEvent; |
6 | use Exception; |
7 | use ExtensionRegistry; |
8 | use MediaWiki\Extension\CentralAuth\User\CentralAuthUser; |
9 | use MediaWiki\MediaWikiServices; |
10 | use MediaWiki\Title\Title; |
11 | use MediaWiki\WikiMap\WikiMap; |
12 | use User; |
13 | |
14 | /** |
15 | * Class BounceHandlerActions |
16 | * |
17 | * Actions to be done on finding out a failing recipient |
18 | * |
19 | * @file |
20 | * @ingroup Extensions |
21 | * @author Tony Thomas, Kunal Mehta, Jeff Green |
22 | * @license GPL-2.0-or-later |
23 | */ |
24 | class BounceHandlerActions { |
25 | |
26 | /** |
27 | * @var string |
28 | */ |
29 | protected $wikiId; |
30 | |
31 | /** |
32 | * @var int |
33 | */ |
34 | protected $bounceRecordPeriod; |
35 | |
36 | /** |
37 | * @var int |
38 | */ |
39 | protected $bounceRecordLimit; |
40 | |
41 | /** |
42 | * @var bool |
43 | */ |
44 | protected $bounceHandlerUnconfirmUsers; |
45 | |
46 | /** |
47 | * @var string |
48 | */ |
49 | protected $emailRaw; |
50 | |
51 | /** |
52 | * @param string $wikiId The database id of the failing recipient |
53 | * @param int $bounceRecordPeriod Time period for which bounce activities are considered |
54 | * before un-subscribing |
55 | * @param int $bounceRecordLimit The number of bounce allowed in the bounceRecordPeriod. |
56 | * @param bool $bounceHandlerUnconfirmUsers Enable/Disable user un-subscribe action |
57 | * @param string $emailRaw The raw bounce Email |
58 | * @throws Exception |
59 | */ |
60 | public function __construct( |
61 | $wikiId, $bounceRecordPeriod, $bounceRecordLimit, $bounceHandlerUnconfirmUsers, $emailRaw |
62 | ) { |
63 | if ( $wikiId !== WikiMap::getCurrentWikiId() ) { |
64 | // We want to use the User class methods, which make no sense on the wrong wiki |
65 | throw new Exception( "BounceHandlerActions constructed for a foreign wiki." ); |
66 | } |
67 | |
68 | $this->wikiId = $wikiId; |
69 | $this->bounceRecordPeriod = $bounceRecordPeriod; |
70 | $this->bounceRecordLimit = $bounceRecordLimit; |
71 | $this->bounceHandlerUnconfirmUsers = $bounceHandlerUnconfirmUsers; |
72 | $this->emailRaw = $emailRaw; |
73 | } |
74 | |
75 | /** |
76 | * Perform actions on users who failed to receive emails in a given period |
77 | * |
78 | * @param array $failedUser The details of the failing user |
79 | * @param array $emailHeaders Email headers |
80 | * @return bool |
81 | */ |
82 | public function handleFailingRecipient( array $failedUser, $emailHeaders ) { |
83 | if ( $this->bounceHandlerUnconfirmUsers ) { |
84 | $originalEmail = $failedUser['rawEmail']; |
85 | $bounceValidPeriod = time() - $this->bounceRecordPeriod; // Unix |
86 | |
87 | $dbr = ProcessBounceEmails::getBounceRecordDB( DB_REPLICA, $this->wikiId ); |
88 | |
89 | $totalBounces = $dbr->selectRowCount( 'bounce_records', |
90 | '*', |
91 | [ |
92 | 'br_user_email' => $originalEmail, |
93 | 'br_timestamp >= ' . $dbr->addQuotes( $dbr->timestamp( $bounceValidPeriod ) ) |
94 | ], |
95 | __METHOD__, |
96 | [ 'LIMIT' => $this->bounceRecordLimit ] |
97 | ); |
98 | |
99 | if ( $totalBounces >= $this->bounceRecordLimit ) { |
100 | $this->unSubscribeUser( $failedUser, $emailHeaders ); |
101 | } |
102 | } |
103 | |
104 | return true; |
105 | } |
106 | |
107 | /** |
108 | * Function to trigger Echo notifications |
109 | * |
110 | * @param int $userId ID of user to be notified |
111 | * @param string $email un-subscribed email address used in notification |
112 | */ |
113 | public function createEchoNotification( $userId, $email ) { |
114 | if ( ExtensionRegistry::getInstance()->isLoaded( 'Echo' ) ) { |
115 | EchoEvent::create( [ |
116 | 'type' => 'unsubscribe-bouncehandler', |
117 | 'extra' => [ |
118 | 'failed-user-id' => $userId, |
119 | 'failed-email' => $email, |
120 | ], |
121 | ] ); |
122 | } |
123 | } |
124 | |
125 | /** |
126 | * Function to inject Echo notification to the last source of bounce for an |
127 | * unsubscribed Global user |
128 | * |
129 | * @param int $bounceUserId |
130 | * @param string $originalEmail |
131 | */ |
132 | public function notifyGlobalUser( $bounceUserId, $originalEmail ) { |
133 | $params = [ |
134 | 'failed-user-id' => $bounceUserId, |
135 | 'failed-email' => $originalEmail, |
136 | 'wikiId' => $this->wikiId, |
137 | 'bounceRecordPeriod' => $this->bounceRecordPeriod, |
138 | 'bounceRecordLimit' => $this->bounceRecordLimit, |
139 | 'bounceHandlerUnconfirmUsers' => $this->bounceHandlerUnconfirmUsers, |
140 | 'emailRaw' => $this->emailRaw, |
141 | ]; |
142 | $title = Title::newFromText( 'BounceHandler Global user notification Job' ); |
143 | $job = new BounceHandlerNotificationJob( $title, $params ); |
144 | MediaWikiServices::getInstance()->getJobQueueGroupFactory()->makeJobQueueGroup( $this->wikiId )->push( $job ); |
145 | } |
146 | |
147 | /** |
148 | * Function to Un-subscribe a failing recipient |
149 | * |
150 | * @param array $failedUser The details of the failing user |
151 | * @param array $emailHeaders Email headers |
152 | */ |
153 | public function unSubscribeUser( array $failedUser, $emailHeaders ) { |
154 | // Un-subscribe the user |
155 | $originalEmail = $failedUser['rawEmail']; |
156 | $bounceUserId = $failedUser['rawUserId']; |
157 | |
158 | $user = User::newFromId( $bounceUserId ); |
159 | $stats = \MediaWiki\MediaWikiServices::getInstance()->getStatsdDataFactory(); |
160 | // Handle the central account email status (if applicable) |
161 | $unsubscribeLocalUser = true; |
162 | if ( ExtensionRegistry::getInstance()->isLoaded( 'CentralAuth' ) ) { |
163 | $caUser = CentralAuthUser::getPrimaryInstance( $user ); |
164 | if ( $caUser->isAttached() ) { |
165 | $unsubscribeLocalUser = false; |
166 | $caUser->setEmailAuthenticationTimestamp( null ); |
167 | $caUser->saveSettings(); |
168 | $this->notifyGlobalUser( $bounceUserId, $originalEmail ); |
169 | wfDebugLog( 'BounceHandler', |
170 | "Un-subscribed global user {$caUser->getName()} <$originalEmail> for " . |
171 | "exceeding Bounce Limit $this->bounceRecordLimit.\nProcessed Headers:\n" . |
172 | $this->formatHeaders( $emailHeaders ) . "\nBounced Email: \n$this->emailRaw" |
173 | ); |
174 | $stats->increment( 'bouncehandler.unsub.global' ); |
175 | } |
176 | } |
177 | if ( $unsubscribeLocalUser ) { |
178 | // Invalidate the email-id of a local user |
179 | $user->setEmailAuthenticationTimestamp( null ); |
180 | $user->saveSettings(); |
181 | $this->createEchoNotification( $bounceUserId, $originalEmail ); |
182 | wfDebugLog( 'BounceHandler', |
183 | "Un-subscribed {$user->getName()} <$originalEmail> for exceeding Bounce limit " . |
184 | "$this->bounceRecordLimit.\nProcessed Headers:\n" . |
185 | $this->formatHeaders( $emailHeaders ) . "\nBounced Email: \n$this->emailRaw" |
186 | ); |
187 | $stats->increment( 'bouncehandler.unsub.local' ); |
188 | } |
189 | } |
190 | |
191 | /** |
192 | * Turns a keyed array into "Key: Value" newline split string |
193 | * |
194 | * @param array $emailHeaders |
195 | * @return string |
196 | */ |
197 | private function formatHeaders( $emailHeaders ) { |
198 | return implode( |
199 | "\n", |
200 | array_map( |
201 | static function ( $v, $k ) { |
202 | return "$k: $v"; |
203 | }, |
204 | $emailHeaders, |
205 | array_keys( $emailHeaders ) |
206 | ) |
207 | ); |
208 | } |
209 | |
210 | } |