Completed
Push — master ( fea716...246f0f )
by adam
04:02
created

MediawikiApi::throwUsageExceptions()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 9
c 1
b 0
f 0
ccs 7
cts 7
cp 1
rs 9.6666
cc 3
eloc 6
nc 2
nop 1
crap 3
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 2
	public static function newFromPage( $url ) {
82 2
		$tempClient = new Client( [ 'headers' => [ 'User-Agent' => 'addwiki-mediawiki-client' ] ] );
83
84
		// Get the page HTML and extract the RSD link.
85 2
		$pageHtml = $tempClient->get( $url )->getBody();
86 2
		$pageDoc = new DOMDocument();
87 2
		$pageDoc->loadHTML( $pageHtml );
88 2
		$link = ( new DOMXpath( $pageDoc ) )->query( 'head/link[@type="application/rsd+xml"][@href]' );
89 2
		if ( $link->length === 0 ) {
90 1
			throw new RsdException( "Unable to find RSD URL in page: $url" );
91
		}
92 1
		$rsdUrl = $link->item( 0 )->attributes->getnamedItem( 'href' )->nodeValue;
93
94
		// Then get the RSD XML, and return the API link.
95 1
		$rsdXml = new SimpleXMLElement( $tempClient->get( $rsdUrl )->getBody() );
96 1
		return self::newFromApiEndpoint( (string)$rsdXml->service->apis->api->attributes()->apiLink );
97
	}
98
99
	/**
100
	 * @param string $apiUrl The API Url
101
	 * @param ClientInterface|null $client Guzzle Client
102
	 * @param MediawikiSession|null $session Inject a custom session here
103
	 */
104 24
	public function __construct( $apiUrl, ClientInterface $client = null,
105
								 MediawikiSession $session = null ) {
106 24
		if ( !is_string( $apiUrl ) ) {
107 4
			throw new InvalidArgumentException( '$apiUrl must be a string' );
108
		}
109 20
		if ( $session === null ) {
110 20
			$session = new MediawikiSession( $this );
111 20
		}
112
113 20
		$this->apiUrl = $apiUrl;
114 20
		$this->client = $client;
115 20
		$this->session = $session;
116
117 20
		$this->logger = new NullLogger();
118 20
	}
119
120
	/**
121
	 * Get the API URL (the URL to which API requests are sent, usually ending in api.php).
122
	 * This is useful if you've created this object via MediawikiApi::newFromPage().
123
	 *
124
	 * @since 2.3
125
	 *
126
	 * @return string The API URL.
127
	 */
128
	public function getApiUrl() {
129
		return $this->apiUrl;
130
	}
131
132
	/**
133
	 * @return ClientInterface
134
	 */
135 21
	private function getClient() {
136 21
		if ( $this->client === null ) {
137 4
			$clientFactory = new ClientFactory();
138 4
			$clientFactory->setLogger( $this->logger );
139 4
			$this->client = $clientFactory->getClient();
140 4
		}
141 21
		return $this->client;
142
	}
143
144
	/**
145
	 * Sets a logger instance on the object
146
	 *
147
	 * @since 1.1
148
	 *
149
	 * @param LoggerInterface $logger
150
	 *
151
	 * @return null
152
	 */
153
	public function setLogger( LoggerInterface $logger ) {
154
		$this->logger = $logger;
155
		$this->session->setLogger( $logger );
156
	}
157
158
	/**
159
	 * @since 2.0
160
	 *
161
	 * @param Request $request
162
	 *
163
	 * @return PromiseInterface
164
	 *         Normally promising an array, though can be mixed (json_decode result)
165
	 *         Can throw UsageExceptions or RejectionExceptions
166
	 */
167 1
	public function getRequestAsync( Request $request ) {
168 1
		$promise = $this->getClient()->requestAsync(
169 1
			'GET',
170 1
			$this->apiUrl,
171 1
			$this->getClientRequestOptions( $request, 'query' )
172 1
		);
173
174
		return $promise->then( function( ResponseInterface $response ) {
175 1
			return call_user_func( [ $this, 'decodeResponse' ], $response );
176 1
		} );
177
	}
178
179
	/**
180
	 * @since 2.0
181
	 *
182
	 * @param Request $request
183
	 *
184
	 * @return PromiseInterface
185
	 *         Normally promising an array, though can be mixed (json_decode result)
186
	 *         Can throw UsageExceptions or RejectionExceptions
187
	 */
