Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
namespace communication_matrix;
18
 
19
use communication_matrix\local\command;
20
use core\http_client;
21
use DirectoryIterator;
22
use Exception;
23
use GuzzleHttp\Psr7\Response;
24
 
25
/**
26
 * The abstract class for a versioned API client for Matrix.
27
 *
28
 * Matrix uses a versioned API, and a handshake occurs between the Client (Moodle) and server, to determine the APIs available.
29
 *
30
 * This client represents a version-less API client.
31
 * Versions are implemented by combining the various features into a versionedclass.
32
 * See v1p1 for example.
33
 *
34
 * @package    communication_matrix
35
 * @copyright  2023 Andrew Lyons <andrew@nicols.co.uk>
36
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37
 */
38
abstract class matrix_client {
39
    /** @var string $serverurl The URL of the home server */
40
    /** @var string $accesstoken The access token of the matrix server */
41
 
42
    /** @var http_client|null The client to use */
43
    protected static http_client|null $client = null;
44
 
45
    /**
46
     * Matrix events constructor to get the room id and refresh token usage if required.
47
     *
48
     * @param string $serverurl The URL of the API server
49
     * @param string $accesstoken The admin access token
50
     */
51
    protected function __construct(
52
        protected string $serverurl,
53
        protected string $accesstoken,
54
    ) {
55
    }
56
 
57
    /**
58
     * Return the versioned instance of the API.
59
     *
60
     * @param string $serverurl The URL of the API server
61
     * @param string $accesstoken The admin access token to use
62
     * @return matrix_client|null
63
     */
64
    public static function instance(
65
        string $serverurl,
66
        string $accesstoken,
67
    ): ?matrix_client {
68
        // Fetch the list of supported API versions.
69
        $clientversions = self::get_supported_versions();
70
 
71
        // Fetch the supported versions from the server.
72
        $serversupports = self::query_server_supports($serverurl);
73
        if ($serversupports === null) {
74
            // Unable to fetch the server versions.
75
            return null;
76
        }
77
        $serverversions = $serversupports->versions;
78
 
79
        // Calculate the intersections and sort to determine the highest combined version.
80
        $versions = array_intersect($clientversions, $serverversions);
81
        if (count($versions) === 0) {
82
            // No versions in common.
83
            throw new \moodle_exception('No supported Matrix API versions found.');
84
        }
85
        asort($versions);
86
        $version = array_key_last($versions);
87
 
88
        $classname = \communication_matrix\local\spec::class . '\\' . $version;
89
 
90
        return new $classname(
91
            $serverurl,
92
            $accesstoken,
93
        );
94
    }
95
 
96
    /**
97
     * Determine if the API supports a feature.
98
     *
99
     * If an Array is provided, this will return true if any of the specified features is implemented.
100
     *
101
     * @param string[]|string $feature The feature to check. This is in the form of a namespaced class.
102
     * @return bool
103
     */
104
    public function implements_feature(array|string $feature): bool {
105
        if (is_array($feature)) {
106
            foreach ($feature as $thisfeature) {
107
                if ($this->implements_feature($thisfeature)) {
108
                    return true;
109
                }
110
            }
111
 
112
            // None of the features are implemented in this API version.
113
            return false;
114
        }
115
 
116
        return in_array($feature, $this->get_supported_features());
117
    }
118
 
119
    /**
120
     * Get a list of the features supported by this client.
121
     *
122
     * @return string[]
123
     */
124
    public function get_supported_features(): array {
125
        $features = [];
126
        $class = static::class;
127
        do {
128
            $features = array_merge($features, class_uses($class));
129
            $class = get_parent_class($class);
130
        } while ($class);
131
 
132
        return $features;
133
    }
134
 
135
    /**
136
     * Require that the API supports a feature.
137
     *
138
     * If an Array is provided, this is treated as a require any of the features.
139
     *
140
     * @param string[]|string $feature The feature to test
141
     * @throws \moodle_exception
142
     */
143
    public function require_feature(array|string $feature): void {
144
        if (!$this->implements_feature($feature)) {
145
            if (is_array($feature)) {
146
                $features = implode(', ', $feature);
147
                throw new \moodle_exception(
148
                    "None of the possible feature are implemented in this Matrix Client: '{$features}'"
149
                );
150
            }
151
            throw new \moodle_exception("The requested feature is not implemented in this Matrix Client: '{$feature}'");
152
        }
153
    }
154
 
155
    /**
156
     * Require that the API supports a list of features.
157
     *
158
     * All features specified will be required.
159
     *
160
     * If an array is provided as one of the features, any of the items in the nested array will be required.
161
     *
162
     * @param string[]|array[] $features The list of features required
163
     *
164
     * Here is an example usage:
165
     * <code>
166
     * $matrixapi->require_features([
167
     *
168
     *     \communication_matrix\local\spec\features\create_room::class,
169
     *     [
170
     *         \communication_matrix\local\spec\features\get_room_info_v1::class,
171
     *         \communication_matrix\local\spec\features\get_room_info_v2::class,
172
     *     ]
173
     * ])
174
     * </code>
175
     */
176
    public function require_features(array $features): void {
177
        array_walk($features, [$this, 'require_feature']);
178
    }
179
 
180
    /**
181
     * Get the URL of the server.
182
     *
183
     * @return string
184
     */
185
    public function get_server_url(): string {
186
        return $this->serverurl;
187
    }
188
 
189
    /**
190
     * Query the supported versions, and any unstable features, from the server.
191
     *
192
     * Servers must implement the client versions API described here:
193
     * - https://spec.matrix.org/latest/client-server-api/#get_matrixclientversions
194
     *
195
     * @param string $serverurl The server base
196
     * @return null|\stdClass The list of supported versions and a list of enabled unstable features
197
     */
198
    protected static function query_server_supports(string $serverurl): ?\stdClass {
199
        // Attempt to return from the cache first.
200
        $cache = \cache::make('communication_matrix', 'serverversions');
201
        $serverkey = sha1($serverurl);
202
        if ($cache->get($serverkey)) {
203
            return $cache->get($serverkey);
204
        }
205
 
206
        // Not in the cache - fetch and store in the cache.
207
        try {
208
            $client = static::get_http_client();
209
            $response = $client->get("{$serverurl}/_matrix/client/versions");
210
 
211
            $supportsdata = json_decode(
212
                json: $response->getBody(),
213
                associative: false,
214
                flags: JSON_THROW_ON_ERROR,
215
            );
216
 
217
            $cache->set($serverkey, $supportsdata);
218
            return $supportsdata;
219
        } catch (\GuzzleHttp\Exception\TransferException $e) {
220
            return null;
221
        }
222
    }
223
 
224
    /**
225
     * Get the list of supported versions based on the available classes.
226
     *
227
     * @return array
228
     */
229
    public static function get_supported_versions(): array {
230
        $versions = [];
231
        $iterator = new DirectoryIterator(__DIR__ . '/local/spec');
232
        foreach ($iterator as $fileinfo) {
233
            if ($fileinfo->isDir()) {
234
                continue;
235
            }
236
 
237
            // Get the classname from the filename.
238
            $classname = substr($fileinfo->getFilename(), 0, -4);
239
 
240
            if (!preg_match('/^v\d+p\d+$/', $classname)) {
241
                // @codeCoverageIgnoreStart
242
                // This file does not fit the format v[MAJOR]p[MINOR]].
243
                continue;
244
                // @codeCoverageIgnoreEnd
245
            }
246
 
247
            $versions[$classname] = "v" . self::get_version_from_classname($classname);
248
        }
249
 
250
        return $versions;
251
    }
252
 
253
    /**
254
     * Get the current token in use.
255
     *
256
     * @return string
257
     */
258
    public function get_token(): string {
259
        return $this->accesstoken;
260
    }
261
 
262
    /**
263
     * Helper to fetch the HTTP Client for the instance.
264
     *
265
     * @return \core\http_client
266
     */
267
    protected function get_client(): \core\http_client {
268
        return static::get_http_client();
269
    }
270
 
271
    /**
272
     * Helper to fetch the HTTP Client.
273
     *
274
     * @return \core\http_client
275
     */
276
    protected static function get_http_client(): \core\http_client {
277
        if (static::$client !== null) {
278
            return static::$client;
279
        }
280
        // @codeCoverageIgnoreStart
281
        return new http_client();
282
        // @codeCoverageIgnoreEnd
283
    }
284
 
285
    /**
286
     * Execute the specified command.
287
     *
288
     * @param command $command
289
     * @return Response
290
     */
291
    protected function execute(
292
        command $command,
293
    ): Response {
294
        $client = $this->get_client();
295
        return $client->send(
296
            $command,
297
            $command->get_options(),
298
        );
299
    }
300
 
301
    /**
302
     * Get the API version of the current instance.
303
     *
304
     * @return string
305
     */
306
    public function get_version(): string {
307
        $reflect = new \ReflectionClass(static::class);
308
        $classname = $reflect->getShortName();
309
        return self::get_version_from_classname($classname);
310
    }
311
 
312
    /**
313
     * Normalise an API version from a classname.
314
     *
315
     * @param string $classname The short classname, omitting any namespace or file extension
316
     * @return string The normalised version
317
     */
318
    protected static function get_version_from_classname(string $classname): string {
319
        $classname = str_replace('v', '', $classname);
320
        $classname = str_replace('p', '.', $classname);
321
        return $classname;
322
    }
323
 
324
    /**
325
     * Check if the API version is at least the specified version.
326
     *
327
     * @param string $minversion The minimum API version required
328
     * @return bool
329
     */
330
    public function meets_version(string $minversion): bool {
331
        $thisversion = $this->get_version();
332
        return version_compare($thisversion, $minversion) >= 0;
333
    }
334
 
335
    /**
336
     * Assert that the API version is at least the specified version.
337
     *
338
     * @param string $minversion The minimum API version required
339
     * @throws Exception
340
     */
341
    public function requires_version(string $minversion): void {
342
        if ($this->meets_version($minversion)) {
343
            return;
344
        }
345
 
346
        throw new \moodle_exception("Matrix API version {$minversion} or higher is required for this command.");
347
    }
348
}