This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | |||
3 | namespace Mediawiki\Api; |
||
4 | |||
5 | use DOMDocument; |
||
6 | use DOMXPath; |
||
7 | use GuzzleHttp\Client; |
||
8 | use GuzzleHttp\ClientInterface; |
||
9 | use GuzzleHttp\Exception\RequestException; |
||
10 | use GuzzleHttp\Promise\PromiseInterface; |
||
11 | use InvalidArgumentException; |
||
12 | use Mediawiki\Api\Guzzle\ClientFactory; |
||
13 | use Psr\Http\Message\ResponseInterface; |
||
14 | use Psr\Log\LoggerAwareInterface; |
||
15 | use Psr\Log\LoggerInterface; |
||
16 | use Psr\Log\LogLevel; |
||
17 | use Psr\Log\NullLogger; |
||
18 | use SimpleXMLElement; |
||
19 | |||
20 | /** |
||
21 | * Main class for this library |
||
22 | * |
||
23 | * @since 0.1 |
||
24 | * |
||
25 | * @author Addshore |
||
26 | */ |
||
27 | class MediawikiApi implements MediawikiApiInterface, LoggerAwareInterface { |
||
28 | |||
29 | /** |
||
30 | * @var ClientInterface|null Should be accessed through getClient |
||
31 | */ |
||
32 | private $client = null; |
||
33 | |||
34 | /** |
||
35 | * @var bool|string |
||
36 | */ |
||
37 | private $isLoggedIn; |
||
38 | |||
39 | /** |
||
40 | * @var MediawikiSession |
||
41 | */ |
||
42 | private $session; |
||
43 | |||
44 | /** |
||
45 | * @var string |
||
46 | */ |
||
47 | private $version; |
||
48 | |||
49 | /** |
||
50 | * @var LoggerInterface |
||
51 | */ |
||
52 | private $logger; |
||
53 | |||
54 | /** |
||
55 | * @var string |
||
56 | */ |
||
57 | private $apiUrl; |
||
58 | |||
59 | /** |
||
60 | * @since 2.0 |
||
61 | * |
||
62 | * @param string $apiEndpoint e.g. https://en.wikipedia.org/w/api.php |
||
63 | * |
||
64 | * @return self returns a MediawikiApi instance using $apiEndpoint |
||
65 | */ |
||
66 | public static function newFromApiEndpoint( $apiEndpoint ) { |
||
67 | return new self( $apiEndpoint ); |
||
68 | } |
||
69 | |||
70 | /** |
||
71 | * Create a new MediawikiApi object from a URL to any page in a MediaWiki website. |
||
72 | * |
||
73 | * @since 2.0 |
||
74 | * @see https://en.wikipedia.org/wiki/Really_Simple_Discovery |
||
75 | * |
||
76 | * @param string $url e.g. https://en.wikipedia.org OR https://de.wikipedia.org/wiki/Berlin |
||
77 | * @return self returns a MediawikiApi instance using the apiEndpoint provided by the RSD |
||
78 | * file accessible on all Mediawiki pages |
||
79 | * @throws RsdException If the RSD URL could not be found in the page's HTML. |
||
80 | */ |
||
81 | 3 | public static function newFromPage( $url ) { |
|
82 | // Set up HTTP client and HTML document. |
||
83 | 3 | $tempClient = new Client( [ 'headers' => [ 'User-Agent' => 'addwiki-mediawiki-client' ] ] ); |
|
84 | 3 | $pageHtml = $tempClient->get( $url )->getBody(); |
|
85 | 3 | $pageDoc = new DOMDocument(); |
|
86 | |||
87 | // Try to load the HTML (turn off errors temporarily; most don't matter, and if they do get |
||
88 | // in the way of finding the API URL, will be reported in the RsdException below). |
||
89 | 3 | $internalErrors = libxml_use_internal_errors( true ); |
|
90 | 3 | $pageDoc->loadHTML( $pageHtml ); |
|
91 | 3 | $libXmlErrors = libxml_get_errors(); |
|
92 | 3 | libxml_use_internal_errors( $internalErrors ); |
|
93 | |||
94 | // Extract the RSD link. |
||
95 | 3 | $xpath = 'head/link[@type="application/rsd+xml"][@href]'; |
|
96 | 3 | $link = ( new DOMXpath( $pageDoc ) )->query( $xpath ); |
|
97 | 3 | if ( $link->length === 0 ) { |
|
98 | // Format libxml errors for display. |
||
99 | $libXmlErrorStr = array_reduce( $libXmlErrors, function ( $prevErr, $err ) { |
||
100 | return $prevErr . ', ' . $err->message . ' (line ' . $err->line . ')'; |
||
101 | 1 | } ); |
|
102 | 1 | if ( $libXmlErrorStr ) { |
|
103 | $libXmlErrorStr = "In addition, libxml had the following errors: $libXmlErrorStr"; |
||
104 | } |
||
105 | 1 | throw new RsdException( "Unable to find RSD URL in page: $url $libXmlErrorStr" ); |
|
106 | } |
||
107 | 2 | $rsdUrl = $link->item( 0 )->attributes->getnamedItem( 'href' )->nodeValue; |
|
108 | |||
109 | // Then get the RSD XML, and return the API link. |
||
110 | 2 | $rsdXml = new SimpleXMLElement( $tempClient->get( $rsdUrl )->getBody() ); |
|
111 | 2 | return self::newFromApiEndpoint( (string)$rsdXml->service->apis->api->attributes()->apiLink ); |
|
112 | } |
||
113 | |||
114 | /** |
||
115 | * @param string $apiUrl The API Url |
||
116 | * @param ClientInterface|null $client Guzzle Client |
||
117 | * @param MediawikiSession|null $session Inject a custom session here |
||
118 | */ |
||
119 | 25 | public function __construct( $apiUrl, ClientInterface $client = null, |
|
120 | MediawikiSession $session = null ) { |
||
121 | 25 | if ( !is_string( $apiUrl ) ) { |
|
122 | 4 | throw new InvalidArgumentException( '$apiUrl must be a string' ); |
|
123 | } |
||
124 | 21 | if ( $session === null ) { |
|
125 | 21 | $session = new MediawikiSession( $this ); |
|
126 | } |
||
127 | |||
128 | 21 | $this->apiUrl = $apiUrl; |
|
129 | 21 | $this->client = $client; |
|
130 | 21 | $this->session = $session; |
|
131 | |||
132 | 21 | $this->logger = new NullLogger(); |
|
133 | 21 | } |
|
134 | |||
135 | /** |
||
136 | * Get the API URL (the URL to which API requests are sent, usually ending in api.php). |
||
137 | * This is useful if you've created this object via MediawikiApi::newFromPage(). |
||
138 | * |
||
139 | * @since 2.3 |
||
140 | * |
||
141 | * @return string The API URL. |
||
142 | */ |
||
143 | public function getApiUrl() { |
||
144 | return $this->apiUrl; |
||
145 | } |
||
146 | |||
147 | /** |
||
148 | * @return ClientInterface |
||
149 | */ |
||
150 | 21 | private function getClient() { |
|
151 | 21 | if ( $this->client === null ) { |
|
152 | 4 | $clientFactory = new ClientFactory(); |
|
153 | 4 | $clientFactory->setLogger( $this->logger ); |
|
154 | 4 | $this->client = $clientFactory->getClient(); |
|
155 | } |
||
156 | 21 | return $this->client; |
|
157 | } |
||
158 | |||
159 | /** |
||
160 | * Sets a logger instance on the object |
||
161 | * |
||
162 | * @since 1.1 |
||
163 | * |
||
164 | * @param LoggerInterface $logger The new Logger object. |
||
165 | * |
||
166 | * @return null |
||
167 | */ |
||
168 | 1 | public function setLogger( LoggerInterface $logger ) { |
|
169 | 1 | $this->logger = $logger; |
|
170 | 1 | $this->session->setLogger( $logger ); |
|
171 | 1 | } |
|
172 | |||
173 | /** |
||
174 | * @since 2.0 |
||
175 | * |
||
176 | * @param Request $request The GET request to send. |
||
177 | * |
||
178 | * @return PromiseInterface |
||
179 | * Normally promising an array, though can be mixed (json_decode result) |
||
180 | * Can throw UsageExceptions or RejectionExceptions |
||
181 | */ |
||
182 | 1 | View Code Duplication | public function getRequestAsync( Request $request ) { |
0 ignored issues
–
show
|
|||
183 | 1 | $promise = $this->getClient()->requestAsync( |
|
184 | 1 | 'GET', |
|
185 | 1 | $this->apiUrl, |
|
186 | 1 | $this->getClientRequestOptions( $request, 'query' ) |
|
187 | ); |
||
188 | |||
189 | return $promise->then( function ( ResponseInterface $response ) { |
||
190 | 1 | return call_user_func( [ $this, 'decodeResponse' ], $response ); |
|
191 | 1 | } ); |
|
192 | } |
||
193 | |||
194 | /** |
||
195 | * @since 2.0 |
||
196 | * |
||
197 | * @param Request $request The POST request to send. |
||
198 | * |
||
199 | * @return PromiseInterface |
||
200 | * Normally promising an array, though can be mixed (json_decode result) |
||
201 | * Can throw UsageExceptions or RejectionExceptions |
||
202 | */ |
||
203 | 1 | View Code Duplication | public function postRequestAsync( Request $request ) { |
0 ignored issues
–
show
This method seems to be duplicated in your project.
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation. You can also find more detailed suggestions in the “Code” section of your repository.
Loading history...
|
|||
204 | 1 | $promise = $this->getClient()->requestAsync( |
|
205 | 1 | 'POST', |
|
206 | 1 | $this->apiUrl, |
|
207 | 1 | $this->getClientRequestOptions( $request, $this->getPostRequestEncoding( $request ) ) |
|
208 | ); |
||
209 | |||
210 | return $promise->then( function ( ResponseInterface $response ) { |
||
211 | 1 | return call_user_func( [ $this, 'decodeResponse' ], $response ); |
|
212 | 1 | } ); |
|
213 | } |
||
214 | |||
215 | /** |
||
216 | * @since 0.2 |
||
217 | * |
||
218 | * @param Request $request The GET request to send. |
||
219 | * |
||
220 | * @return mixed Normally an array |
||
221 | */ |
||
222 | 9 | View Code Duplication | public function getRequest( Request $request ) { |
0 ignored issues
–
show
This method seems to be duplicated in your project.
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation. You can also find more detailed suggestions in the “Code” section of your repository.
Loading history...
|
|||
223 | 9 | $response = $this->getClient()->request( |
|
224 | 9 | 'GET', |
|
225 | 9 | $this->apiUrl, |
|
226 | 9 | $this->getClientRequestOptions( $request, 'query' ) |
|
227 | ); |
||
228 | |||
229 | 9 | return $this->decodeResponse( $response ); |
|
230 | } |
||
231 | |||
232 | /** |
||
233 | * @since 0.2 |
||
234 | * |
||
235 | * @param Request $request The POST request to send. |
||
236 | * |
||
237 | * @return mixed Normally an array |
||
238 | */ |
||
239 | 10 | View Code Duplication | public function postRequest( Request $request ) { |
0 ignored issues
–
show
This method seems to be duplicated in your project.
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation. You can also find more detailed suggestions in the “Code” section of your repository.
Loading history...
|
|||
240 | 10 | $response = $this->getClient()->request( |
|
241 | 10 | 'POST', |
|
242 | 10 | $this->apiUrl, |
|
243 | 10 | $this->getClientRequestOptions( $request, $this->getPostRequestEncoding( $request ) ) |
|
244 | ); |
||
245 | |||
246 | 10 | return $this->decodeResponse( $response ); |
|
247 | } |
||
248 | |||
249 | /** |
||
250 | * @param ResponseInterface $response |
||
251 | * |
||
252 | * @return mixed |
||
253 | * @throws UsageException |
||
254 | */ |
||
255 | 21 | private function decodeResponse( ResponseInterface $response ) { |
|
256 | 21 | $resultArray = json_decode( $response->getBody(), true ); |
|
257 | |||
258 | 21 | $this->logWarnings( $resultArray ); |
|
259 | 21 | $this->throwUsageExceptions( $resultArray ); |
|
260 | |||
261 | 19 | return $resultArray; |
|
262 | } |
||
263 | |||
264 | /** |
||
265 | * @param Request $request |
||
266 | * |
||
267 | * @return string |
||
268 | */ |
||
269 | 9 | private function getPostRequestEncoding( Request $request ) { |
|
270 | 9 | if ( $request instanceof MultipartRequest ) { |
|
271 | return 'multipart'; |
||
272 | } |
||
273 | 9 | foreach ( $request->getParams() as $value ) { |
|
274 | 9 | if ( is_resource( $value ) ) { |
|
275 | 9 | return 'multipart'; |
|
276 | } |
||
277 | } |
||
278 | 8 | return 'form_params'; |
|
279 | } |
||
280 | |||
281 | /** |
||
282 | * @param Request $request |
||
283 | * @param string $paramsKey either 'query' or 'multipart' |
||
284 | * |
||
285 | * @throws RequestException |
||
286 | * |
||
287 | * @return array as needed by ClientInterface::get and ClientInterface::post |
||
288 | */ |
||
289 | 21 | private function getClientRequestOptions( Request $request, $paramsKey ) { |
|
290 | 21 | $params = array_merge( $request->getParams(), [ 'format' => 'json' ] ); |
|
291 | 21 | if ( $paramsKey === 'multipart' ) { |
|
292 | 1 | $params = $this->encodeMultipartParams( $request, $params ); |
|
293 | } |
||
294 | |||
295 | return [ |
||
296 | 21 | $paramsKey => $params, |
|
297 | 21 | 'headers' => array_merge( $this->getDefaultHeaders(), $request->getHeaders() ), |
|
298 | ]; |
||
299 | } |
||
300 | |||
301 | /** |
||
302 | * Turn the normal key-value array of request parameters into a multipart array where each |
||
303 | * parameter is a new array with a 'name' and 'contents' elements (and optionally more, if the |
||
304 | * request is a MultipartRequest). |
||
305 | * |
||
306 | * @param Request $request The request to which the parameters belong. |
||
307 | * @param string[] $params The existing parameters. Not the same as $request->getParams(). |
||
308 | * |
||
309 | * @return array |
||
310 | */ |
||
311 | 1 | private function encodeMultipartParams( Request $request, $params ) { |
|
312 | // See if there are any multipart parameters in this request. |
||
313 | 1 | $multipartParams = ( $request instanceof MultipartRequest ) |
|
314 | ? $request->getMultipartParams() |
||
315 | 1 | : []; |
|
316 | 1 | return array_map( |
|
317 | 1 | function ( $name, $value ) use ( $multipartParams ) { |
|
318 | $partParams = [ |
||
319 | 1 | 'name' => $name, |
|
320 | 1 | 'contents' => $value, |
|
321 | ]; |
||
322 | 1 | if ( isset( $multipartParams[ $name ] ) ) { |
|
323 | // If extra parameters have been set for this part, use them. |
||
324 | $partParams = array_merge( $multipartParams[ $name ], $partParams ); |
||
325 | } |
||
326 | 1 | return $partParams; |
|
327 | 1 | }, |
|
328 | 1 | array_keys( $params ), |
|
329 | 1 | $params |
|
330 | ); |
||
331 | } |
||
332 | |||
333 | /** |
||
334 | * @return array |
||
335 | */ |
||
336 | 17 | private function getDefaultHeaders() { |
|
337 | return [ |
||
338 | 17 | 'User-Agent' => $this->getUserAgent(), |
|
339 | ]; |
||
340 | } |
||
341 | |||
342 | 17 | private function getUserAgent() { |
|
343 | 17 | $loggedIn = $this->isLoggedin(); |
|
344 | 17 | if ( $loggedIn ) { |
|
345 | return 'addwiki-mediawiki-client/' . $loggedIn; |
||
346 | } |
||
347 | 17 | return 'addwiki-mediawiki-client'; |
|
348 | } |
||
349 | |||
350 | /** |
||
351 | * @param array $result |
||
352 | */ |
||
353 | 18 | private function logWarnings( $result ) { |
|
354 | 18 | if ( is_array( $result ) ) { |
|
355 | // Let's see if there is 'warnings' key on the first level of the array... |
||
356 | 17 | if ( $this->logWarning( $result ) ) { |
|
357 | 1 | return; |
|
358 | } |
||
359 | |||
360 | // ...if no then go one level deeper and check there for it. |
||
361 | 16 | foreach ( $result as $value ) { |
|
362 | 15 | if ( !is_array( $value ) ) { |
|
363 | 7 | continue; |
|
364 | } |
||
365 | |||
366 | 10 | $this->logWarning( $value ); |
|
367 | } |
||
368 | } |
||
369 | 17 | } |
|
370 | |||
371 | /** |
||
372 | * @param array $array Array response to look for warning in. |
||
373 | * |
||
374 | * @return bool Whether any warning has been logged or not. |
||
375 | */ |
||
376 | 17 | protected function logWarning( $array ) { |
|
377 | 17 | $found = false; |
|
378 | |||
379 | 17 | if ( !array_key_exists( 'warnings', $array ) ) { |
|
380 | 16 | return false; |
|
381 | } |
||
382 | |||
383 | 1 | foreach ( $array['warnings'] as $module => $warningData ) { |
|
384 | // Accommodate both formatversion=2 and old-style API results |
||
385 | 1 | $logPrefix = $module . ': '; |
|
386 | 1 | if ( isset( $warningData['*'] ) ) { |
|
387 | $this->logger->warning( $logPrefix . $warningData['*'], [ 'data' => $warningData ] ); |
||
388 | 1 | } elseif ( isset( $warningData['warnings'] ) ) { |
|
389 | $this->logger->warning( $logPrefix . $warningData['warnings'], [ 'data' => $warningData ] ); |
||
390 | } else { |
||
391 | 1 | $this->logger->warning( $logPrefix, [ 'data' => $warningData ] ); |
|
392 | } |
||
393 | |||
394 | 1 | $found = true; |
|
395 | } |
||
396 | |||
397 | 1 | return $found; |
|
398 | } |
||
399 | |||
400 | /** |
||
401 | * @param array $result |
||
402 | * |
||
403 | * @throws UsageException |
||
404 | */ |
||
405 | 17 | private function throwUsageExceptions( $result ) { |
|
406 | 17 | if ( is_array( $result ) && array_key_exists( 'error', $result ) ) { |
|
407 | 2 | throw new UsageException( |
|
408 | 2 | $result['error']['code'], |
|
409 | 2 | $result['error']['info'], |
|
410 | 2 | $result |
|
411 | ); |
||
412 | } |
||
413 | 15 | } |
|
414 | |||
415 | /** |
||
416 | * @since 0.1 |
||
417 | * |
||
418 | * @return bool|string false or the name of the current user |
||
419 | */ |
||
420 | 17 | public function isLoggedin() { |
|
421 | 17 | return $this->isLoggedIn; |
|
422 | } |
||
423 | |||
424 | /** |
||
425 | * @since 0.1 |
||
426 | * |
||
427 | * @param ApiUser $apiUser The ApiUser to log in as. |
||
428 | * |
||
429 | * @throws UsageException |
||
430 | * @return bool success |
||
431 | */ |
||
432 | 2 | public function login( ApiUser $apiUser ) { |
|
433 | 2 | $this->logger->log( LogLevel::DEBUG, 'Logging in' ); |
|
434 | 2 | $credentials = $this->getLoginParams( $apiUser ); |
|
435 | 2 | $result = $this->postRequest( new SimpleRequest( 'login', $credentials ) ); |
|
436 | 2 | if ( $result['login']['result'] == "NeedToken" ) { |
|
437 | 2 | $params = array_merge( [ 'lgtoken' => $result['login']['token'] ], $credentials ); |
|
438 | 2 | $result = $this->postRequest( new SimpleRequest( 'login', $params ) ); |
|
439 | } |
||
440 | 2 | if ( $result['login']['result'] == "Success" ) { |
|
441 | 1 | $this->isLoggedIn = $apiUser->getUsername(); |
|
442 | 1 | return true; |
|
443 | } |
||
444 | |||
445 | 1 | $this->isLoggedIn = false; |
|
446 | 1 | $this->logger->log( LogLevel::DEBUG, 'Login failed.', $result ); |
|
447 | 1 | $this->throwLoginUsageException( $result ); |
|
448 | return false; |
||
449 | } |
||
450 | |||
451 | /** |
||
452 | * @param ApiUser $apiUser |
||
453 | * |
||
454 | * @return string[] |
||
455 | */ |
||
456 | 2 | private function getLoginParams( ApiUser $apiUser ) { |
|
457 | $params = [ |
||
458 | 2 | 'lgname' => $apiUser->getUsername(), |
|
459 | 2 | 'lgpassword' => $apiUser->getPassword(), |
|
460 | ]; |
||
461 | |||
462 | 2 | if ( $apiUser->getDomain() !== null ) { |
|
463 | $params['lgdomain'] = $apiUser->getDomain(); |
||
464 | } |
||
465 | 2 | return $params; |
|
466 | } |
||
467 | |||
468 | /** |
||
469 | * @param array $result |
||
470 | * |
||
471 | * @throws UsageException |
||
472 | */ |
||
473 | 1 | private function throwLoginUsageException( $result ) { |
|
474 | 1 | $loginResult = $result['login']['result']; |
|
475 | |||
476 | 1 | throw new UsageException( |
|
477 | 1 | 'login-' . $loginResult, |
|
478 | 1 | array_key_exists( 'reason', $result['login'] ) |
|
479 | ? $result['login']['reason'] |
||
480 | 1 | : 'No Reason given', |
|
481 | 1 | $result |
|
482 | ); |
||
483 | } |
||
484 | |||
485 | /** |
||
486 | * @since 0.1 |
||
487 | * |
||
488 | * @return bool success |
||
489 | */ |
||
490 | 2 | public function logout() { |
|
491 | 2 | $this->logger->log( LogLevel::DEBUG, 'Logging out' ); |
|
492 | 2 | $result = $this->postRequest( new SimpleRequest( 'logout', [ |
|
493 | 2 | 'token' => $this->getToken() |
|
494 | 1 | ] ) ); |
|
495 | 1 | if ( $result === [] ) { |
|
496 | 1 | $this->isLoggedIn = false; |
|
497 | $this->clearTokens(); |
||
498 | 1 | return true; |
|
499 | } |
||
500 | return false; |
||
501 | } |
||
502 | |||
503 | /** |
||
504 | * @since 0.1 |
||
505 | * |
||
506 | * @param string $type The token type to get. |
||
507 | * |
||
508 | 2 | * @return string |
|
509 | 2 | */ |
|
510 | public function getToken( $type = 'csrf' ) { |
||
511 | return $this->session->getToken( $type ); |
||
512 | } |
||
513 | |||
514 | /** |
||
515 | * Clear all tokens stored by the API. |
||
516 | * |
||
517 | 1 | * @since 0.1 |
|
518 | 1 | */ |
|
519 | 1 | public function clearTokens() { |
|
520 | $this->session->clearTokens(); |
||
521 | } |
||
522 | |||
523 | /** |
||
524 | 4 | * @return string |
|
525 | 4 | */ |
|
526 | 4 | public function getVersion() { |
|
527 | 4 | if ( !isset( $this->version ) ) { |
|
528 | $result = $this->getRequest( new SimpleRequest( 'query', [ |
||
529 | 'meta' => 'siteinfo', |
||
530 | 4 | 'continue' => '', |
|
531 | 4 | ] ) ); |
|
532 | 4 | preg_match( |
|
533 | 4 | '/\d+(?:\.\d+)+/', |
|
534 | $result['query']['general']['generator'], |
||
535 | 4 | $versionParts |
|
536 | ); |
||
537 | 4 | $this->version = $versionParts[0]; |
|
538 | } |
||
539 | return $this->version; |
||
540 | } |
||
541 | |||
542 | } |
||
543 |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.