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