Completed
Push — master ( 48f745...94a091 )
by adam
09:26
created

MediawikiApi::logWarnings()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 13.575

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 12
ccs 3
cts 10
cp 0.3
rs 8.8571
cc 5
eloc 7
nc 4
nop 1
crap 13.575
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
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
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 24
	public function __construct( $apiUrl, ClientInterface $client = null, MediawikiSession $session = null ) {
92 24
		if( !is_string( $apiUrl ) ) {
93 4
			throw new InvalidArgumentException( '$apiUrl must be a string' );
94
		}
95 20
		if( $session === null ) {
96 20
			$session = new MediawikiSession( $this );
97 20
		}
98
99 20
		$this->apiUrl = $apiUrl;
100 20
		$this->client = $client;
101 20
		$this->session = $session;
102
103 20
		$this->logger = new NullLogger();
104 20
	}
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
	 *
110
	 * @since 2.3
111
	 *
112
	 * @return string The API URL.
113
	 */
114
	public function getApiUrl() {
115
		return $this->apiUrl;
116
	}
117
118
	/**
119
	 * @return ClientInterface
120
	 */
121 21
	private function getClient() {
122 21
		if( $this->client === null ) {
123 4
			$clientFactory = new ClientFactory();
124 4
			$clientFactory->setLogger( $this->logger );
125 4
			$this->client = $clientFactory->getClient();
126 4
		}
127 21
		return $this->client;
128
	}
129
130
	/**
131
	 * Sets a logger instance on the object
132
	 *
133
	 * @since 1.1
134
	 *
135
	 * @param LoggerInterface $logger
136
	 *
137
	 * @return null
138
	 */
139
	public function setLogger( LoggerInterface $logger ) {
140
		$this->logger = $logger;
141
		$this->session->setLogger( $logger );
142
	}
143
144
	/**
145
	 * @since 2.0
146
	 *
147
	 * @param Request $request
148
	 *
149
	 * @return PromiseInterface
150
	 *         Normally promising an array, though can be mixed (json_decode result)
151
	 *         Can throw UsageExceptions or RejectionExceptions
152
	 */
153 1
	public function getRequestAsync( Request $request ) {
154 1
		$promise = $this->getClient()->requestAsync(
155 1
			'GET',
156 1
			$this->apiUrl,
157 1
			$this->getClientRequestOptions( $request, 'query' )
158 1
		);
159
160
		return $promise->then( function( ResponseInterface $response ) {
161 1
			return call_user_func( array( $this, 'decodeResponse' ), $response );
162 1
		} );
163
	}
164
165
	/**
166
	 * @since 2.0
167
	 *
168
	 * @param Request $request
169
	 *
170
	 * @return PromiseInterface
171
	 *         Normally promising an array, though can be mixed (json_decode result)
172
	 *         Can throw UsageExceptions or RejectionExceptions
173
	 */
174 1
	public function postRequestAsync( Request $request ) {
175 1
		$promise = $this->getClient()->requestAsync(
176 1
			'POST',
177 1
			$this->apiUrl,
178 1
			$this->getClientRequestOptions( $request, $this->getPostRequestEncoding( $request ) )
179 1
		);
180
181
		return $promise->then( function( ResponseInterface $response ) {
182 1
			return call_user_func( array( $this, 'decodeResponse' ), $response );
183 1
		} );
184
	}
185
186
	/**
187
	 * @since 0.2
188
	 *
189
	 * @param Request $request
190
	 *
191
	 * @return mixed Normally an array
192
	 */
193 9
	public function getRequest( Request $request ) {
194 9
		$response = $this->getClient()->request(
195 9
			'GET',
196 9
			$this->apiUrl,
197 9
			$this->getClientRequestOptions( $request, 'query' )
198 9
		);
199
200 9
		return $this->decodeResponse( $response );
201
	}
202
203
	/**
204
	 * @since 0.2
205
	 *
206
	 * @param Request $request
207
	 *
208
	 * @return mixed Normally an array
209
	 */
