Completed
Pull Request — master (#24)
by Sam
02:51
created

MediawikiApi::decodeResponse()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 8
c 0
b 0
f 0
ccs 5
cts 5
cp 1
rs 9.4285
cc 1
eloc 5
nc 1
nop 1
crap 1
1
<?php
2
3
namespace Mediawiki\Api;
4
5
use GuzzleHttp\Client;
6
use GuzzleHttp\ClientInterface;
7
use GuzzleHttp\Exception\RequestException;
8
use GuzzleHttp\Promise\PromiseInterface;
9
use InvalidArgumentException;
10
use Mediawiki\Api\Guzzle\ClientFactory;
11
use Psr\Http\Message\ResponseInterface;
12
use Psr\Log\LoggerAwareInterface;
13
use Psr\Log\LoggerInterface;
14
use Psr\Log\LogLevel;
15
use Psr\Log\NullLogger;
16
use SimpleXMLElement;
17
18
/**
19
 * Main class for this library
20
 *
21
 * @since 0.1
22
 *
23
 * @author Addshore
24
 */
25
class MediawikiApi implements MediawikiApiInterface, LoggerAwareInterface {
26
27
	/**
28
	 * @var ClientInterface|null Should be accessed through getClient
29
	 */
30
	private $client = null;
31
32
	/**
33
	 * @var bool|string
34
	 */
35
	private $isLoggedIn;
36
37
	/**
38
	 * @var MediawikiSession
39
	 */
40
	private $session;
41
42
	/**
43
	 * @var string
44
	 */
45
	private $version;
46
47
	/**
48
	 * @var LoggerInterface
49
	 */
50
	private $logger;
51
52
	/**
53
	 * @var string
54
	 */
55
	private $apiUrl;
56
57
	/**
58
	 * @since 2.0.0
59
	 *
60
	 * @param string $apiEndpoint e.g. https://en.wikipedia.org/w/api.php
61
	 *
62
	 * @return self returns a MediawikiApi instance using $apiEndpoint
63
	 */
64
	public static function newFromApiEndpoint( $apiEndpoint ) {
65
		return new self( $apiEndpoint );
66
	}
67
68
	/**
69
	 * @since 2.0.0
70
	 *
71
	 * @param string $url e.g. https://en.wikipedia.org OR https://de.wikipedia.org/wiki/Berlin
72
	 *
73
	 * @return self returns a MediawikiApi instance using the apiEndpoint provided by the RSD
74
	 *              file accessible on all Mediawiki pages
75
	 *
76
	 * @see https://en.wikipedia.org/wiki/Really_Simple_Discovery
77
	 */
78 1
	public static function newFromPage( $url ) {
79 1
		$tempClient = new Client( array( 'headers' => array( 'User-Agent' => 'addwiki-mediawiki-client' ) ) );
80 1
		$pageXml = new SimpleXMLElement( $tempClient->get( $url )->getBody() );
81 1
		$rsdElement = $pageXml->xpath( 'head/link[@type="application/rsd+xml"][@href]' );
82 1
		$rsdXml = new SimpleXMLElement( $tempClient->get( (string) $rsdElement[0]->attributes()['href'] )->getBody() );
83 1
		return self::newFromApiEndpoint( (string) $rsdXml->service->apis->api->attributes()->apiLink );
84
	}
85
86
	/**
87
	 * @param string $apiUrl The API Url
88
	 * @param ClientInterface|null $client Guzzle Client
89
	 * @param MediawikiSession|null $session Inject a custom session here
90
	 */
91 23
	public function __construct( $apiUrl, ClientInterface $client = null, MediawikiSession $session = null ) {
92 23
		if( !is_string( $apiUrl ) ) {
93 4
			throw new InvalidArgumentException( '$apiUrl must be a string' );
94
		}
95 19
		if( $session === null ) {
96 19
			$session = new MediawikiSession( $this );
97 19
		}
98
99 19
		$this->apiUrl = $apiUrl;
100 19
		$this->client = $client;
101 19
		$this->session = $session;
102
103 19
		$this->logger = new NullLogger();
104 19
	}
105
106
	/**
107
	 * Get the API URL (the URL to which API requests are sent, usually ending in api.php).
108
	 * This is useful if you've created this object via MediawikiApi::newFromPage().
109
	 * @return string The API URL.
110
	 */
111
	public function getApiUrl()
112
	{
113
		return $this->apiUrl;
114
	}
115
116
	/**
117
	 * @return ClientInterface
118
	 */
119 20
	private function getClient() {
120 20
		if( $this->client === null ) {
121 4
			$clientFactory = new ClientFactory();
122 4
			$clientFactory->setLogger( $this->logger );
123 4
			$this->client = $clientFactory->getClient();
124 4
		}
125 20
		return $this->client;
126
	}
127
128
	/**
129
	 * Sets a logger instance on the object
130
	 *
131
	 * @since 1.1
132
	 *
133
	 * @param LoggerInterface $logger
134
	 *
135
	 * @return null
136
	 */
137
	public function setLogger( LoggerInterface $logger ) {
138
		$this->logger = $logger;
139
		$this->session->setLogger( $logger );
140
	}
141
142
	/**
143
	 * @since 2.0
144
	 *
145
	 * @param Request $request
146
	 *
147
	 * @return PromiseInterface
148
	 *         Normally promising an array, though can be mixed (json_decode result)
149
	 *         Can throw UsageExceptions or RejectionExceptions
150
	 */
151 1
	public function getRequestAsync( Request $request ) {
152 1
		$promise = $this->getClient()->requestAsync(
153 1
			'GET',
154 1
			$this->apiUrl,
155 1
			$this->getClientRequestOptions( $request, 'query' )
156 1
		);
157
158
		return $promise->then( function( ResponseInterface $response ) {
159 1
			return call_user_func( array( $this, 'decodeResponse' ), $response );
160 1
		} );
161
	}
162
163
	/**
164
	 * @since 2.0
165
	 *
166
	 * @param Request $request
167
	 *
168
	 * @return PromiseInterface
169
	 *         Normally promising an array, though can be mixed (json_decode result)
170
	 *         Can throw UsageExceptions or RejectionExceptions
171
	 */
172 1
	public function postRequestAsync( Request $request ) {
173 1
		$promise = $this->getClient()->requestAsync(
174 1
			'POST',
175 1
			$this->apiUrl,
176 1
			$this->getClientRequestOptions( $request, 'form_params' )
177 1
		);
178
179 1
		return $promise->then( function( ResponseInterface $response ) {
180
			return call_user_func( array( $this, 'decodeResponse' ), $response );
181 1
		} );
182
	}
183
184
	/**
185
	 * @since 0.2
186
	 *
187
	 * @param Request $request
188
	 *
189
	 * @return mixed Normally an array
190
	 */
191 9
	public function getRequest( Request $request ) {
192 9
		$response = $this->getClient()->request(
193 9
			'GET',
194 9
			$this->apiUrl,
195 9
			$this->getClientRequestOptions( $request, 'query' )
196 9
		);
197
198 9
		return $this->decodeResponse( $response );
199
	}
200
201
	/**
202
	 * @since 0.2
203
	 *
204
	 * @param Request $request
205
	 *
206
	 * @return mixed Normally an array
207
	 */
208 9
	public function postRequest( Request $request ) {
209 9
		$response = $this->getClient()->request(
210 9
			'POST',
211 9
			$this->apiUrl,
212 9
			$this->getClientRequestOptions( $request, 'form_params' )
213 9
		);
214
215 8
		return $this->decodeResponse( $response );
216
	}
217
218
	/**
219
	 * @param ResponseInterface $response
220
	 *
221
	 * @return mixed
222
	 * @throws UsageException
223
	 */
224 18
	private function decodeResponse( ResponseInterface $response ) {
225 18
		$resultArray = json_decode( $response->getBody(), true );
226
227 18
		$this->logWarnings( $resultArray );
228 18
		$this->throwUsageExceptions( $resultArray );
229
230 16
		return $resultArray;
231
	}
232
233
	/**
234
	 * @param Request $request
235
	 * @param string $paramsKey either 'query' or 'form_params'
236
	 *
237
	 * @throws RequestException
238
	 *
239
	 * @return array as needed by ClientInterface::get and ClientInterface::post
240
	 */
241 20
	private function getClientRequestOptions( Request $request, $paramsKey ) {
242
		return array(
243 20
			$paramsKey => array_merge( $request->getParams(), array( 'format' => 'json' ) ),
244 20
			'headers' => array_merge( $this->getDefaultHeaders(), $request->getHeaders() ),
245 20
		);
246
	}
247
248
	/**
249
	 * @return array
250
	 */
251 16
	private function getDefaultHeaders() {
252
		return array(
253 16
			'User-Agent' => $this->getUserAgent(),
254 16
		);
255
	}
256
257 16
	private function getUserAgent() {
258 16
		$loggedIn = $this->isLoggedin();
259 16
		if( $loggedIn ) {
260
			return 'addwiki-mediawiki-client/' . $loggedIn;
261
		}
262 16
		return 'addwiki-mediawiki-client';
263
	}
264
265
	/**
266
	 * @param $result
267
	 */
268 16
	private function logWarnings( $result ) {
269 16
		if( is_array( $result ) && array_key_exists( 'warnings', $result ) ) {
270
			foreach( $result['warnings'] as $module => $warningData ) {
271
				$this->logger->log( LogLevel::WARNING, $module . ': ' . $warningData['*'], array( 'data' => $warningData ) );
272
			}
273
		}
274 16
	}
275
276
	/**
277
	 * @param array $result
278
	 *
279
	 * @throws UsageException
280
	 */
281 16
	private function throwUsageExceptions( $result ) {
282 16
		if( is_array( $result ) && array_key_exists( 'error', $result ) ) {
283 2
			throw new UsageException(
284 2
				$result['error']['code'],
285 2
				$result['error']['info'],
286
				$result
287 2
			);
288
		}
289 14
	}
290
291
	/**
292
	 * @since 0.1
293
	 *
294
	 * @return bool|string false or the name of the current user
295
	 */
296 16
	public function isLoggedin() {
297 16
		return $this->isLoggedIn;
298
	}
299
300
	/**
301
	 * @since 0.1
302
	 *
303
	 * @param ApiUser $apiUser
304
	 *
305
	 * @throws UsageException
306
	 * @return bool success
307
	 */
308 2
	public function login( ApiUser $apiUser ) {
309 2
		$this->logger->log( LogLevel::DEBUG, 'Logging in' );
310 2
		$credentials = $this->getLoginParams( $apiUser );
311 2
		$result = $this->postRequest( new SimpleRequest( 'login', $credentials ) );
312 2
		if ( $result['login']['result'] == "NeedToken" ) {
313 2
			$result = $this->postRequest( new SimpleRequest( 'login', array_merge( array( 'lgtoken' => $result['login']['token'] ), $credentials) ) );
314 2
		}
315 2
		if ( $result['login']['result'] == "Success" ) {
316 1
			$this->isLoggedIn = $apiUser->getUsername();
317 1
			return true;
318
		}
319
320 1
		$this->isLoggedIn = false;
321 1
		$this->throwLoginUsageException( $result );
322
		return false;
323
	}
324
325
	/**
326
	 * @param ApiUser $apiUser
327
	 *
328
	 * @return string[]
329
	 */
330 2
	private function getLoginParams( ApiUser $apiUser ) {
331
		$params = array(
332 2
			'lgname' => $apiUser->getUsername(),
333 2
			'lgpassword' => $apiUser->getPassword(),
334 2
		);
335
336 2
		if( !is_null( $apiUser->getDomain() ) ) {
337
			$params['lgdomain'] = $apiUser->getDomain();
338
		}
339 2
		return $params;
340
	}
341
342
	/**
343
	 * @param array $result
344
	 *
345
	 * @throws UsageException
346
	 */
347 1
	private function throwLoginUsageException( $result ) {
348 1
		$loginResult = $result['login']['result'];
349
350 1
		throw new UsageException(
351 1
			'login-' . $loginResult,
352 1
			$this->getLoginExceptionMessage( $loginResult ),
353
			$result
354 1
		);
355
	}
356
357
	/**
358
	 * @param string $loginResult
359
	 *
360
	 * @return string
361
	 */
362 1
	private function getLoginExceptionMessage( $loginResult ) {
363
		switch( $loginResult ) {
364 1
			case 'Illegal';
365
				return 'You provided an illegal username';
366 1
			case 'NotExists';
367
				return 'The username you provided doesn\'t exist';
368 1
			case 'WrongPass';
369
				return 'The password you provided is incorrect';
370 1
			case 'WrongPluginPass';
371
				return 'An authentication plugin rather than MediaWiki itself rejected the password';
372 1
			case 'CreateBlocked';
373
				return 'The wiki tried to automatically create a new account for you, but your IP address has been blocked from account creation';
374 1
			case 'Throttled';
375
				return 'You\'ve logged in too many times in a short time.';
376 1
			case 'Blocked';
377
				return 'User is blocked';
378 1
			case 'NeedToken';
379
				return 'Either you did not provide the login token or the sessionid cookie.';
380 1
			default:
381 1
				return $loginResult;
382 1
		}
383
	}
384
385
	/**
386
	 * @since 0.1
387
	 *
388
	 * @return bool success
389
	 */
390 2
	public function logout() {
391 2
		$this->logger->log( LogLevel::DEBUG, 'Logging out' );
392 2
		$result = $this->postRequest( new SimpleRequest( 'logout' ) );
393 2
		if( $result === array() ) {
394 1
			$this->isLoggedIn = false;
395 1
			$this->clearTokens();
396 1
			return true;
397
		}
398 1
		return false;
399
	}
400
401
	/**
402
	 * @since 0.1
403
	 *
404
	 * @param string $type
405
	 *
406
	 * @return string
407
	 */
408 2
	public function getToken( $type = 'csrf' ) {
409 2
		return $this->session->getToken( $type );
410
	}
411
412
	/**
413
	 * @since 0.1
414
	 *
415
	 * Clears all tokens stored by the api
416
	 */
417 1
	public function clearTokens() {
418 1
		$this->session->clearTokens();
419 1
	}
420
421
	/**
422
	 * @return string
423
	 */
424 4
	public function getVersion(){
425 4
		if( !isset( $this->version ) ) {
426 4
			$result = $this->getRequest( new SimpleRequest( 'query', array(
427 4
				'meta' => 'siteinfo',
428 4
				'continue' => '',
429 4
			) ) );
430 4
			preg_match(
431 4
				'/\d+(?:\.\d+)+/',
432 4
				$result['query']['general']['generator'],
433
				$versionParts
434 4
			);
435 4
			$this->version = $versionParts[0];
436 4
		}
437 4
		return $this->version;
438
	}
439
440
}
441