Completed
Push — master ( 49512c...0c2959 )
by adam
02:34
created

MediawikiApi::getUserAgent()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2.032

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 7
ccs 4
cts 5
cp 0.8
rs 9.4286
cc 2
eloc 5
nc 2
nop 0
crap 2.032
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\ClientFactory;
12
use Mediawiki\Api\Guzzle\MiddlewareFactory;
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 LoggerAwareInterface {
28
29
	/**
30
	 * @var Client|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.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
	 * @since 2.0.0
72
	 *
73
	 * @param string $url e.g. https://en.wikipedia.org OR https://de.wikipedia.org/wiki/Berlin
74
	 *
75
	 * @return self returns a MediawikiApi instance using the apiEndpoint provided by the RSD
76
	 *              file accessible on all Mediawiki pages
77
	 *
78
	 * @see https://en.wikipedia.org/wiki/Really_Simple_Discovery
79
	 */
80 1
	public static function newFromPage( $url ) {
81 1
		$tempClient = new Client( array( 'headers' => array( 'User-Agent' => 'addwiki-mediawiki-client' ) ) );
82 1
		$pageXml = new SimpleXMLElement( $tempClient->get( $url )->getBody() );
83 1
		$rsdElement = $pageXml->xpath( 'head/link[@type="application/rsd+xml"][@href]' );
84 1
		$rsdXml = new SimpleXMLElement( $tempClient->get( $rsdElement[0]->attributes()['href'] )->getBody() );
85 1
		return self::newFromApiEndpoint( $rsdXml->service->apis->api->attributes()->apiLink->__toString() );
86
	}
87
88
	/**
89
	 * @access private
90
	 *
91
	 * @param string $apiUrl The API Url
92
	 * @param Client|null $client Guzzle Client
93
	 * @param MediawikiSession|null $session Inject a custom session here
94
	 */
95 23
	public function __construct( $apiUrl, Client $client = null, MediawikiSession $session = null ) {
96 23
		if( !is_string( $apiUrl ) ) {
97 4
			throw new InvalidArgumentException( '$apiUrl must be a string' );
98
		}
99 19
		if( $session === null ) {
100 19
			$session = new MediawikiSession( $this );
101 19
		}
102
103 19
		$this->apiUrl = $apiUrl;
104 19
		$this->client = $client;
105 19
		$this->session = $session;
106
107 19
		$this->logger = new NullLogger();
108 19
	}
109
110
	/**
111
	 * @return Client
112
	 */
113 20
	private function getClient() {
114 20
		if( $this->client === null ) {
115 4
			$clientFactory = new ClientFactory();
116 4
			$clientFactory->setLogger( $this->logger );
117 4
			$this->client = $clientFactory->getClient();
118 4
		}
119 20
		return $this->client;
120
	}
121
122
	/**
123
	 * Sets a logger instance on the object
124
	 *
125
	 * @since 1.1
126
	 *
127
	 * @param LoggerInterface $logger
128
	 *
129
	 * @return null
130
	 */
131
	public function setLogger( LoggerInterface $logger ) {
132
		$this->logger = $logger;
133
		$this->session->setLogger( $logger );
134
	}
135
136
	/**
137
	 * @since 2.0
138
	 *
139
	 * @param Request $request
140
	 *
141
	 * @return PromiseInterface
142
	 *         Normally promising an array, though can be mixed (json_decode result)
143
	 *         Can throw UsageExceptions or RejectionExceptions
144
	 */
145 1
	public function getRequestAsync( Request $request ) {
146 1
		$promise = $this->getClient()->getAsync(
147 1
			$this->apiUrl,
148 1
			$this->getClientRequestOptions( $request, 'query' )
149 1
		);
150
151
		return $promise->then( function( ResponseInterface $response ) {
152 1
			return call_user_func( array( $this, 'decodeResponse' ), $response );
153 1
		} );
154
	}
155
156
	/**
157
	 * @since 2.0
158
	 *
159
	 * @param Request $request
160
	 *
161
	 * @return PromiseInterface
162
	 *         Normally promising an array, though can be mixed (json_decode result)
163
	 *         Can throw UsageExceptions or RejectionExceptions
164
	 */
165 1
	public function postRequestAsync( Request $request ) {
166 1
		$promise = $this->getClient()->postAsync(
167 1
			$this->apiUrl,
168 1
			$this->getClientRequestOptions( $request, 'form_params' )
169 1
		);
170
171 1
		return $promise->then( function( ResponseInterface $response ) {
172 1
			return call_user_func( array( $this, 'decodeResponse' ), $response );
173 1
		} );
174
	}
175
176
	/**
177
	 * @since 0.2
178
	 *
179
	 * @param Request $request
180
	 *
181
	 * @return mixed Normally an array
182
	 */
183 9
	public function getRequest( Request $request ) {
184 9
		$response = $this->getClient()->get(
185 9
			$this->apiUrl,
186 9
			$this->getClientRequestOptions( $request, 'query' )
187 9
		);
188
189 9
		return $this->decodeResponse( $response );
190
	}
191
192
	/**
193
	 * @since 0.2
194
	 *
195
	 * @param Request $request
196
	 *
197
	 * @return mixed Normally an array
198
	 */
199 9
	public function postRequest( Request $request ) {
200 9
		$response = $this->getClient()->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