210 10
	public function postRequest( Request $request ) {
211 10
		$response = $this->getClient()->request(
212 10
			'POST',
213 10
			$this->apiUrl,
214 10
			$this->getClientRequestOptions( $request, $this->getPostRequestEncoding( $request ) )
215 10
		);
216
217 10
		return $this->decodeResponse( $response );
218
	}
219
220
	/**
221
	 * @param ResponseInterface $response
222
	 *
223
	 * @return mixed
224
	 * @throws UsageException
225
	 */
226 21
	private function decodeResponse( ResponseInterface $response ) {
227 21
		$resultArray = json_decode( $response->getBody(), true );
228
229 21
		$this->logWarnings( $resultArray );
230 21
		$this->throwUsageExceptions( $resultArray );
231
232 19
		return $resultArray;
233
	}
234
235
    /**
236
     * @param Request $request
237
     *
238
     * @return string
239
     */
240 9
	private function getPostRequestEncoding( Request $request ) {
241 9
	    foreach ( $request->getParams() as $value ) {
242 9
            if ( is_resource( $value ) ) {
243 1
                return 'multipart';
244
            }
245 9
        }
246 8
        return 'form_params';
247
    }
248
249
	/**
250
	 * @param Request $request
251
	 * @param string $paramsKey either 'query' or 'multipart'
252
	 *
253
	 * @throws RequestException
254
	 *
255
	 * @return array as needed by ClientInterface::get and ClientInterface::post
256
	 */
257 21
	private function getClientRequestOptions( Request $request, $paramsKey ) {
258
259 21
		$params = array_merge( $request->getParams(), array( 'format' => 'json' ) );
260 21
		if ( $paramsKey === 'multipart' ) {
261 1
			$params = $this->encodeMultipartParams( $params );
262 1
		}
263
264
		return array(
265 21
			$paramsKey => $params,
266 21
			'headers' => array_merge( $this->getDefaultHeaders(), $request->getHeaders() ),
267 21
		);
268
	}
269
270
	/**
271
	 * @param array $params
272
	 *
273
	 * @return array
274
	 */
275 1
	private function encodeMultipartParams( $params ) {
276
277 1
		return array_map(
278 1
			function ( $name, $value ) {
279
280
				return array(
281 1
					'name' => $name,
282 1
					'contents' => $value,
283 1
				);
284 1
			},
285 1
			array_keys( $params ),
286
			$params
287 1
		);
288
	}
289
290
	/**
291
	 * @return array
292
	 */
293 17
	private function getDefaultHeaders() {
294
		return array(
295 17
			'User-Agent' => $this->getUserAgent(),
296 17
		);
297
	}
298
299 17
	private function getUserAgent() {
300 17
		$loggedIn = $this->isLoggedin();
301 17
		if( $loggedIn ) {
302
			return 'addwiki-mediawiki-client/' . $loggedIn;
303
		}
304 17
		return 'addwiki-mediawiki-client';
305
	}
306
307
	/**
308
	 * @param $result
309
	 */
310 17
	private function logWarnings( $result ) {
311 17
		if( is_array( $result ) && array_key_exists( 'warnings', $result ) ) {
312
			foreach( $result['warnings'] as $module => $warningData ) {
313
				// Accomodate both formatversion=2 and old-style API results
314
				if( isset( $warningData['*'] ) ) {
315
					$this->logger->log( LogLevel::WARNING, $module . ': ' . $warningData['*'], array( 'data' => $warningData ) );
316
				} else {
317
					$this->logger->log( LogLevel::WARNING, $module . ': ' . $warningData['warnings'], array( 'data' => $warningData ) );
318
				}
319
			}
320
		}
321 17
	}
322
323
	/**
324
	 * @param array $result
325
	 *
326
	 * @throws UsageException
327
	 */
328 17
	private function throwUsageExceptions( $result ) {
329 17
		if( is_array( $result ) && array_key_exists( 'error', $result ) ) {
330 2
			throw new UsageException(
331 2
				$result['error']['code'],
332 2
				$result['error']['info'],
333
				$result
334 2
			);
335
		}
336 15
	}
