Completed
Push — master ( 89ec26...6a878a )
by adam
04:53 queued 02:13
created

MediawikiApi::getLoginParams()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2.0625

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 11
ccs 6
cts 8
cp 0.75
rs 9.4285
cc 2
eloc 7
nc 2
nop 1
crap 2.0625
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( $rsdElement[0]->attributes()['href'] )->getBody() );
83 1
		return self::newFromApiEndpoint( $rsdXml->service->apis->api->attributes()->apiLink->__toString() );
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
	 * @return ClientInterface
108
	 */
109 20
	private function getClient() {
110 20
		if( $this->client === null ) {
111 4
			$clientFactory = new ClientFactory();
112 4
			$clientFactory->setLogger( $this->logger );
113 4
			$this->client = $clientFactory->getClient();
114 4
		}
115 20
		return $this->client;
116
	}
117
118
	/**
119
	 * Sets a logger instance on the object
120
	 *
121
	 * @since 1.1
122
	 *
123
	 * @param LoggerInterface $logger
124
	 *
125
	 * @return null
126
	 */
127
	public function setLogger( LoggerInterface $logger ) {
128
		$this->logger = $logger;
129
		$this->session->setLogger( $logger );
130
	}
131
132
	/**
133
	 * @since 2.0
134
	 *
135
	 * @param Request $request
136
	 *
137
	 * @return PromiseInterface
138
	 *         Normally promising an array, though can be mixed (json_decode result)
139
	 *         Can throw UsageExceptions or RejectionExceptions
140
	 */
141 1
	public function getRequestAsync( Request $request ) {
142 1
		$promise = $this->getClient()->requestAsync(
143 1
			'GET',
144 1
			$this->apiUrl,
145 1
			$this->getClientRequestOptions( $request, 'query' )
146 1
		);
147
148
		return $promise->then( function( ResponseInterface $response ) {
149 1
			return call_user_func( array( $this, 'decodeResponse' ), $response );
150 1
		} );
151
	}
152
153
	/**
154
	 * @since 2.0
155
	 *
156
	 * @param Request $request
157
	 *
158
	 * @return PromiseInterface
159
	 *         Normally promising an array, though can be mixed (json_decode result)
160
	 *         Can throw UsageExceptions or RejectionExceptions
161
	 */
162 1
	public function postRequestAsync( Request $request ) {
163 1
		$promise = $this->getClient()->requestAsync(
164 1
			'POST',
165 1
			$this->apiUrl,
166 1
			$this->getClientRequestOptions( $request, 'form_params' )
167 1
		);
168
169 1
		return $promise->then( function( ResponseInterface $response ) {
170 1
			return call_user_func( array( $this, 'decodeResponse' ), $response );
171 1
		} );
172
	}
173
174
	/**
175
	 * @since 0.2
176
	 *
177
	 * @param Request $request
178
	 *
179
	 * @return mixed Normally an array
180
	 */
181 9
	public function getRequest( Request $request ) {
182 9
		$response = $this->getClient()->request(
183 9
			'GET',
184 9
			$this->apiUrl,
185 9
			$this->getClientRequestOptions( $request, 'query' )
186 9
		);
187
188 9
		return $this->decodeResponse( $response );
189
	}
190
191
	/**
192
	 * @since 0.2
193
	 *
194
	 * @param Request $request
195
	 *
196
	 * @return mixed Normally an array
197
	 */
198 9
	public function postRequest( Request $request ) {
199 9
		$response = $this->getClient()->request(
200 9
			'POST',
201 9
			$this->apiUrl,
202 9
			$this->getClientRequestOptions( $request, 'form_params' )
203 9
		);
204
205 9
		return $this->decodeResponse( $response );
206
	}
207
208
	/**
209
	 * @param ResponseInterface $response
210
	 *
211
	 * @return mixed
212
	 * @throws UsageException
213
	 */
214 20
	private function decodeResponse( ResponseInterface $response ) {
215 20
		$resultArray = json_decode( $response->getBody(), true );
216
217 20
		$this->logWarnings( $resultArray );
218 20
		$this->throwUsageExceptions( $resultArray );
219
220 18
		return $resultArray;
221
	}
222
223
	/**
224
	 * @param Request $request
225
	 * @param string $paramsKey either 'query' or 'form_params'
226
	 *
227
	 * @throws RequestException
228
	 *
229
	 * @return array as needed by ClientInterface::get and ClientInterface::post
230
	 */
231 20
	private function getClientRequestOptions( Request $request, $paramsKey ) {
232
		return array(
233 20
			$paramsKey => array_merge( $request->getParams(), array( 'format' => 'json' ) ),
234 20
			'headers' => array_merge( $this->getDefaultHeaders(), $request->getHeaders() ),
235 20
		);
236
	}
237
238
	/**
239
	 * @return array
240
	 */
241 16
	private function getDefaultHeaders() {
242
		return array(
243 16
			'User-Agent' => $this->getUserAgent(),
244 16
		);
245
	}
246
247 16
	private function getUserAgent() {
248 16
		$loggedIn = $this->isLoggedin();
249 16
		if( $loggedIn ) {
250
			return 'addwiki-mediawiki-client/' . $loggedIn;
251
		}
252 16
		return 'addwiki-mediawiki-client';
253
	}
254
255
	/**
256
	 * @param $result
257
	 */
258 16
	private function logWarnings( $result ) {
259 16
		if( is_array( $result ) && array_key_exists( 'warnings', $result ) ) {
260
			foreach( $result['warnings'] as $module => $warningData ) {
261
				$this->logger->log( LogLevel::WARNING, $module . ': ' . $warningData['*'], array( 'data' => $warningData ) );
262
			}
263
		}
264 16
	}
