Completed
Pull Request — master (#34)
by Sam
04:45
created

MediawikiApi::newFromPage()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 5

Importance

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