Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 123 |
|
0.00% |
0 / 9 |
CRAP | |
0.00% |
0 / 1 |
ForeignWikiRequest | |
0.00% |
0 / 123 |
|
0.00% |
0 / 9 |
930 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
execute | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
getCentralId | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
canUseCentralAuth | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
30 | |||
getCentralAuthToken | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
6 | |||
getCsrfToken | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
20 | |||
getRequestParams | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
30 | |||
getQueryParams | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
doRequests | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
42 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Notifications; |
4 | |
5 | use ApiMain; |
6 | use CentralAuthSessionProvider; |
7 | use Exception; |
8 | use MediaWiki\Logger\LoggerFactory; |
9 | use MediaWiki\MediaWikiServices; |
10 | use MediaWiki\Request\FauxRequest; |
11 | use MediaWiki\Request\WebRequest; |
12 | use MediaWiki\Session\SessionManager; |
13 | use MediaWiki\User\CentralId\CentralIdLookup; |
14 | use MediaWiki\User\User; |
15 | use MediaWiki\User\UserIdentity; |
16 | use MediaWiki\WikiMap\WikiMap; |
17 | use MWExceptionHandler; |
18 | use RequestContext; |
19 | |
20 | class ForeignWikiRequest { |
21 | |
22 | /** @var User */ |
23 | protected $user; |
24 | |
25 | /** @var array */ |
26 | protected $params; |
27 | |
28 | /** @var array */ |
29 | protected $wikis; |
30 | |
31 | /** @var string|null */ |
32 | protected $wikiParam; |
33 | |
34 | /** @var string */ |
35 | protected $method; |
36 | |
37 | /** @var string|null */ |
38 | protected $tokenType; |
39 | |
40 | /** @var string[]|null */ |
41 | protected $csrfTokens; |
42 | |
43 | /** |
44 | * @param User $user |
45 | * @param array $params Request parameters |
46 | * @param array $wikis Wikis to send the request to |
47 | * @param string|null $wikiParam Parameter name to set to the name of the wiki |
48 | * @param string|null $postToken If set, use POST requests and inject a token of this type; |
49 | * if null, use GET requests. |
50 | */ |
51 | public function __construct( User $user, array $params, array $wikis, $wikiParam = null, $postToken = null ) { |
52 | $this->user = $user; |
53 | $this->params = $params; |
54 | $this->wikis = $wikis; |
55 | $this->wikiParam = $wikiParam; |
56 | $this->method = $postToken === null ? 'GET' : 'POST'; |
57 | $this->tokenType = $postToken; |
58 | |
59 | $this->csrfTokens = null; |
60 | } |
61 | |
62 | /** |
63 | * Execute the request |
64 | * @param WebRequest|null $originalRequest Original request data to be sent with these requests |
65 | * @return array[] [ wiki => result ] |
66 | */ |
67 | public function execute( ?WebRequest $originalRequest = null ) { |
68 | if ( !$this->canUseCentralAuth() ) { |
69 | return []; |
70 | } |
71 | |
72 | $reqs = $this->getRequestParams( |
73 | $this->method, |
74 | function ( string $wiki ) use ( $originalRequest ) { |
75 | return $this->getQueryParams( $wiki, $originalRequest ); |
76 | }, |
77 | $originalRequest |
78 | ); |
79 | return $this->doRequests( $reqs ); |
80 | } |
81 | |
82 | /** |
83 | * @param UserIdentity $user |
84 | * @return int |
85 | */ |
86 | protected function getCentralId( $user ) { |
87 | return MediaWikiServices::getInstance() |
88 | ->getCentralIdLookup() |
89 | ->centralIdFromLocalUser( $user, CentralIdLookup::AUDIENCE_RAW ); |
90 | } |
91 | |
92 | protected function canUseCentralAuth() { |
93 | global $wgFullyInitialised; |
94 | |
95 | return $wgFullyInitialised && |
96 | RequestContext::getMain()->getUser()->isSafeToLoad() && |
97 | $this->user->isSafeToLoad() && |
98 | SessionManager::getGlobalSession()->getProvider() instanceof CentralAuthSessionProvider && |
99 | $this->getCentralId( $this->user ) !== 0; |
100 | } |
101 | |
102 | /** |
103 | * Returns CentralAuth token, or null on failure. |
104 | * |
105 | * @param User $user |
106 | * @return string|null |
107 | */ |
108 | protected function getCentralAuthToken( User $user ) { |
109 | $context = new RequestContext; |
110 | $context->setRequest( new FauxRequest( [ 'action' => 'centralauthtoken' ] ) ); |
111 | $context->setUser( $user ); |
112 | |
113 | $api = new ApiMain( $context ); |
114 | |
115 | try { |
116 | $api->execute(); |
117 | |
118 | return $api->getResult()->getResultData( [ 'centralauthtoken', 'centralauthtoken' ] ); |
119 | } catch ( Exception $ex ) { |
120 | LoggerFactory::getInstance( 'Echo' )->debug( |
121 | 'Exception when fetching CentralAuth token: wiki: {wiki}, userName: {userName}, ' . |
122 | 'userId: {userId}, centralId: {centralId}, exception: {exception}', |
123 | [ |
124 | 'wiki' => WikiMap::getCurrentWikiId(), |
125 | 'userName' => $user->getName(), |
126 | 'userId' => $user->getId(), |
127 | 'centralId' => $this->getCentralId( $user ), |
128 | 'exception' => $ex, |
129 | ] |
130 | ); |
131 | |
132 | MWExceptionHandler::logException( $ex ); |
133 | |
134 | return null; |
135 | } |
136 | } |
137 | |
138 | /** |
139 | * Get the CSRF token for a given wiki. |
140 | * This method fetches the tokens for all requested wikis at once and caches the result. |
141 | * |
142 | * @param string $wiki Name of the wiki to get a token for |
143 | * @param WebRequest|null $originalRequest Original request data to be sent with these requests |
144 | * @return string Token, or empty string if an unable to retrieve the token. |
145 | */ |
146 | protected function getCsrfToken( $wiki, ?WebRequest $originalRequest ) { |
147 | if ( $this->csrfTokens === null ) { |
148 | $this->csrfTokens = []; |
149 | $reqs = $this->getRequestParams( 'GET', function ( string $wiki ) { |
150 | // This doesn't depend on the wiki, but 'centralauthtoken' must be different every time |
151 | return [ |
152 | 'action' => 'query', |
153 | 'meta' => 'tokens', |
154 | 'type' => $this->tokenType, |
155 | 'format' => 'json', |
156 | 'formatversion' => '1', |
157 | 'errorformat' => 'bc', |
158 | 'centralauthtoken' => $this->getCentralAuthToken( $this->user ), |
159 | ]; |
160 | }, $originalRequest ); |
161 | $responses = $this->doRequests( $reqs ); |
162 | foreach ( $responses as $w => $response ) { |
163 | if ( isset( $response['query']['tokens']['csrftoken'] ) ) { |
164 | $this->csrfTokens[$w] = $response['query']['tokens']['csrftoken']; |
165 | } else { |
166 | LoggerFactory::getInstance( 'Echo' )->warning( |
167 | __METHOD__ . ': Unexpected CSRF token API response from {wiki}', |
168 | [ |
169 | 'wiki' => $wiki, |
170 | 'response' => $response, |
171 | ] |
172 | ); |
173 | } |
174 | } |
175 | } |
176 | return $this->csrfTokens[$wiki] ?? ''; |
177 | } |
178 | |
179 | /** |
180 | * @param string $method 'GET' or 'POST' |
181 | * @param callable $makeParams Callback that takes a wiki name and returns an associative array of |
182 | * query string / POST parameters |
183 | * @param WebRequest|null $originalRequest Original request data to be sent with these requests |
184 | * @return array[] Array of request parameters to pass to doRequests(), keyed by wiki name |
185 | */ |
186 | protected function getRequestParams( $method, $makeParams, ?WebRequest $originalRequest ) { |
187 | $apis = ForeignNotifications::getApiEndpoints( $this->wikis ); |
188 | if ( !$apis ) { |
189 | return []; |
190 | } |
191 | |
192 | $reqs = []; |
193 | foreach ( $apis as $wiki => $api ) { |
194 | $queryKey = $method === 'POST' ? 'body' : 'query'; |
195 | $reqs[$wiki] = [ |
196 | 'method' => $method, |
197 | 'url' => $api['url'], |
198 | $queryKey => $makeParams( $wiki ) |
199 | ]; |
200 | |
201 | if ( $originalRequest ) { |
202 | $reqs[$wiki]['headers'] = [ |
203 | 'X-Forwarded-For' => $originalRequest->getIP(), |
204 | 'User-Agent' => ( |
205 | $originalRequest->getHeader( 'User-Agent' ) |
206 | . ' (via ForeignWikiRequest MediaWiki/' . MW_VERSION . ')' |
207 | ), |
208 | ]; |
209 | } |
210 | } |
211 | |
212 | return $reqs; |
213 | } |
214 | |
215 | /** |
216 | * @param string $wiki Wiki name |
217 | * @param WebRequest|null $originalRequest Original request data to be sent with these requests |
218 | * @return array |
219 | */ |
220 | protected function getQueryParams( $wiki, ?WebRequest $originalRequest ) { |
221 | $extraParams = []; |
222 | if ( $this->wikiParam ) { |
223 | // Only request data from that specific wiki, or they'd all spawn |
224 | // cross-wiki api requests... |
225 | $extraParams[$this->wikiParam] = $wiki; |
226 | } |
227 | if ( $this->method === 'POST' ) { |
228 | $extraParams['token'] = $this->getCsrfToken( $wiki, $originalRequest ); |
229 | } |
230 | |
231 | return [ |
232 | 'centralauthtoken' => $this->getCentralAuthToken( $this->user ), |
233 | // once all the results are gathered & merged, they'll be output in the |
234 | // user requested format |
235 | // but this is going to be an internal request & we don't want those |
236 | // results in the format the user requested but in a fixed format that |
237 | // we can interpret here |
238 | 'format' => 'json', |
239 | 'formatversion' => '1', |
240 | 'errorformat' => 'bc', |
241 | ] + $extraParams + $this->params; |
242 | } |
243 | |
244 | /** |
245 | * @param array $reqs API request params |
246 | * @return array[] |
247 | * @throws Exception |
248 | */ |
249 | protected function doRequests( array $reqs ) { |
250 | $http = MediaWikiServices::getInstance()->getHttpRequestFactory()->createMultiClient(); |
251 | $responses = $http->runMulti( $reqs ); |
252 | |
253 | $results = []; |
254 | foreach ( $responses as $wiki => $response ) { |
255 | $statusCode = $response['response']['code']; |
256 | |
257 | if ( $statusCode >= 200 && $statusCode <= 299 ) { |
258 | $parsed = json_decode( $response['response']['body'], true ); |
259 | if ( $parsed ) { |
260 | $results[$wiki] = $parsed; |
261 | } |
262 | } |
263 | |
264 | if ( !isset( $results[$wiki] ) ) { |
265 | LoggerFactory::getInstance( 'Echo' )->warning( |
266 | 'Failed to fetch API response from {wiki}. Error: {error}', |
267 | [ |
268 | 'wiki' => $wiki, |
269 | 'error' => $response['response']['error'] ?? 'unknown', |
270 | 'statusCode' => $statusCode, |
271 | 'response' => $response['response'], |
272 | 'request' => $reqs[$wiki], |
273 | ] |
274 | ); |
275 | } |
276 | } |
277 | |
278 | return $results; |
279 | } |
280 | } |