Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.
   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
  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          $serverversions = $serversupports->versions;
  74  
  75          // Calculate the intersections and sort to determine the highest combined version.
  76          $versions = array_intersect($clientversions, $serverversions);
  77          if (count($versions) === 0) {
  78              // No versions in common.
  79              throw new \moodle_exception('No supported Matrix API versions found.');
  80          }
  81          asort($versions);
  82          $version = array_key_last($versions);
  83  
  84          $classname = \communication_matrix\local\spec::class . '\\' . $version;
  85  
  86          return new $classname(
  87              $serverurl,
  88              $accesstoken,
  89          );
  90      }
  91  
  92      /**
  93       * Determine if the API supports a feature.
  94       *
  95       * If an Array is provided, this will return true if any of the specified features is implemented.
  96       *
  97       * @param string[]|string $feature The feature to check. This is in the form of a namespaced class.
  98       * @return bool
  99       */
 100      public function implements_feature(array|string $feature): bool {
 101          if (is_array($feature)) {
 102              foreach ($feature as $thisfeature) {
 103                  if ($this->implements_feature($thisfeature)) {
 104                      return true;
 105                  }
 106              }
 107  
 108              // None of the features are implemented in this API version.
 109              return false;
 110          }
 111  
 112          return in_array($feature, $this->get_supported_features());
 113      }
 114  
 115      /**
 116       * Get a list of the features supported by this client.
 117       *
 118       * @return string[]
 119       */
 120      public function get_supported_features(): array {
 121          $features = [];
 122          $class = static::class;
 123          do {
 124              $features = array_merge($features, class_uses($class));
 125              $class = get_parent_class($class);
 126          } while ($class);
 127  
 128          return $features;
 129      }
 130  
 131      /**
 132       * Require that the API supports a feature.
 133       *
 134       * If an Array is provided, this is treated as a require any of the features.
 135       *
 136       * @param string[]|string $feature The feature to test
 137       * @throws \moodle_exception
 138       */
 139      public function require_feature(array|string $feature): void {
 140          if (!$this->implements_feature($feature)) {
 141              if (is_array($feature)) {
 142                  $features = implode(', ', $feature);
 143                  throw new \moodle_exception(
 144                      "None of the possible feature are implemented in this Matrix Client: '{$features}'"
 145                  );
 146              }
 147              throw new \moodle_exception("The requested feature is not implemented in this Matrix Client: '{$feature}'");
 148          }
 149      }
 150  
 151      /**
 152       * Require that the API supports a list of features.
 153       *
 154       * All features specified will be required.
 155       *
 156       * If an array is provided as one of the features, any of the items in the nested array will be required.
 157       *
 158       * @param string[]|array[] $features The list of features required
 159       *
 160       * Here is an example usage:
 161       * <code>
 162       * $matrixapi->require_features([
 163       *
 164       *     \communication_matrix\local\spec\features\create_room::class,
 165       *     [
 166       *         \communication_matrix\local\spec\features\get_room_info_v1::class,
 167       *         \communication_matrix\local\spec\features\get_room_info_v2::class,
 168       *     ]
 169       * ])
 170       * </code>
 171       */
 172      public function require_features(array $features): void {
 173          array_walk($features, [$this, 'require_feature']);
 174      }
 175  
 176      /**
 177       * Get the URL of the server.
 178       *
 179       * @return string
 180       */
 181      public function get_server_url(): string {
 182          return $this->serverurl;
 183      }
 184  
 185      /**
 186       * Query the supported versions, and any unstable features, from the server.
 187       *
 188       * Servers must implement the client versions API described here:
 189       * - https://spec.matrix.org/latest/client-server-api/#get_matrixclientversions
 190       *
 191       * @param string $serverurl The server base
 192       * @return \stdClass The list of supported versions and a list of enabled unstable features
 193       */
 194      protected static function query_server_supports(string $serverurl): \stdClass {
 195          // Attempt to return from the cache first.
 196          $cache = \cache::make('communication_matrix', 'serverversions');
 197          $serverkey = sha1($serverurl);
 198          if ($cache->get($serverkey)) {
 199              return $cache->get($serverkey);
 200          }
 201  
 202          // Not in the cache - fetch and store in the cache.
 203          $client = static::get_http_client();
 204          $response = $client->get("{$serverurl}/_matrix/client/versions");
 205          $supportsdata = json_decode(
 206              json: $response->getBody(),
 207              associative: false,
 208              flags: JSON_THROW_ON_ERROR,
 209          );
 210  
 211          $cache->set($serverkey, $supportsdata);
 212  
 213          return $supportsdata;
 214      }
 215  
 216      /**
 217       * Get the list of supported versions based on the available classes.
 218       *
 219       * @return array
 220       */
 221      public static function get_supported_versions(): array {
 222          $versions = [];
 223          $iterator = new DirectoryIterator(__DIR__ . '/local/spec');
 224          foreach ($iterator as $fileinfo) {
 225              if ($fileinfo->isDir()) {
 226                  continue;
 227              }
 228  
 229              // Get the classname from the filename.
 230              $classname = substr($fileinfo->getFilename(), 0, -4);
 231  
 232              if (!preg_match('/^v\d+p\d+$/', $classname)) {
 233                  // @codeCoverageIgnoreStart
 234                  // This file does not fit the format v[MAJOR]p[MINOR]].
 235                  continue;
 236                  // @codeCoverageIgnoreEnd
 237              }
 238  
 239              $versions[$classname] = "v" . self::get_version_from_classname($classname);
 240          }
 241  
 242          return $versions;
 243      }
 244  
 245      /**
 246       * Get the current token in use.
 247       *
 248       * @return string
 249       */
 250      public function get_token(): string {
 251          return $this->accesstoken;
 252      }
 253  
 254      /**
 255       * Helper to fetch the HTTP Client for the instance.
 256       *
 257       * @return \core\http_client
 258       */
 259      protected function get_client(): \core\http_client {
 260          return static::get_http_client();
 261      }
 262  
 263      /**
 264       * Helper to fetch the HTTP Client.
 265       *
 266       * @return \core\http_client
 267       */
 268      protected static function get_http_client(): \core\http_client {
 269          if (static::$client !== null) {
 270              return static::$client;
 271          }
 272          // @codeCoverageIgnoreStart
 273          return new http_client();
 274          // @codeCoverageIgnoreEnd
 275      }
 276  
 277      /**
 278       * Execute the specified command.
 279       *
 280       * @param command $command
 281       * @return Response
 282       */
 283      protected function execute(
 284          command $command,
 285      ): Response {
 286          $client = $this->get_client();
 287          return $client->send(
 288              $command,
 289              $command->get_options(),
 290          );
 291      }
 292  
 293      /**
 294       * Get the API version of the current instance.
 295       *
 296       * @return string
 297       */
 298      public function get_version(): string {
 299          $reflect = new \ReflectionClass(static::class);
 300          $classname = $reflect->getShortName();
 301          return self::get_version_from_classname($classname);
 302      }
 303  
 304      /**
 305       * Normalise an API version from a classname.
 306       *
 307       * @param string $classname The short classname, omitting any namespace or file extension
 308       * @return string The normalised version
 309       */
 310      protected static function get_version_from_classname(string $classname): string {
 311          $classname = str_replace('v', '', $classname);
 312          $classname = str_replace('p', '.', $classname);
 313          return $classname;
 314      }
 315  
 316      /**
 317       * Check if the API version is at least the specified version.
 318       *
 319       * @param string $minversion The minimum API version required
 320       * @return bool
 321       */
 322      public function meets_version(string $minversion): bool {
 323          $thisversion = $this->get_version();
 324          return version_compare($thisversion, $minversion) >= 0;
 325      }
 326  
 327      /**
 328       * Assert that the API version is at least the specified version.
 329       *
 330       * @param string $minversion The minimum API version required
 331       * @throws Exception
 332       */
 333      public function requires_version(string $minversion): void {
 334          if ($this->meets_version($minversion)) {
 335              return;
 336          }
 337  
 338          throw new \moodle_exception("Matrix API version {$minversion} or higher is required for this command.");
 339      }
 340  }