[ 'User-Agent' => 'addwiki-mediawiki-client' ] ] ); $pageHtml = $tempClient->get( $url )->getBody(); $pageDoc = new DOMDocument(); // Try to load the HTML (turn off errors temporarily; most don't matter, and if they do get // in the way of finding the API URL, will be reported in the RsdException below). $internalErrors = libxml_use_internal_errors( true ); $pageDoc->loadHTML( $pageHtml ); $libXmlErrors = libxml_get_errors(); libxml_use_internal_errors( $internalErrors ); // Extract the RSD link. $xpath = 'head/link[@type="application/rsd+xml"][@href]'; $link = ( new DOMXpath( $pageDoc ) )->query( $xpath ); if ( $link->length === 0 ) { // Format libxml errors for display. $libXmlErrorStr = array_reduce( $libXmlErrors, function ( $prevErr, $err ) { return $prevErr . ', ' . $err->message . ' (line '.$err->line . ')'; } ); if ( $libXmlErrorStr ) { $libXmlErrorStr = "In addition, libxml had the following errors: $libXmlErrorStr"; } throw new RsdException( "Unable to find RSD URL in page: $url $libXmlErrorStr" ); } $rsdUrl = $link->item( 0 )->attributes->getnamedItem( 'href' )->nodeValue; // Then get the RSD XML, and return the API link. $rsdXml = new SimpleXMLElement( $tempClient->get( $rsdUrl )->getBody() ); return self::newFromApiEndpoint( (string)$rsdXml->service->apis->api->attributes()->apiLink ); } /** * @param string $apiUrl The API Url * @param ClientInterface|null $client Guzzle Client * @param MediawikiSession|null $session Inject a custom session here */ public function __construct( $apiUrl, ClientInterface $client = null, MediawikiSession $session = null ) { if ( !is_string( $apiUrl ) ) { throw new InvalidArgumentException( '$apiUrl must be a string' ); } if ( $session === null ) { $session = new MediawikiSession( $this ); } $this->apiUrl = $apiUrl; $this->client = $client; $this->session = $session; $this->logger = new NullLogger(); } /** * Get the API URL (the URL to which API requests are sent, usually ending in api.php). * This is useful if you've created this object via MediawikiApi::newFromPage(). * * @since 2.3 * * @return string The API URL. */ public function getApiUrl() { return $this->apiUrl; } /** * @return ClientInterface */ private function getClient() { if ( $this->client === null ) { $clientFactory = new ClientFactory(); $clientFactory->setLogger( $this->logger ); $this->client = $clientFactory->getClient(); } return $this->client; } /** * Sets a logger instance on the object * * @since 1.1 * * @param LoggerInterface $logger The new Logger object. * * @return null */ public function setLogger( LoggerInterface $logger ) { $this->logger = $logger; $this->session->setLogger( $logger ); } /** * @since 2.0 * * @param Request $request The GET request to send. * * @return PromiseInterface * Normally promising an array, though can be mixed (json_decode result) * Can throw UsageExceptions or RejectionExceptions */ public function getRequestAsync( Request $request ) { $promise = $this->getClient()->requestAsync( 'GET', $this->apiUrl, $this->getClientRequestOptions( $request, 'query' ) ); return $promise->then( function ( ResponseInterface $response ) { return call_user_func( [ $this, 'decodeResponse' ], $response ); } ); } /** * @since 2.0 * * @param Request $request The POST request to send. * * @return PromiseInterface * Normally promising an array, though can be mixed (json_decode result) * Can throw UsageExceptions or RejectionExceptions */ public function postRequestAsync( Request $request ) { $promise = $this->getClient()->requestAsync( 'POST', $this->apiUrl, $this->getClientRequestOptions( $request, $this->getPostRequestEncoding( $request ) ) ); return $promise->then( function ( ResponseInterface $response ) { return call_user_func( [ $this, 'decodeResponse' ], $response ); } ); } /** * @since 0.2 * * @param Request $request The GET request to send. * * @return mixed Normally an array */ public function getRequest( Request $request ) { $response = $this->getClient()->request( 'GET', $this->apiUrl, $this->getClientRequestOptions( $request, 'query' ) ); return $this->decodeResponse( $response ); } /** * @since 0.2 * * @param Request $request The POST request to send. * * @return mixed Normally an array */ public function postRequest( Request $request ) { $response = $this->getClient()->request( 'POST', $this->apiUrl, $this->getClientRequestOptions( $request, $this->getPostRequestEncoding( $request ) ) ); return $this->decodeResponse( $response ); } /** * @param ResponseInterface $response * * @return mixed * @throws UsageException */ private function decodeResponse( ResponseInterface $response ) { $resultArray = json_decode( $response->getBody(), true ); $this->logWarnings( $resultArray ); $this->throwUsageExceptions( $resultArray ); return $resultArray; } /** * @param Request $request * * @return string */ private function getPostRequestEncoding( Request $request ) { if ( $request instanceof MultipartRequest ) { return 'multipart'; } foreach ( $request->getParams() as $value ) { if ( is_resource( $value ) ) { return 'multipart'; } } return 'form_params'; } /** * @param Request $request * @param string $paramsKey either 'query' or 'multipart' * * @throws RequestException * * @return array as needed by ClientInterface::get and ClientInterface::post */ private function getClientRequestOptions( Request $request, $paramsKey ) { $params = array_merge( $request->getParams(), [ 'format' => 'json' ] ); if ( $paramsKey === 'multipart' ) { $params = $this->encodeMultipartParams( $request, $params ); } return [ $paramsKey => $params, 'headers' => array_merge( $this->getDefaultHeaders(), $request->getHeaders() ), ]; } /** * Turn the normal key-value array of request parameters into a multipart array where each * parameter is a new array with a 'name' and 'contents' elements (and optionally more, if the * request is a MultipartRequest). * * @param Request $request The request to which the parameters belong. * @param string[] $params The existing parameters. Not the same as $request->getParams(). * * @return array */ private function encodeMultipartParams( Request $request, $params ) { // See if there are any multipart parameters in this request. $multipartParams = ( $request instanceof MultipartRequest ) ? $request->getMultipartParams() : []; return array_map( function ( $name, $value ) use ( $multipartParams ) { $partParams = [ 'name' => $name, 'contents' => $value, ]; if ( isset( $multipartParams[ $name ] ) ) { // If extra parameters have been set for this part, use them. $partParams = array_merge( $multipartParams[ $name ], $partParams ); } return $partParams; }, array_keys( $params ), $params ); } /** * @return array */ private function getDefaultHeaders() { return [ 'User-Agent' => $this->getUserAgent(), ]; } private function getUserAgent() { $loggedIn = $this->isLoggedin(); if ( $loggedIn ) { return 'addwiki-mediawiki-client/' . $loggedIn; } return 'addwiki-mediawiki-client'; } /** * @param $result */ private function logWarnings( $result ) { if ( is_array( $result ) && array_key_exists( 'warnings', $result ) ) { foreach ( $result['warnings'] as $module => $warningData ) { // Accomodate both formatversion=2 and old-style API results $logPrefix = $module . ': '; if ( isset( $warningData['*'] ) ) { $this->logger->warning( $logPrefix . $warningData['*'], [ 'data' => $warningData ] ); } else { $this->logger->warning( $logPrefix . $warningData['warnings'], [ 'data' => $warningData ] ); } } } } /** * @param array $result * * @throws UsageException */ private function throwUsageExceptions( $result ) { if ( is_array( $result ) && array_key_exists( 'error', $result ) ) { throw new UsageException( $result['error']['code'], $result['error']['info'], $result ); } } /** * @since 0.1 * * @return bool|string false or the name of the current user */ public function isLoggedin() { return $this->isLoggedIn; } /** * @since 0.1 * * @param ApiUser $apiUser The ApiUser to log in as. * * @throws UsageException * @return bool success */ public function login( ApiUser $apiUser ) { $this->logger->log( LogLevel::DEBUG, 'Logging in' ); $credentials = $this->getLoginParams( $apiUser ); $result = $this->postRequest( new SimpleRequest( 'login', $credentials ) ); if ( $result['login']['result'] == "NeedToken" ) { $params = array_merge( [ 'lgtoken' => $result['login']['token'] ], $credentials ); $result = $this->postRequest( new SimpleRequest( 'login', $params ) ); } if ( $result['login']['result'] == "Success" ) { $this->isLoggedIn = $apiUser->getUsername(); return true; } $this->isLoggedIn = false; $this->logger->log( LogLevel::DEBUG, 'Login failed.', $result ); $this->throwLoginUsageException( $result ); return false; } /** * @param ApiUser $apiUser * * @return string[] */ private function getLoginParams( ApiUser $apiUser ) { $params = [ 'lgname' => $apiUser->getUsername(), 'lgpassword' => $apiUser->getPassword(), ]; if ( !is_null( $apiUser->getDomain() ) ) { $params['lgdomain'] = $apiUser->getDomain(); } return $params; } /** * @param array $result * * @throws UsageException */ private function throwLoginUsageException( $result ) { $loginResult = $result['login']['result']; throw new UsageException( 'login-' . $loginResult, array_key_exists( 'reason', $result['login'] ) ? $result['login']['reason'] : 'No Reason given', $result ); } /** * @since 0.1 * * @return bool success */ public function logout() { $this->logger->log( LogLevel::DEBUG, 'Logging out' ); $result = $this->postRequest( new SimpleRequest( 'logout' ) ); if ( $result === [] ) { $this->isLoggedIn = false; $this->clearTokens(); return true; } return false; } /** * @since 0.1 * * @param string $type The token type to get. * * @return string */ public function getToken( $type = 'csrf' ) { return $this->session->getToken( $type ); } /** * Clear all tokens stored by the API. * * @since 0.1 */ public function clearTokens() { $this->session->clearTokens(); } /** * @return string */ public function getVersion() { if ( !isset( $this->version ) ) { $result = $this->getRequest( new SimpleRequest( 'query', [ 'meta' => 'siteinfo', 'continue' => '', ] ) ); preg_match( '/\d+(?:\.\d+)+/', $result['query']['general']['generator'], $versionParts ); $this->version = $versionParts[0]; } return $this->version; } }