Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
29.90% |
29 / 97 |
|
0.00% |
0 / 5 |
CRAP | |
0.00% |
0 / 1 |
MessageSender | |
29.90% |
29 / 97 |
|
0.00% |
0 / 5 |
143.37 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
editPage | |
93.55% |
29 / 31 |
|
0.00% |
0 / 1 |
7.01 | |||
addLQTThread | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
addFlowTopic | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
makeAPIRequest | |
0.00% |
0 / 45 |
|
0.00% |
0 / 1 |
90 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace MediaWiki\MassMessage; |
5 | |
6 | use MediaWiki\Api\ApiMain; |
7 | use MediaWiki\Api\ApiMessage; |
8 | use MediaWiki\Api\ApiResult; |
9 | use MediaWiki\Api\ApiUsageException; |
10 | use MediaWiki\Context\RequestContext; |
11 | use MediaWiki\Deferred\DeferredUpdates; |
12 | use MediaWiki\Json\FormatJson; |
13 | use MediaWiki\MediaWikiServices; |
14 | use MediaWiki\Permissions\PermissionManager; |
15 | use MediaWiki\Request\DerivativeRequest; |
16 | use MediaWiki\Title\Title; |
17 | use MediaWiki\User\User; |
18 | use Wikimedia\ScopedCallback; |
19 | |
20 | /** |
21 | * Post messages on target pages |
22 | * @author Abijeet Patro |
23 | * @since 2022.08 |
24 | * @license GPL-2.0-or-later |
25 | */ |
26 | class MessageSender { |
27 | /** @var PermissionManager */ |
28 | private $permissionManager; |
29 | /** @var callable|null */ |
30 | private $failureCallback; |
31 | |
32 | /** |
33 | * @param PermissionManager $permissionManager |
34 | * @param callable|null $failureCallback |
35 | */ |
36 | public function __construct( |
37 | PermissionManager $permissionManager, |
38 | ?callable $failureCallback |
39 | ) { |
40 | $this->permissionManager = $permissionManager; |
41 | $this->failureCallback = $failureCallback; |
42 | } |
43 | |
44 | /** |
45 | * @param Title $target |
46 | * @param string $message |
47 | * @param string $subject |
48 | * @param User $user |
49 | * @param string $dedupeHash |
50 | * @return bool |
51 | */ |
52 | public function editPage( |
53 | Title $target, |
54 | string $message, |
55 | string $subject, |
56 | User $user, |
57 | string $dedupeHash |
58 | ): bool { |
59 | $params = [ |
60 | 'action' => 'edit', |
61 | 'title' => $target->getPrefixedText(), |
62 | 'section' => 'new', |
63 | 'summary' => $subject, |
64 | 'text' => $message, |
65 | 'notminor' => true, |
66 | 'token' => $user->getEditToken() |
67 | ]; |
68 | |
69 | if ( $target->inNamespace( NS_USER_TALK ) ) { |
70 | $params['bot'] = true; |
71 | } |
72 | |
73 | $result = $this->makeAPIRequest( $params, $user ); |
74 | if ( $result ) { |
75 | // Apply change tag if the edit succeeded |
76 | $resultData = $result->getResultData(); |
77 | if ( !isset( $resultData['edit']['result'] ) |
78 | || $resultData['edit']['result'] !== 'Success' |
79 | ) { |
80 | // job should retry the edit |
81 | return false; |
82 | } |
83 | if ( !isset( $resultData['edit']['nochange'] ) |
84 | && $resultData['edit']['newrevid'] |
85 | ) { |
86 | $revId = $resultData['edit']['newrevid']; |
87 | DeferredUpdates::addCallableUpdate( static function () use ( $revId, $dedupeHash ) { |
88 | MediaWikiServices::getInstance()->getChangeTagsStore()->addTags( |
89 | [ 'massmessage-delivery' ], |
90 | null, |
91 | $revId, |
92 | null, |
93 | FormatJson::encode( [ 'dedupe_hash' => $dedupeHash ] ), |
94 | ); |
95 | } ); |
96 | } |
97 | return true; |
98 | } |
99 | return false; |
100 | } |
101 | |
102 | /** |
103 | * @param Title $target |
104 | * @param string $message |
105 | * @param string $subject |
106 | * @param User $user |
107 | * @return bool |
108 | */ |
109 | public function addLQTThread( |
110 | Title $target, |
111 | string $message, |
112 | string $subject, |
113 | User $user |
114 | ): bool { |
115 | $params = [ |
116 | 'action' => 'threadaction', |
117 | 'threadaction' => 'newthread', |
118 | 'talkpage' => $target, |
119 | 'subject' => $subject, |
120 | 'text' => $message, |
121 | 'token' => $user->getEditToken() |
122 | // LQT will automatically mark the edit as bot if we're a bot, so don't set here |
123 | ]; |
124 | |
125 | return (bool)$this->makeAPIRequest( $params, $user ); |
126 | } |
127 | |
128 | /** |
129 | * @param Title $target |
130 | * @param string $message |
131 | * @param string $subject |
132 | * @param User $user |
133 | * @return bool |
134 | */ |
135 | public function addFlowTopic( |
136 | Title $target, |
137 | string $message, |
138 | string $subject, |
139 | User $user |
140 | ): bool { |
141 | $params = [ |
142 | 'action' => 'flow', |
143 | 'page' => $target->getPrefixedText(), |
144 | 'submodule' => 'new-topic', |
145 | 'nttopic' => $subject, |
146 | 'ntcontent' => $message, |
147 | 'token' => $user->getEditToken(), |
148 | ]; |
149 | |
150 | return (bool)$this->makeAPIRequest( $params, $user ); |
151 | } |
152 | |
153 | /** |
154 | * Construct and make an API request based on the given params and return the results. |
155 | * |
156 | * @param array $params |
157 | * @param User $ourUser |
158 | * @return ?ApiResult |
159 | */ |
160 | private function makeAPIRequest( array $params, User $ourUser ): ?ApiResult { |
161 | // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgUser |
162 | global $wgUser, $wgRequest; |
163 | |
164 | // Add our hook functions to make the MassMessage user IP block-exempt and email confirmed. |
165 | // Done here so that it's not unnecessarily called on every page load. |
166 | $lock = $this->permissionManager->addTemporaryUserRights( $ourUser, [ 'ipblock-exempt' ] ); |
167 | $hookScope = MediaWikiServices::getInstance()->getHookContainer()->scopedRegister( |
168 | 'EmailConfirmed', |
169 | [ MassMessageHooks::class, 'onEmailConfirmed' ] |
170 | ); |
171 | |
172 | $oldRequest = $wgRequest; |
173 | $oldUser = $wgUser; |
174 | |
175 | $wgRequest = new DerivativeRequest( |
176 | $wgRequest, |
177 | $params, |
178 | // was posted? |
179 | true |
180 | ); |
181 | // New user objects will use $wgRequest, so we set that |
182 | // to our DerivativeRequest, so we don't run into any issues. |
183 | $wgUser = $ourUser; |
184 | $context = RequestContext::getMain(); |
185 | // All further internal API requests will use the main |
186 | // RequestContext, so setting it here will fix it for |
187 | // all other internal uses, like how LQT does |
188 | $oldCUser = $context->getUser(); |
189 | $oldCRequest = $context->getRequest(); |
190 | $context->setUser( $ourUser ); |
191 | $context->setRequest( $wgRequest ); |
192 | |
193 | $api = new ApiMain( |
194 | $wgRequest, |
195 | // enable write? |
196 | true |
197 | ); |
198 | try { |
199 | $attemptCount = 0; |
200 | while ( true ) { |
201 | try { |
202 | $api->execute(); |
203 | // Continue after the while block if the API request succeeds |
204 | break; |
205 | } catch ( ApiUsageException $e ) { |
206 | $attemptCount++; |
207 | $isEditConflict = false; |
208 | foreach ( $e->getStatusValue()->getErrors() as $error ) { |
209 | if ( ApiMessage::create( $error )->getApiCode() === 'editconflict' ) { |
210 | $isEditConflict = true; |
211 | break; |
212 | } |
213 | } |
214 | // If the failure is not caused by an edit conflict or if there |
215 | // have been too many failures, log the (first) error and continue |
216 | // execution. Otherwise retry the request. |
217 | if ( !$isEditConflict || $attemptCount >= 5 ) { |
218 | foreach ( $e->getStatusValue()->getErrors() as $error ) { |
219 | if ( $this->failureCallback ) { |
220 | call_user_func( $this->failureCallback, ApiMessage::create( $error )->getApiCode() ); |
221 | } |
222 | break; |
223 | } |
224 | return null; |
225 | } |
226 | } |
227 | } |
228 | return $api->getResult(); |
229 | } finally { |
230 | // Cleanup all the stuff we polluted |
231 | ScopedCallback::consume( $hookScope ); |
232 | $context->setUser( $oldCUser ); |
233 | $context->setRequest( $oldCRequest ); |
234 | $wgUser = $oldUser; |
235 | $wgRequest = $oldRequest; |
236 | } |
237 | } |
238 | } |