Vous avez reçu un message "Your GitLab account has been locked ..." ? Pas d'inquiétude : lisez cet article https://docs.gricad-pages.univ-grenoble-alpes.fr/help/unlock/

fetcher.php 5.03 KB
Newer Older
Francois Gannaz's avatar
Francois Gannaz committed
1
2
3
4
<?php

/**
 * @license http://www.gnu.org/licenses/gpl-3.0.html  GNU GPL v3
Francois Gannaz's avatar
Francois Gannaz committed
5
 * @copyright  2019 Université Grenoble Alpes
Francois Gannaz's avatar
Francois Gannaz committed
6
7
8
9
 */

namespace mod_labnbook\fetch;

Francois Gannaz's avatar
Francois Gannaz committed
10
use Firebase\JWT\JWT;
Francois Gannaz's avatar
Francois Gannaz committed
11
12

/**
Francois Gannaz's avatar
Francois Gannaz committed
13
14
15
 * Fetch info from the LabNbook API.
 *
 * This class does not depend on the environment (Moodle user, config, role, etc).
Francois Gannaz's avatar
Francois Gannaz committed
16
17
18
 */
class fetcher
{
Francois Gannaz's avatar
Francois Gannaz committed
19
20
    const TIMEOUT = 2; // seconds to wait for LabNbook connection

Francois Gannaz's avatar
Francois Gannaz committed
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
    /**
     * @var string Root URL to the API
     */
    private $baseUrl;

    /**
     * @var string Secret key for signing JWT
     */
    private $secret;

    /**
     * @var environment base of the JWT payload across requests
     */
    private $environment;

    /**
Francois Gannaz's avatar
Francois Gannaz committed
37
     * @var \Stdclass { code, message }, e.g. { code: 403, message: "not auth" }
Francois Gannaz's avatar
Francois Gannaz committed
38
39
40
     */
    private $lastError;

Francois Gannaz's avatar
Francois Gannaz committed
41
42
    public function __construct(string $baseUrl, string $secret, environment $environment)
    {
Francois Gannaz's avatar
Francois Gannaz committed
43
44
45
        if (!$secret) {
            throw new \Exception("The JWT secret key is empty.");
        }
Francois Gannaz's avatar
Francois Gannaz committed
46
        $this->baseUrl = \rtrim($baseUrl, '/');
Francois Gannaz's avatar
Francois Gannaz committed
47
48
49
50
51
52
53
        $this->secret = $secret;
        $this->environment = $environment;
    }

    /**
     * @return \Stdclass {code, message}
     */
Francois Gannaz's avatar
Francois Gannaz committed
54
55
    public function getLastError(): ?\Stdclass
    {
Francois Gannaz's avatar
Francois Gannaz committed
56
        return (object) $this->lastError;
Francois Gannaz's avatar
Francois Gannaz committed
57
58
59
60
61
62
63
64
65
66
    }

    /**
     * Query the LabNbook API.
     *
     * @param string $verb E.g. "GET"
     * @param string $urlPath E.g. "/v1/mission"
     * @param array $payload Default is empty. Else any assoc array.
     * @return mixed
     */
Francois Gannaz's avatar
Francois Gannaz committed
67
68
69
70
    public function fetch(string $verb, string $urlPath, array $payload = [])
    {
        $curl = \curl_init();
        \curl_setopt_array($curl, [
Francois Gannaz's avatar
Francois Gannaz committed
71
72
73
74
75
76
77
78
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CUSTOMREQUEST => $verb,
            CURLOPT_URL => $this->createUrl($urlPath),
            CURLOPT_HTTPHEADER => [
                'Content-Type: application/json',
                'Accept: application/json',
                'Authorization: Bearer ' . $this->encodeJwtToken($urlPath, $payload),
            ],
Francois Gannaz's avatar
Francois Gannaz committed
79
            CURLOPT_CONNECTTIMEOUT => self::TIMEOUT,
Francois Gannaz's avatar
Francois Gannaz committed
80
        ]);
Francois Gannaz's avatar
Francois Gannaz committed
81
82
        $response = \curl_exec($curl);
        if (\curl_errno($curl) !== 0) {
Francois Gannaz's avatar
Francois Gannaz committed
83
            $this->lastError = (object) [
David Beniamine's avatar
David Beniamine committed
84
                'code' => \curl_getinfo($ch, CURLINFO_HTTP_CODE),
Francois Gannaz's avatar
Francois Gannaz committed
85
                'message' => \curl_error($curl),
Francois Gannaz's avatar
Francois Gannaz committed
86
87
            ];
        }
Francois Gannaz's avatar
Francois Gannaz committed
88
89
        $error = $this->validateResponse($curl, $response);
        if ($error) {
Francois Gannaz's avatar
Francois Gannaz committed
90
91
            \curl_close($curl);
            \error_log("Query $verb $urlPath to labnbook API failed. $error");
Francois Gannaz's avatar
Francois Gannaz committed
92
93
94
95
            throw new \Exception("HTTP request failed.");
        }

        // send decoded response?
Francois Gannaz's avatar
Francois Gannaz committed
96
97
        $contentType = \curl_getinfo($curl, CURLINFO_CONTENT_TYPE);
        \curl_close($curl);
Francois Gannaz's avatar
Francois Gannaz committed
98
        if ($contentType === 'application/json') {
Francois Gannaz's avatar
Francois Gannaz committed
99
            return \json_decode($response);
Francois Gannaz's avatar
Francois Gannaz committed
100
        }
Francois Gannaz's avatar
Francois Gannaz committed
101
        return $response;
Francois Gannaz's avatar
Francois Gannaz committed
102
103
    }

Francois Gannaz's avatar
Francois Gannaz committed
104
105
106
    public function createUrl(string $urlPath): string
    {
        return $this->baseUrl . '/' . \ltrim($urlPath, '/');
Francois Gannaz's avatar
Francois Gannaz committed
107
108
109
110
111
112
113
114
115
116
    }

