Completed
Push — master ( 63129a...5dad3c )
by adam
03:38
created

MediawikiApi::getLoginExceptionMessage()   C

Complexity

Conditions 9
Paths 9

Size

Total Lines 22
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 14.184

Importance

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