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