Completed
Push — fixTravisCoverageReporting ( 3b7662...4fbbcc )
by adam
02:51
created

MediawikiApi::getClientRequestOptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 6
ccs 4
cts 4
cp 1
rs 9.4286
cc 1
eloc 4
nc 1
nop 2
crap 1
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 2
	public static function newFromPage( $url ) {
80 2
		$tempClient = new Client( array( 'headers' => array( 'User-Agent' => 'addwiki-mediawiki-client' ) ) );
81 2
		$pageXml = new SimpleXMLElement( $tempClient->get( $url )->getBody() );
82 2
		$rsdElement = $pageXml->xpath( 'head/link[@type="application/rsd+xml"][@href]' );
83 2
		$rsdXml = new SimpleXMLElement( $tempClient->get( $rsdElement[0]->attributes()['href'] )->getBody() );
84 2
		return 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 46
	public function __construct( $apiUrl, Client $client = null, MediawikiSession $session = null ) {
95 46
		if( !is_string( $apiUrl ) ) {
96 8
			throw new InvalidArgumentException( '$apiUrl must be a string' );
97
		}
98 38
		if( $session === null ) {
99 38
			$session = new MediawikiSession( $this );
100 38
		}
101
102 38
		$this->apiUrl = $apiUrl;
103 38
		$this->client = $client;
104 38
		$this->session = $session;
105
106 38
		$this->logger = new NullLogger();
107 38
	}
108
109
	/**
110
	 * @return Client
111
	 */
112 40
	private function getClient() {
113 40
		if( $this->client === null ) {
114 8
			$middlewareFactory = new MiddlewareFactory();
115 8
			$middlewareFactory->setLogger( $this->logger );
116
117 8
			$handlerStack = HandlerStack::create( new CurlHandler() );
118 8
			$handlerStack->push( $middlewareFactory->retry() );
119
120 8
			$this->client = new Client( array(
121 8
				'cookies' => true,
122 8
				'handler' => $handlerStack,
123 8
			) );
124 8
		}
125 40
		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 2
	public function getRequestAsync( Request $request ) {
152 2
		$promise = $this->getClient()->getAsync(
153 2
			$this->apiUrl,
154 2
			$this->getClientRequestOptions( $request, 'query' )
155 2
		);
156
157
		return $promise->then( function( ResponseInterface $response ) {
158 2
			return call_user_func( array( $this, 'decodeResponse' ), $response );
159 2
		} );
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 2
	public function postRequestAsync( Request $request ) {
172 2
		$promise = $this->getClient()->postAsync(
173 2
			$this->apiUrl,
174 2
			$this->getClientRequestOptions( $request, 'form_params' )
175 2
		);
176
177 2
		return $promise->then( function( ResponseInterface $response ) {
178 2
			return call_user_func( array( $this, 'decodeResponse' ), $response );
179 2
		} );
180
	}
181
182
	/**
183
	 * @since 0.2
184
	 *
185
	 * @param Request $request
186
	 *
187
	 * @return mixed Normally an array
188
	 */
189 18
	public function getRequest( Request $request ) {
190 18
		$response = $this->getClient()->get(
191 18
			$this->apiUrl,
192 18
			$this->getClientRequestOptions( $request, 'query' )
193 18
		);
194
195 18
		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 18
	public function postRequest( Request $request ) {
206 18
		$response = $this->getClient()->post(
207 18
			$this->apiUrl,
208 18
			$this->getClientRequestOptions( $request, 'form_params' )
209 18
		);
210
211 18
		return $this->decodeResponse( $response );
212
	}
213
214
	/**
215
	 * @param ResponseInterface $response
216
	 *
217
	 * @return mixed
218
	 * @throws UsageException
219
	 */
220 40
	private function decodeResponse( ResponseInterface $response ) {
221 40
		$resultArray = json_decode( $response->getBody(), true );
222
223 40
		$this->logWarnings( $resultArray );
224 40
		$this->throwUsageExceptions( $resultArray );
225
226 36
		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 40
	private function getClientRequestOptions( Request $request, $paramsKey ) {
238
		return array(
239 40
			$paramsKey => array_merge( $request->getParams(), array( 'format' => 'json' ) ),
240 40
			'headers' => array_merge( $this->getDefaultHeaders(), $request->getHeaders() ),
241 40
		);
242
	}
243
244
	/**
245
	 * @return array
246
	 */
247 32
	private function getDefaultHeaders() {
248
		return array(
249 32
			'User-Agent' => $this->getUserAgent(),
250 32
		);
251
	}
252
253 32
	private function getUserAgent() {
254 32
		$loggedIn = $this->isLoggedin();
255 32
		if( $loggedIn ) {
256
			return 'addwiki-mediawiki-client/' . $loggedIn;
257
		}
258 32
		return 'addwiki-mediawiki-client';
259
	}
260
261
	/**
262
	 * @param $result
263
	 */
264 32
	private function logWarnings( $result ) {
265 32
		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 32
	}
271
272
	/**
273
	 * @param array $result
274
	 *
275
	 * @throws UsageException
276
	 */
277 32
	private function throwUsageExceptions( $result ) {
278 32
		if( is_array( $result ) && array_key_exists( 'error', $result ) ) {
279 4
			throw new UsageException(
280 4
				$result['error']['code'],
281 4
				$result['error']['info'],
282
				$result
283 4
			);
284
		}
285 28
	}
286
287
	/**
288
	 * @since 0.1
289
	 *
290
	 * @return bool|string false or the name of the current user
291
	 */
292 32
	public function isLoggedin() {
293 32
		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 4
	public function login( ApiUser $apiUser ) {
305 4
		$this->logger->log( LogLevel::DEBUG, 'Logging in' );
306 4
		$credentials = $this->getLoginParams( $apiUser );
307 4
		$result = $this->postRequest( new SimpleRequest( 'login', $credentials ) );
308 4
		if ( $result['login']['result'] == "NeedToken" ) {
309 4
			$result = $this->postRequest( new SimpleRequest( 'login', array_merge( array( 'lgtoken' => $result['login']['token'] ), $credentials) ) );
310 4
		}
311 4
		if ( $result['login']['result'] == "Success" ) {
312 2
			$this->isLoggedIn = $apiUser->getUsername();
313 2
			return true;
314
		}
315
316 2
		$this->isLoggedIn = false;
317 2
		$this->throwLoginUsageException( $result );
318
		return false;
319
	}
320
321
	/**
322
	 * @param ApiUser $apiUser
323
	 *
324
	 * @return string[]
325
	 */
326 4
	private function getLoginParams( ApiUser $apiUser ) {
327
		$params = array(
328 4
			'lgname' => $apiUser->getUsername(),
329 4
			'lgpassword' => $apiUser->getPassword(),
330 4
		);
331
332 4
		if( !is_null( $apiUser->getDomain() ) ) {
333
			$params['lgdomain'] = $apiUser->getDomain();
334
		}
335 4
		return $params;
336
	}
337
338
	/**
339
	 * @param array $result
340
	 *
341
	 * @throws UsageException
342
	 */
343 2
	private function throwLoginUsageException( $result ) {
344 2
		$loginResult = $result['login']['result'];
345
346 2
		throw new UsageException(
347 2
			'login-' . $loginResult,
348 2
			$this->getLoginExceptionMessage( $loginResult ),
349
			$result
350 2
		);
351
	}
352
353
	/**
354
	 * @param string $loginResult
355
	 *
356
	 * @return string
357
	 */
358 2
	private function getLoginExceptionMessage( $loginResult ) {
359
		switch( $loginResult ) {
360 2
			case 'Illegal';
361
				return 'You provided an illegal username';
362 2
			case 'NotExists';
363
				return 'The username you provided doesn\'t exist';
364 2
			case 'WrongPass';
365
				return 'The password you provided is incorrect';
366 2
			case 'WrongPluginPass';
367
				return 'An authentication plugin rather than MediaWiki itself rejected the password';
368 2
			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 2
			case 'Throttled';
371
				return 'You\'ve logged in too many times in a short time.';
372 2
			case 'Blocked';
373
				return 'User is blocked';
374 2
			case 'NeedToken';
375
				return 'Either you did not provide the login token or the sessionid cookie.';
376 2
			default:
377 2
				return $loginResult;
378 2
		}
379
	}
380
381
	/**
382
	 * @since 0.1
383
	 *
384
	 * @return bool success
385
	 */
386 4
	public function logout() {
387 4
		$this->logger->log( LogLevel::DEBUG, 'Logging out' );
388 4
		$result = $this->postRequest( new SimpleRequest( 'logout' ) );
389 4
		if( $result === array() ) {
390 2
			$this->isLoggedIn = false;
391 2
			$this->clearTokens();
392 2
			return true;
393
		}
394 2
		return false;
395
	}
396
397
	/**
398
	 * @since 0.1
399
	 *
400
	 * @param string $type
401
	 *
402
	 * @return string
403
	 */
404 4
	public function getToken( $type = 'csrf' ) {
405 4
		return $this->session->getToken( $type );
406
	}
407
408
	/**
409
	 * @since 0.1
410
	 *
411
	 * Clears all tokens stored by the api
412
	 */
413 2
	public function clearTokens() {
414 2
		$this->session->clearTokens();
415 2
	}
416
417
	/**
418
	 * @return string
419
	 */
420 8
	public function getVersion(){
421 8
		if( !isset( $this->version ) ) {
422 8
			$result = $this->getRequest( new SimpleRequest( 'query', array(
423 8
				'meta' => 'siteinfo',
424 8
				'continue' => '',
425 8
			) ) );
426 8
			preg_match(
427 8
				'/\d+(?:\.\d+)+/',
428 8
				$result['query']['general']['generator'],
429
				$versionParts
430 8
			);
431 8
			$this->version = $versionParts[0];
432 8
		}
433 8
		return $this->version;
434
	}
435
436
}
437