Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 68
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiHelper
0.00% covered (danger)
0.00%
0 / 68
0.00% covered (danger)
0.00%
0 / 3
306
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 makeRequest
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
72
 makeCurlRequest
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
56
1<?php
2
3declare( strict_types = 1 );
4
5namespace Wikimedia\Parsoid\Config\Api;
6
7class ApiHelper {
8
9    /** @var string */
10    private $endpoint;
11
12    /** @var array */
13    private $curlopt;
14
15    /** @var string */
16    private $cacheDir;
17
18    /** @var bool|string */
19    private $writeToCache;
20
21    /** @var bool */
22    private $onlyCached;
23
24    /**
25     * @param array $opts
26     *  - apiEndpoint: (string) URL for api.php. Required.
27     *  - apiTimeout: (int) Timeout, in sections. Default 60.
28     *  - userAgent: (string) User agent prefix.
29     *  - cacheDir: (string) If present, looks aside to the specified directory
30     *    for a cached response before making a network request.
31     *  - writeToCache: (bool|string) If present and truthy, writes successful
32     *    network requests to `cacheDir` so they can be reused.  If set to
33     *    the string 'pretty', prettifies the JSON returned before writing it.
34     *  - onlyCached: (bool) If present and truthy, throws an error if a
35     *    desired API request is not found in `cacheDir`, in order to avoid
36     *    polluting benchmark results.
37     */
38    public function __construct( array $opts ) {
39        if ( !isset( $opts['apiEndpoint'] ) ) {
40            throw new \InvalidArgumentException( '$opts[\'apiEndpoint\'] must be set' );
41        }
42        $this->endpoint = $opts['apiEndpoint'];
43
44        $this->cacheDir = $opts['cacheDir'] ?? null;
45        $this->writeToCache = $opts['writeToCache'] ?? false;
46        $this->onlyCached = $opts['onlyCached'] ?? false;
47
48        $this->curlopt = [
49            CURLOPT_USERAGENT => trim( ( $opts['userAgent'] ?? '' ) . ' ApiEnv/1.0 Parsoid-PHP/0.1' ),
50            CURLOPT_CONNECTTIMEOUT => $opts['apiTimeout'] ?? 60,
51            CURLOPT_TIMEOUT => $opts['apiTimeout'] ?? 60,
52            CURLOPT_FOLLOWLOCATION => false,
53            CURLOPT_ENCODING => '', // Enable compression
54            CURLOPT_SAFE_UPLOAD => true,
55            CURLOPT_RETURNTRANSFER => true,
56        ];
57    }
58
59    /**
60     * Make an API request
61     * @param array $params API parameters
62     * @return array API response data
63     */
64    public function makeRequest( array $params ): array {
65        $filename = null;
66        $params += [ 'formatversion' => 2 ];
67        if ( $this->cacheDir !== null ) {
68            # sort the parameters for a repeatable filename
69            ksort( $params );
70            $query = $this->endpoint . "?" . http_build_query( $params );
71            $queryHash = hash( 'sha256', $query );
72            $filename = $this->cacheDir . DIRECTORY_SEPARATOR .
73                parse_url( $query, PHP_URL_HOST ) . '-' .
74                substr( $queryHash, 0, 8 );
75            if ( file_exists( $filename ) ) {
76                $res = file_get_contents( $filename );
77                $filename = null; // We don't need to write this back
78            } else {
79                $res = $this->makeCurlRequest( $params );
80            }
81        } else {
82            $res = $this->makeCurlRequest( $params );
83        }
84
85        $data = json_decode( $res, true );
86        if ( !is_array( $data ) ) {
87            throw new \RuntimeException( "HTTP request failed: Response was not a JSON array" );
88        }
89
90        if ( isset( $data['error'] ) ) {
91            $e = $data['error'];
92            throw new \RuntimeException( "MediaWiki API error: [{$e['code']}{$e['info']}" );
93        }
94
95        if ( $filename && $this->writeToCache ) {
96            if ( $this->writeToCache === 'pretty' ) {
97                /* Prettify the results */
98                $dataPretty = [
99                    '__endpoint__' => $this->endpoint,
100                    '__params__' => $params,
101                ] + $data;
102                $res = json_encode(
103                    $dataPretty, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
104                );
105            }
106            file_put_contents( $filename, $res );
107        }
108
109        return $data;
110    }
111
112    private function makeCurlRequest( array $params ): string {
113        if ( $this->onlyCached ) {
114            throw new \RuntimeException( "Failed to find request in recorded cache." );
115        }
116        $ch = curl_init( $this->endpoint );
117        if ( !$ch ) {
118            throw new \RuntimeException( "Failed to open curl handle to $this->endpoint" );
119        }
120
121        $params['format'] = 'json';
122        $params['formatversion'] ??= '2';
123
124        $opts = [
125            CURLOPT_POST => true,
126            CURLOPT_POSTFIELDS => $params,
127        ] + $this->curlopt;
128        if ( !curl_setopt_array( $ch, $opts ) ) {
129            throw new \RuntimeException( "Error setting curl options: " . curl_error( $ch ) );
130        }
131
132        $res = curl_exec( $ch );
133
134        if ( curl_errno( $ch ) !== 0 ) {
135            throw new \RuntimeException( "HTTP request failed: " . curl_error( $ch ) );
136        }
137
138        $code = curl_getinfo( $ch, CURLINFO_RESPONSE_CODE );
139        if ( $code !== 200 ) {
140            throw new \RuntimeException( "HTTP request failed: HTTP code $code" );
141        }
142
143        if ( !$res ) {
144            throw new \RuntimeException( "HTTP request failed: Empty response" );
145        }
146
147        return $res;
148    }
149}