188 1
	public function postRequestAsync( Request $request ) {
189 1
		$promise = $this->getClient()->requestAsync(
190 1
			'POST',
191 1
			$this->apiUrl,
192 1
			$this->getClientRequestOptions( $request, $this->getPostRequestEncoding( $request ) )
193 1
		);
194
195
		return $promise->then( function( ResponseInterface $response ) {
196 1
			return call_user_func( [ $this, 'decodeResponse' ], $response );
197 1
		} );
198
	}
199
200
	/**
201
	 * @since 0.2
202
	 *
203
	 * @param Request $request
204
	 *
205
	 * @return mixed Normally an array
206
	 */
207 9
	public function getRequest( Request $request ) {
208 9
		$response = $this->getClient()->request(
209 9
			'GET',
210 9
			$this->apiUrl,
211 9
			$this->getClientRequestOptions( $request, 'query' )
212 9
		);
213
214 9
		return $this->decodeResponse( $response );
215
	}
216
217
	/**
218
	 * @since 0.2
219
	 *
220
	 * @param Request $request
221
	 *
222
	 * @return mixed Normally an array
223
	 */
224 10
	public function postRequest( Request $request ) {
225 10
		$response = $this->getClient()->request(
226 10
			'POST',
227 10
			$this->apiUrl,
228 10
			$this->getClientRequestOptions( $request, $this->getPostRequestEncoding( $request ) )
229 10
		);
230
231 10
		return $this->decodeResponse( $response );
232
	}
233
234
	/**
235
	 * @param ResponseInterface $response
236
	 *
237
	 * @return mixed
238
	 * @throws UsageException
239
	 */
240 21
	private function decodeResponse( ResponseInterface $response ) {
241 21
		$resultArray = json_decode( $response->getBody(), true );
242
243 21
		$this->logWarnings( $resultArray );
244 21
		$this->throwUsageExceptions( $resultArray );
245
246 19
		return $resultArray;
247
	}
248
249
	/**
250
	 * @param Request $request
251
	 *
252
	 * @return string
253
	 */
254 9
	private function getPostRequestEncoding( Request $request ) {
255 9
	    foreach ( $request->getParams() as $value ) {
256 9
			if ( is_resource( $value ) ) {
257 1
				return 'multipart';
258
			}
259 9
		   }
260 8
		return 'form_params';
261
	}
262
263
	/**
264
	 * @param Request $request
265
	 * @param string $paramsKey either 'query' or 'multipart'
266
	 *
267
	 * @throws RequestException
268
	 *
269
	 * @return array as needed by ClientInterface::get and ClientInterface::post
270
	 */
271 21
	private function getClientRequestOptions( Request $request, $paramsKey ) {
272
273 21
		$params = array_merge( $request->getParams(), [ 'format' => 'json' ] );
274 21
		if ( $paramsKey === 'multipart' ) {
275 1
			$params = $this->encodeMultipartParams( $params );
276 1
		}
277
278
		return [
279 21
			$paramsKey => $params,
280 21
			'headers' => array_merge( $this->getDefaultHeaders(), $request->getHeaders() ),
281 21
		];
282
	}
283
284
	/**
285
	 * @param array $params
286
	 *
287
	 * @return array
288
	 */
289 1
	private function encodeMultipartParams( $params ) {
290
291 1
		return array_map(
292 1
			function ( $name, $value ) {
293
294
				return [
295 1
					'name' => $name,
296 1
					'contents' => $value,
297 1
				];
298 1
			},
299 1
			array_keys( $params ),
300
			$params
301 1
		);
302
	}
303
304
	/**
305
	 * @return array
306
	 */
307 17
	private function getDefaultHeaders() {
308
		return [
309 17
			'User-Agent' => $this->getUserAgent(),
310 17
		];
311
	}
312
313 17
	private function getUserAgent() {
314 17
		$loggedIn = $this->isLoggedin();
315 17
		if ( $loggedIn ) {
316
			return 'addwiki-mediawiki-client/' . $loggedIn;
317
		}
318 17
		return 'addwiki-mediawiki-client';
319
	}
320
321
	/**
322
	 * @param $result
323
	 */
324 17
	private function logWarnings( $result ) {
325 17
		if ( is_array( $result ) && array_key_exists( 'warnings', $result ) ) {
326
			foreach ( $result['warnings'] as $module => $warningData ) {
327
				// Accomodate both formatversion=2 and old-style API results
328
				$logPrefix = $module . ': ';
329
				if ( isset( $warningData['*'] ) ) {
330
					$this->logger->warning( $logPrefix . $warningData['*'], [ 'data' => $warningData ] );
331
				} else {
332
					$this->logger->warning( $logPrefix . $warningData['warnings'], [ 'data' => $warningData ] );
333
				}
334
			}
335
		}
336 17
	}
