Completed
Push — master ( a89c1a...480ef2 )
by adam
02:31
created

MediawikiApi::getApiUrl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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