265
266
	/**
267
	 * @param array $result
268
	 *
269
	 * @throws UsageException
270
	 */
271 16
	private function throwUsageExceptions( $result ) {
272 16
		if( is_array( $result ) && array_key_exists( 'error', $result ) ) {
273 2
			throw new UsageException(
274 2
				$result['error']['code'],
275 2
				$result['error']['info'],
276
				$result
277 2
			);
278
		}
279 14
	}
280
281
	/**
282
	 * @since 0.1
283
	 *
284
	 * @return bool|string false or the name of the current user
285
	 */
286 16
	public function isLoggedin() {
287 16
		return $this->isLoggedIn;
288
	}
289
290
	/**
291
	 * @since 0.1
292
	 *
293
	 * @param ApiUser $apiUser
294
	 *
295
	 * @throws UsageException
296
	 * @return bool success
297
	 */
298 2
	public function login( ApiUser $apiUser ) {
299 2
		$this->logger->log( LogLevel::DEBUG, 'Logging in' );
300 2
		$credentials = $this->getLoginParams( $apiUser );
301 2
		$result = $this->postRequest( new SimpleRequest( 'login', $credentials ) );
302 2
		if ( $result['login']['result'] == "NeedToken" ) {
303 2
			$result = $this->postRequest( new SimpleRequest( 'login', array_merge( array( 'lgtoken' => $result['login']['token'] ), $credentials) ) );
304 2
		}
305 2
		if ( $result['login']['result'] == "Success" ) {
306 1
			$this->isLoggedIn = $apiUser->getUsername();
307 1
			return true;
308
		}
309
310 1
		$this->isLoggedIn = false;
311 1
		$this->throwLoginUsageException( $result );
312
		return false;
313
	}
314
315
	/**
316
	 * @param ApiUser $apiUser
317
	 *
318
	 * @return string[]
319
	 */
320 2
	private function getLoginParams( ApiUser $apiUser ) {
321
		$params = array(
322 2
			'lgname' => $apiUser->getUsername(),
323 2
			'lgpassword' => $apiUser->getPassword(),
324 2
		);
325
326 2
		if( !is_null( $apiUser->getDomain() ) ) {
327
			$params['lgdomain'] = $apiUser->getDomain();
328
		}
329 2
		return $params;
330
	}
331
332
	/**
333
	 * @param array $result
334
	 *
335
	 * @throws UsageException
336
	 */
337 1
	private function throwLoginUsageException( $result ) {
338 1
		$loginResult = $result['login']['result'];
339
340 1
		throw new UsageException(
341 1
			'login-' . $loginResult,
342 1
			$this->getLoginExceptionMessage( $loginResult ),
343
			$result
344 1
		);
345
	}
346
347
	/**
348
	 * @param string $loginResult
349
	 *
350
	 * @return string
351
	 */
352 1
	private function getLoginExceptionMessage( $loginResult ) {
353
		switch( $loginResult ) {
354 1
			case 'Illegal';
355
				return 'You provided an illegal username';
356 1
			case 'NotExists';
357
				return 'The username you provided doesn\'t exist';
358 1
			case 'WrongPass';
359
				return 'The password you provided is incorrect';
360 1
			case 'WrongPluginPass';
361
				return 'An authentication plugin rather than MediaWiki itself rejected the password';
362 1
			case 'CreateBlocked';
363
				return 'The wiki tried to automatically create a new account for you, but your IP address has been blocked from account creation';
364 1
			case 'Throttled';
365
				return 'You\'ve logged in too many times in a short time.';
366 1
			case 'Blocked';
367
				return 'User is blocked';
368 1
			case 'NeedToken';
369
				return 'Either you did not provide the login token or the sessionid cookie.';
370 1
			default:
371 1
				return $loginResult;
372 1
		}
373
	}
374
375
	/**
376
	 * @since 0.1
377
	 *
378
	 * @return bool success
379
	 */
380 2
	public function logout() {
381 2
		$this->logger->log( LogLevel::DEBUG, 'Logging out' );
382 2
		$result = $this->postRequest( new SimpleRequest( 'logout' ) );
383 2
		if( $result === array() ) {
384 1
			$this->isLoggedIn = false;
385 1
			$this->clearTokens();
386 1
			return true;
387
		}
388 1
		return false;
389
	}
390
391
	/**
392
	 * @since 0.1
393
	 *
394
	 * @param string $type
395
	 *
396
	 * @return string
397
	 */
398 2
	public function getToken( $type = 'csrf' ) {
399 2
		return $this->session->getToken( $type );
400
	}
401
402
	/**
403
	 * @since 0.1
404
	 *
405
	 * Clears all tokens stored by the api
406
	 */
407 1
	public function clearTokens() {
408 1
		$this->session->clearTokens();
409 1
	}
410
411
	/**
412
	 * @return string
413
	 */
414 4
	public function getVersion(){
415 4
		if( !isset( $this->version ) ) {
416 4
			$result = $this->getRequest( new SimpleRequest( 'query', array(
417 4
				'meta' => 'siteinfo',
418 4
				'continue' => '',
419 4
			) ) );
420 4
			preg_match(
421 4
				'/\d+(?:\.\d+)+/',
422 4
				$result['query']['general']['generator'],
423
				$versionParts
424 4
			);
425 4
			$this->version = $versionParts[0];
426 4
		}
427 4
		return $this->version;
428
	}
429
430
}
431