Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
71.43% covered (warning)
71.43%
5 / 7
CRAP
69.64% covered (warning)
69.64%
39 / 56
RemoteApiRequestExecutor
0.00% covered (danger)
0.00%
0 / 1
71.43% covered (warning)
71.43%
5 / 7
33.34
69.64% covered (warning)
69.64%
39 / 56
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
6 / 6
 setLogger
n/a
0 / 0
1
n/a
0 / 0
 execute
0.00% covered (danger)
0.00%
0 / 1
10.40
40.00% covered (danger)
40.00%
4 / 10
 getCentralId
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 canUseCentralAuth
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
2 / 2
 getAuthorizedApiUrl
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
3 / 3
 getCsrfToken
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
12 / 12
 doRequest
0.00% covered (danger)
0.00%
0 / 1
11.17
47.62% covered (danger)
47.62%
10 / 21
<?php
namespace FileImporter\Remote\MediaWiki;
use CentralIdLookup;
use Exception;
use FileImporter\Data\SourceUrl;
use FileImporter\Services\Http\HttpRequestExecutor;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use User;
/**
 * Use CentralAuth to execute API calls on a sibling wiki.
 *
 * @license GPL-2.0-or-later
 */
class RemoteApiRequestExecutor implements LoggerAwareInterface {
    /**
     * @var LoggerInterface
     */
    private $logger;
    /**
     * @var HttpApiLookup
     */
    private $httpApiLookup;
    /**
     * @var HttpRequestExecutor
     */
    private $httpRequestExecutor;
    /**
     * @var CentralAuthTokenProvider
     */
    private $centralAuthTokenProvider;
    /**
     * @var CentralIdLookup
     */
    private $centralIdLookup;
    /**
     * @param HttpApiLookup $httpApiLookup
     * @param HttpRequestExecutor $httpRequestExecutor
     * @param CentralAuthTokenProvider $centralAuthTokenProvider
     * @param CentralIdLookup $centralIdLookup
     */
    public function __construct(
        HttpApiLookup $httpApiLookup,
        HttpRequestExecutor $httpRequestExecutor,
        CentralAuthTokenProvider $centralAuthTokenProvider,
        CentralIdLookup $centralIdLookup
    ) {
        $this->httpApiLookup = $httpApiLookup;
        $this->httpRequestExecutor = $httpRequestExecutor;
        $this->centralAuthTokenProvider = $centralAuthTokenProvider;
        $this->centralIdLookup = $centralIdLookup;
        $this->logger = new NullLogger();
    }
    /**
     * @param LoggerInterface $logger
     * @codeCoverageIgnore
     */
    public function setLogger( LoggerInterface $logger ) {
        $this->logger = $logger;
    }
    /**
     * @param SourceUrl $sourceUrl
     * @param User $user
     * @param array $params API request params
     * @param bool $usePost
     * @return array|null Null in case of an error. Calling code can't understand why, but the error
     *  is logged.
     */
    public function execute(
        SourceUrl $sourceUrl,
        User $user,
        array $params,
        bool $usePost = false
    ): ?array {
        // TODO handle error
        if ( !$this->canUseCentralAuth( $user ) ) {
            $this->logger->error( __METHOD__ . ' user can\'t use CentralAuth.' );
            return null;
        }
        $result = $this->doRequest( $sourceUrl, $user, $params, $usePost );
        // It's an array of "errors" with errorformat=plaintext, but a single "error" without.
        // Each error contains "code" and "info" with formatversion=2, but "code" and "*" without.
        if ( isset( $result['errors'] ) || isset( $result['error'] ) ) {
            $this->logger->error( 'Remote API responded with an error', [
                'sourceUrl' => $sourceUrl->getUrl(),
                'apiParameters' => $params,
                'response' => $result,
            ] );
        }
        return $result;
    }
    /**
     * @param User $user
     * @return int
     */
    private function getCentralId( User $user ): int {
        return $this->centralIdLookup->centralIdFromLocalUser(
            $user,
            CentralIdLookup::AUDIENCE_RAW
        );
    }
    /**
     * @param User $user
     * @return bool
     */
    private function canUseCentralAuth( User $user ): bool {
        return $user->isSafeToLoad() &&
            $this->getCentralId( $user ) !== 0;
    }
    /**
     * @param SourceUrl $sourceUrl
     * @param User $user
     * @return string
     * @throws Exception
     */
    private function getAuthorizedApiUrl(
        SourceUrl $sourceUrl,
        User $user
    ): string {
        $url = $this->httpApiLookup->getApiUrl( $sourceUrl );
        return wfAppendQuery( $url, [
            'centralauthtoken' => $this->centralAuthTokenProvider->getToken( $user ),
        ] );
    }
    /**
     * @param SourceUrl $sourceUrl
     * @param User $user
     * @return string|null
     */
    public function getCsrfToken( SourceUrl $sourceUrl, User $user ): ?string {
        try {
            $tokenRequestUrl = $this->getAuthorizedApiUrl( $sourceUrl, $user );
        } catch ( Exception $ex ) {
            $this->logger->error( 'Failed to get centralauthtoken: ' .
                $ex->getMessage() );
            return null;
        }
        $tokenRequest = $this->httpRequestExecutor->execute( $tokenRequestUrl, [
            'action' => 'query',
            'meta' => 'tokens',
            'type' => 'csrf',
            'format' => 'json',
            'formatversion' => 2,
        ] );
        $tokenData = json_decode( $tokenRequest->getContent(), true );
        $token = $tokenData['query']['tokens']['csrftoken'] ?? null;
        if ( !$token ) {
            $this->logger->error( __METHOD__ . ' failed to get CSRF token.' );
        }
        return $token;
    }
    /**
     * @param SourceUrl $sourceUrl
     * @param User $user
     * @param array $params API request params
     * @param bool $usePost
     * @return array|null Null in case of an error. Calling code can't understand why, but the error
     *  is logged.
     */
    private function doRequest(
        SourceUrl $sourceUrl,
        User $user,
        array $params,
        bool $usePost
    ): ?array {
        /** @var array|null $result */
        $result = null;
        /** @var \MWHttpRequest|null $request */
        $request = null;
        try {
            $requestUrl = $this->getAuthorizedApiUrl( $sourceUrl, $user );
            $this->logger->debug( 'Got cross-wiki, authorized API URL: ' . $requestUrl );
            if ( $usePost ) {
                $request = $this->httpRequestExecutor->executePost( $requestUrl, $params );
            } else {
                $request = $this->httpRequestExecutor->execute( $requestUrl, $params );
            }
            $result = json_decode( $request->getContent(), true );
            if ( $result === null ) {
                $this->logger->error( __METHOD__ . ' failed to decode response from ' .
                    $request->getFinalUrl() );
            }
        } catch ( Exception $ex ) {
            if ( !$request ) {
                $msg = __METHOD__ . ' failed to do remote request to ' . $sourceUrl->getHost() .
                    ' with params ' . json_encode( $params ) . ': ' . $ex->getMessage();
            } else {
                $msg = __METHOD__ . ' failed to do remote request to ' . $request->getFinalUrl() .
                    ' with params ' . json_encode( $params ) .
                    ' and response headers ' . json_encode( $request->getResponseHeaders() ) .
                    ': ' . $ex->getMessage();
            }
            $this->logger->error( $msg );
        }
        return $result;
    }
}