337
338
	/**
339
	 * @since 0.1
340
	 *
341
	 * @return bool|string false or the name of the current user
342
	 */
343 17
	public function isLoggedin() {
344 17
		return $this->isLoggedIn;
345
	}
346
347
	/**
348
	 * @since 0.1
349
	 *
350
	 * @param ApiUser $apiUser
351
	 *
352
	 * @throws UsageException
353
	 * @return bool success
354
	 */
355 2
	public function login( ApiUser $apiUser ) {
356 2
		$this->logger->log( LogLevel::DEBUG, 'Logging in' );
357 2
		$credentials = $this->getLoginParams( $apiUser );
358 2
		$result = $this->postRequest( new SimpleRequest( 'login', $credentials ) );
359 2
		if ( $result['login']['result'] == "NeedToken" ) {
360 2
			$result = $this->postRequest( new SimpleRequest( 'login', array_merge( array( 'lgtoken' => $result['login']['token'] ), $credentials) ) );
361 2
		}
362 2
		if ( $result['login']['result'] == "Success" ) {
363 1
			$this->isLoggedIn = $apiUser->getUsername();
364 1
			return true;
365
		}
366
367 1
		$this->isLoggedIn = false;
368 1
		$this->logger->log( LogLevel::DEBUG, 'Login failed.', $result );
369 1
		$this->throwLoginUsageException( $result );
370
		return false;
371
	}
372
373
	/**
374
	 * @param ApiUser $apiUser
375
	 *
376
	 * @return string[]
377
	 */
378 2
	private function getLoginParams( ApiUser $apiUser ) {
379
		$params = array(
380 2
			'lgname' => $apiUser->getUsername(),
381 2
			'lgpassword' => $apiUser->getPassword(),
382 2
		);
383
384 2
		if( !is_null( $apiUser->getDomain() ) ) {
385
			$params['lgdomain'] = $apiUser->getDomain();
386
		}
387 2
		return $params;
388
	}
389
390
	/**
391
	 * @param array $result
392
	 *
393
	 * @throws UsageException
394
	 */
395 1
	private function throwLoginUsageException( $result ) {
396 1
		$loginResult = $result['login']['result'];
397
398 1
		throw new UsageException(
399 1
			'login-' . $loginResult,
400 1
			array_key_exists( 'reason', $result['login'] )
401 1
				? $result['login']['reason']
402 1
				: 'No Reason given',
403
			$result
404 1
		);
405
	}
406
407
	/**
408
	 * @since 0.1
409
	 *
410
	 * @return bool success
411
	 */
412 2
	public function logout() {
413 2
		$this->logger->log( LogLevel::DEBUG, 'Logging out' );
414 2
		$result = $this->postRequest( new SimpleRequest( 'logout' ) );
415 2
		if( $result === array() ) {
416 1
			$this->isLoggedIn = false;
417 1
			$this->clearTokens();
418 1
			return true;
419
		}
420 1
		return false;
421
	}
422
423
	/**
424
	 * @since 0.1
425
	 *
426
	 * @param string $type
427
	 *
428
	 * @return string
429
	 */
430 2
	public function getToken( $type = 'csrf' ) {
431 2
		return $this->session->getToken( $type );
432
	}
433
434
	/**
435
	 * @since 0.1
436
	 *
437
	 * Clears all tokens stored by the api
438
	 */
439 1
	public function clearTokens() {
440 1
		$this->session->clearTokens();
441 1
	}
442
443
	/**
444
	 * @return string
445
	 */
446 4
	public function getVersion(){
447 4
		if( !isset( $this->version ) ) {
448 4
			$result = $this->getRequest( new SimpleRequest( 'query', array(
449 4
				'meta' => 'siteinfo',
450 4
				'continue' => '',
451 4
			) ) );
452 4
			preg_match(
453 4
				'/\d+(?:\.\d+)+/',
454 4
				$result['query']['general']['generator'],
455
				$versionParts
456 4
			);
457 4
			$this->version = $versionParts[0];
458 4
		}
459 4
		return $this->version;
460
	}
461
462
}
463