    /**
     * Returns the JS source code that will fetch the data in a promise.
     *
     * @param string $verb E.g. "GET"
     * @param string $urlPath E.g. "/v1/mission"
     * @param array $payload Default is empty. Else any assoc array.
     * @return string JS source code
     */
Francois Gannaz's avatar
Francois Gannaz committed
117
118
    public function getJsFetch(string $verb, string $urlPath, array $payload = [])
    {
Francois Gannaz's avatar
Francois Gannaz committed
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
        $url = \json_encode($this->createUrl($urlPath));
        $verb = \json_encode($verb);
        $token = $this->encodeJwtToken($urlPath, $payload);
        // no cache, no cookies, no referrer
        return <<<EOJS
fetch($url, {
    method: $verb,
    cache: 'no-cache',
    credentials: 'omit',
    headers: {
        'Content-Type': 'application/json',
        Authorization: 'Bearer $token',
    },
    mode: 'cors',
    referrer: 'no-referrer',
})
EOJS;
    }

Francois Gannaz's avatar
Francois Gannaz committed
138
139
    public function encodeJwtToken(string $urlPath, array $payload): string
    {
Francois Gannaz's avatar
Francois Gannaz committed
140
141
142
143
        $jwtPayload = $this->environment->createPayload();
        if ($payload) {
            $jwtPayload['data'] = $payload;
        }
Francois Gannaz's avatar
Francois Gannaz committed
144
145
        $jwtPayload['dest'] = '/' . \trim($urlPath, '/');
        $jwtPayload['iat'] = \time(); // UTC timestamp
Francois Gannaz's avatar
Francois Gannaz committed
146
147
148
149
150
151
152
153
        return JWT::encode($jwtPayload, $this->secret, 'HS256');
    }

    /**
     * @param resource $curl
     * @param resource $response
     * @return string if OK "", else error message
     */
Francois Gannaz's avatar
Francois Gannaz committed
154
155
    protected function validateResponse($curl, $response)
    {
Francois Gannaz's avatar
Francois Gannaz committed
156
        if ($response === false) {
Francois Gannaz's avatar
Francois Gannaz committed
157
158
            $this->lastError = (object) ['message' => "Network error? " . \curl_error($curl)];
            return "Network error? " . \curl_error($curl);
Francois Gannaz's avatar
Francois Gannaz committed
159
        }
Francois Gannaz's avatar
Francois Gannaz committed
160
        $responseCode = \curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
Francois Gannaz's avatar
Francois Gannaz committed
161
        if ($responseCode != 200) {
Francois Gannaz's avatar
Francois Gannaz committed
162
163
            $this->lastError = \json_decode($response); // Do not verify signature?
            if (\json_last_error() !== JSON_ERROR_NONE) {
Francois Gannaz's avatar
Francois Gannaz committed
164
165
166
167
168
169
170
171
                $this->lastError = (object) ['message' => $response];
            }
            $this->lastError->code = $responseCode;
            return "HTTP code $responseCode, response: " . $response;
        }
        return "";
    }
}