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