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 ExtensionRegistry; |
7 | use InvalidArgumentException; |
8 | use MediaWiki\Extension\CentralAuth\User\CentralAuthUser; |
9 | use MediaWiki\MediaWikiServices; |
10 | use MediaWiki\Title\Title; |
11 | use MediaWiki\User\User; |
12 | use MediaWiki\WikiMap\WikiMap; |
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 | */ |
59 | public function __construct( |
60 | $wikiId, $bounceRecordPeriod, $bounceRecordLimit, $bounceHandlerUnconfirmUsers, $emailRaw |
61 | ) { |
62 | if ( $wikiId !== WikiMap::getCurrentWikiId() ) { |
63 | // We want to use the User class methods, which make no sense on the wrong wiki |
64 | throw new InvalidArgumentException( "BounceHandlerActions constructed for a foreign wiki." ); |
65 | } |
66 | |
67 | $this->wikiId = $wikiId; |
68 | $this->bounceRecordPeriod = $bounceRecordPeriod; |
69 | $this->bounceRecordLimit = $bounceRecordLimit; |
70 | $this->bounceHandlerUnconfirmUsers = $bounceHandlerUnconfirmUsers; |
71 | $this->emailRaw = $emailRaw; |
72 | } |
73 | |
74 | /** |
75 | * Perform actions on users who failed to receive emails in a given period |
76 | * |
77 | * @param array $failedUser The details of the failing user |
78 | * @param array $emailHeaders Email headers |
79 | * @return bool |
80 | */ |
81 | public function handleFailingRecipient( array $failedUser, $emailHeaders ) { |
82 | if ( $this->bounceHandlerUnconfirmUsers ) { |
83 | $originalEmail = $failedUser['rawEmail']; |
84 | $bounceValidPeriod = time() - $this->bounceRecordPeriod; // Unix |
85 | |
86 | $dbr = ProcessBounceEmails::getBounceRecordDB( DB_REPLICA, $this->wikiId ); |
87 | |
88 | $totalBounces = $dbr->newSelectQueryBuilder() |
89 | ->select( '*' ) |
90 | ->from( 'bounce_records' ) |
91 | ->where( [ |
92 | 'br_user_email' => $originalEmail, |
93 | $dbr->expr( 'br_timestamp', '>=', $dbr->timestamp( $bounceValidPeriod ) ) |
94 | ] ) |
95 | ->limit( $this->bounceRecordLimit ) |
96 | ->caller( __METHOD__ )->fetchRowCount(); |
97 | |
98 | if ( $totalBounces >= $this->bounceRecordLimit ) { |
99 | $this->unSubscribeUser( $failedUser, $emailHeaders ); |
100 | } |
101 | } |
102 | |
103 | return true; |
104 | } |
105 | |
106 | /** |
107 | * Function to trigger Echo notifications |
108 | * |
109 | * @param int $userId ID of user to be notified |
110 | * @param string $email un-subscribed email address used in notification |
111 | */ |
112 | public function createEchoNotification( $userId, $email ) { |
113 | if ( ExtensionRegistry::getInstance()->isLoaded( 'Echo' ) ) { |
114 | EchoEvent::create( [ |
115 | 'type' => 'unsubscribe-bouncehandler', |
116 | 'extra' => [ |
117 | 'failed-user-id' => $userId, |
118 | 'failed-email' => $email, |
119 | ], |
120 | ] ); |
121 | } |
122 | } |
123 | |
124 | /** |
125 | * Function to inject Echo notification to the last source of bounce for an |
126 | * unsubscribed Global user |
127 | * |
128 | * @param int $bounceUserId |
129 | * @param string $originalEmail |
130 | */ |
131 | public function notifyGlobalUser( $bounceUserId, $originalEmail ) { |
132 | $params = [ |
133 | 'failed-user-id' => $bounceUserId, |
134 | 'failed-email' => $originalEmail, |
135 | 'wikiId' => $this->wikiId, |
136 | 'bounceRecordPeriod' => $this->bounceRecordPeriod, |
137 | 'bounceRecordLimit' => $this->bounceRecordLimit, |
138 | 'bounceHandlerUnconfirmUsers' => $this->bounceHandlerUnconfirmUsers, |
139 | 'emailRaw' => $this->emailRaw, |
140 | ]; |
141 | $title = Title::newFromText( 'BounceHandler Global user notification Job' ); |
142 | $job = new BounceHandlerNotificationJob( $title, $params ); |
143 | MediaWikiServices::getInstance()->getJobQueueGroupFactory()->makeJobQueueGroup( $this->wikiId )->push( $job ); |
144 | } |
145 | |
146 | /** |
147 | * Function to Un-subscribe a failing recipient |
148 | * |
149 | * @param array $failedUser The details of the failing user |
150 | * @param array $emailHeaders Email headers |
151 | */ |
152 | public function unSubscribeUser( array $failedUser, $emailHeaders ) { |
153 | // Un-subscribe the user |
154 | $originalEmail = $failedUser['rawEmail']; |
155 | $bounceUserId = $failedUser['rawUserId']; |
156 | |
157 | $user = User::newFromId( $bounceUserId ); |
158 | $stats = \MediaWiki\MediaWikiServices::getInstance()->getStatsdDataFactory(); |
159 | // Handle the central account email status (if applicable) |
160 | $unsubscribeLocalUser = true; |
161 | if ( ExtensionRegistry::getInstance()->isLoaded( 'CentralAuth' ) ) { |
162 | $caUser = CentralAuthUser::getPrimaryInstance( $user ); |
163 | if ( $caUser->isAttached() ) { |
164 | $unsubscribeLocalUser = false; |
165 | $caUser->setEmailAuthenticationTimestamp( null ); |
166 | $caUser->saveSettings(); |
167 | $this->notifyGlobalUser( $bounceUserId, $originalEmail ); |
168 | wfDebugLog( 'BounceHandler', |
169 | "Un-subscribed global user {$caUser->getName()} <$originalEmail> for " . |
170 | "exceeding Bounce Limit $this->bounceRecordLimit.\nProcessed Headers:\n" . |
171 | $this->formatHeaders( $emailHeaders ) . "\nBounced Email: \n$this->emailRaw" |
172 | ); |
173 | $stats->increment( 'bouncehandler.unsub.global' ); |
174 | } |
175 | } |
176 | if ( $unsubscribeLocalUser ) { |
177 | // Invalidate the email-id of a local user |
178 | $user->setEmailAuthenticationTimestamp( null ); |
179 | $user->saveSettings(); |
180 | $this->createEchoNotification( $bounceUserId, $originalEmail ); |
181 | wfDebugLog( 'BounceHandler', |
182 | "Un-subscribed {$user->getName()} <$originalEmail> for exceeding Bounce limit " . |
183 | "$this->bounceRecordLimit.\nProcessed Headers:\n" . |
184 | $this->formatHeaders( $emailHeaders ) . "\nBounced Email: \n$this->emailRaw" |
185 | ); |
186 | $stats->increment( 'bouncehandler.unsub.local' ); |
187 | } |
188 | } |
189 | |
190 | /** |
191 | * Turns a keyed array into "Key: Value" newline split string |
192 | * |
193 | * @param array $emailHeaders |
194 | * @return string |
195 | */ |
196 | private function formatHeaders( $emailHeaders ) { |
197 | return implode( |
198 | "\n", |
199 | array_map( |
200 | static function ( $v, $k ) { |
201 | return "$k: $v"; |
202 | }, |
203 | $emailHeaders, |
204 | array_keys( $emailHeaders ) |
205 | ) |
206 | ); |
207 | } |
208 | |
209 | } |