337
338
	/**
339
	 * @param array $result
340
	 *
341
	 * @throws UsageException
342
	 */
343 17
	private function throwUsageExceptions( $result ) {
344 17
		if ( is_array( $result ) && array_key_exists( 'error', $result ) ) {
345 2
			throw new UsageException(
346 2
				$result['error']['code'],
347 2
				$result['error']['info'],
348
				$result
349 2
			);
350
		}
351 15
	}
352
353
	/**
354
	 * @since 0.1
355
	 *
356
	 * @return bool|string false or the name of the current user
357
	 */
358 17
	public function isLoggedin() {
359 17
		return $this->isLoggedIn;
360
	}
361
362
	/**
363
	 * @since 0.1
364
	 *
365
	 * @param ApiUser $apiUser
366
	 *
367
	 * @throws UsageException
368
	 * @return bool success
369
	 */
370 2
	public function login( ApiUser $apiUser ) {
371 2
		$this->logger->log( LogLevel::DEBUG, 'Logging in' );
372 2
		$credentials = $this->getLoginParams( $apiUser );
373 2
		$result = $this->postRequest( new SimpleRequest( 'login', $credentials ) );
374 2
		if ( $result['login']['result'] == "NeedToken" ) {
375 2
			$params = array_merge( [ 'lgtoken' => $result['login']['token'] ], $credentials );
376 2
			$result = $this->postRequest( new SimpleRequest( 'login', $params ) );
377 2
		}
378 2
		if ( $result['login']['result'] == "Success" ) {
379 1
			$this->isLoggedIn = $apiUser->getUsername();
380 1
			return true;
381
		}
382
383 1
		$this->isLoggedIn = false;
384 1
		$this->logger->log( LogLevel::DEBUG, 'Login failed.', $result );
385 1
		$this->throwLoginUsageException( $result );
386
		return false;
387
	}
388
389
	/**
390
	 * @param ApiUser $apiUser
391
	 *
392
	 * @return string[]
393
	 */
394 2
	private function getLoginParams( ApiUser $apiUser ) {
395
		$params = [
396 2
			'lgname' => $apiUser->getUsername(),
397 2
			'lgpassword' => $apiUser->getPassword(),
398 2
		];
399
400 2
		if ( !is_null( $apiUser->getDomain() ) ) {
401
			$params['lgdomain'] = $apiUser->getDomain();
402
		}
403 2
		return $params;
404
	}
405
406
	/**
407
	 * @param array $result
408
	 *
409
	 * @throws UsageException
410
	 */
411 1
	private function throwLoginUsageException( $result ) {
412 1
		$loginResult = $result['login']['result'];
413
414 1
		throw new UsageException(
415 1
			'login-' . $loginResult,
416 1
			array_key_exists( 'reason', $result['login'] )
417 1
				? $result['login']['reason']
418 1
				: 'No Reason given',
419
			$result
420 1
		);
421
	}
422
423
	/**
424
	 * @since 0.1
425
	 *
426
	 * @return bool success
427
	 */
428 2
	public function logout() {
429 2
		$this->logger->log( LogLevel::DEBUG, 'Logging out' );
430 2
		$result = $this->postRequest( new SimpleRequest( 'logout' ) );
431 2
		if ( $result === [] ) {
432 1
			$this->isLoggedIn = false;
433 1
			$this->clearTokens();
434 1
			return true;
435
		}
436 1
		return false;
437
	}
438
439
	/**
440
	 * @since 0.1
441
	 *
442
	 * @param string $type
443
	 *
444
	 * @return string
445
	 */
446 2
	public function getToken( $type = 'csrf' ) {
447 2
		return $this->session->getToken( $type );
448
	}
449
450
	/**
451
	 * @since 0.1
452
	 *
453
	 * Clears all tokens stored by the api
454
	 */
455 1
	public function clearTokens() {
456 1
		$this->session->clearTokens();
457 1
	}
458
459
	/**
460
	 * @return string
461
	 */
462 4
	public function getVersion() {
463 4
		if ( !isset( $this->version ) ) {
464 4
			$result = $this->getRequest( new SimpleRequest( 'query', [
465 4
				'meta' => 'siteinfo',
466 4
				'continue' => '',
467 4
			] ) );
468 4
			preg_match(
469 4
				'/\d+(?:\.\d+)+/',
470 4
				$result['query']['general']['generator'],
471
				$versionParts
472 4
			);
473 4
			$this->version = $versionParts[0];
474 4
		}
475 4
		return $this->version;
476
	}
477
478
}
479