Completed
Pull Request — master (#35)
by Sam
03:43
created

MediawikiApi::setLogger()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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