Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]

   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  /**
  18   * Represent the url for each method and the encoding of the parameters and response.
  19   *
  20   * @package    core_badges
  21   * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   * @author     Yuliya Bozhko <yuliya.bozhko@totaralms.com>
  24   */
  25  
  26  namespace core_badges;
  27  
  28  defined('MOODLE_INTERNAL') || die();
  29  
  30  global $CFG;
  31  require_once($CFG->libdir . '/filelib.php');
  32  
  33  use context_system;
  34  use core_badges\external\assertion_exporter;
  35  use core_badges\external\collection_exporter;
  36  use core_badges\external\issuer_exporter;
  37  use core_badges\external\badgeclass_exporter;
  38  use curl;
  39  
  40  /**
  41   * Represent a single method for the remote api.
  42   *
  43   * @package    core_badges
  44   * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
  45   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  46   */
  47  class backpack_api_mapping {
  48  
  49      /** @var string The action of this method. */
  50      public $action;
  51  
  52      /** @var string The base url of this backpack. */
  53      private $url;
  54  
  55      /** @var array List of parameters for this method. */
  56      public $params;
  57  
  58      /** @var string Name of a class to export parameters for this method. */
  59      public $requestexporter;
  60  
  61      /** @var string Name of a class to export response for this method. */
  62      public $responseexporter;
  63  
  64      /** @var boolean This method returns an array of responses. */
  65      public $multiple;
  66  
  67      /** @var string get or post methods. */
  68      public $method;
  69  
  70      /** @var boolean json decode the response. */
  71      public $json;
  72  
  73      /** @var boolean Authentication is required for this request. */
  74      public $authrequired;
  75  
  76      /** @var boolean Differentiate the function that can be called on a user backpack or a site backpack. */
  77      private $isuserbackpack;
  78  
  79      /** @var string Error string from authentication request. */
  80      private static $authenticationerror = '';
  81  
  82      /**
  83       * Create a mapping.
  84       *
  85       * @param string $action The action of this method.
  86       * @param string $url The base url of this backpack.
  87       * @param mixed $postparams List of parameters for this method.
  88       * @param string $requestexporter Name of a class to export parameters for this method.
  89       * @param string $responseexporter Name of a class to export response for this method.
  90       * @param boolean $multiple This method returns an array of responses.
  91       * @param string $method get or post methods.
  92       * @param boolean $json json decode the response.
  93       * @param boolean $authrequired Authentication is required for this request.
  94       * @param boolean $isuserbackpack user backpack or a site backpack.
  95       * @param integer $backpackapiversion OpenBadges version 1 or 2.
  96       */
  97      public function __construct($action, $url, $postparams, $requestexporter, $responseexporter,
  98                                  $multiple, $method, $json, $authrequired, $isuserbackpack, $backpackapiversion) {
  99          $this->action = $action;
 100          $this->url = $url;
 101          $this->postparams = $postparams;
 102          $this->requestexporter = $requestexporter;
 103          $this->responseexporter = $responseexporter;
 104          $this->multiple = $multiple;
 105          $this->method = $method;
 106          $this->json = $json;
 107          $this->authrequired = $authrequired;
 108          $this->isuserbackpack = $isuserbackpack;
 109          $this->backpackapiversion = $backpackapiversion;
 110      }
 111  
 112      /**
 113       * Get the unique key for the token.
 114       *
 115       * @param string $type The type of token.
 116       * @return string
 117       */
 118      private function get_token_key($type) {
 119          $prefix = 'badges_';
 120          if ($this->isuserbackpack) {
 121              $prefix .= 'user_backpack_';
 122          } else {
 123              $prefix .= 'site_backpack_';
 124          }
 125          $prefix .= $type . '_token';
 126          return $prefix;
 127      }
 128  
 129      /**
 130       * Remember the error message in a static variable.
 131       *
 132       * @param string $msg The message.
 133       */
 134      public static function set_authentication_error($msg) {
 135          self::$authenticationerror = $msg;
 136      }
 137  
 138      /**
 139       * Get the last authentication error in this request.
 140       *
 141       * @return string
 142       */
 143      public static function get_authentication_error() {
 144          return self::$authenticationerror;
 145      }
 146  
 147      /**
 148       * Does the action match this mapping?
 149       *
 150       * @param string $action The action.
 151       * @return boolean
 152       */
 153      public function is_match($action) {
 154          return $this->action == $action;
 155      }
 156  
 157      /**
 158       * Parse the method url and insert parameters.
 159       *
 160       * @param string $apiurl The raw apiurl.
 161       * @param string $param1 The first parameter.
 162       * @param string $param2 The second parameter.
 163       * @return string
 164       */
 165      private function get_url($apiurl, $param1, $param2) {
 166          $urlscheme = parse_url($apiurl, PHP_URL_SCHEME);
 167          $urlhost = parse_url($apiurl, PHP_URL_HOST);
 168  
 169          $url = $this->url;
 170          $url = str_replace('[SCHEME]', $urlscheme, $url);
 171          $url = str_replace('[HOST]', $urlhost, $url);
 172          $url = str_replace('[URL]', $apiurl, $url);
 173          $url = str_replace('[PARAM1]', $param1 ?? '', $url);
 174          $url = str_replace('[PARAM2]', $param2 ?? '', $url);
 175  
 176          return $url;
 177      }
 178  
 179      /**
 180       * Parse the post parameters and insert replacements.
 181       *
 182       * @param string $email The api username.
 183       * @param string $password The api password.
 184       * @param string $param The parameter.
 185       * @return mixed
 186       */
 187      private function get_post_params($email, $password, $param) {
 188          global $PAGE;
 189  
 190          if ($this->method == 'get') {
 191              return '';
 192          }
 193  
 194          $request = $this->postparams;
 195          if ($request === '[PARAM]') {
 196              $value = $param;
 197              foreach ($value as $key => $keyvalue) {
 198                  if (gettype($value[$key]) == 'array') {
 199                      $newkey = 'related_' . $key;
 200                      $value[$newkey] = $value[$key];
 201                      unset($value[$key]);
 202                  }
 203              }
 204          } else if (is_array($request)) {
 205              foreach ($request as $key => $value) {
 206                  if ($value == '[EMAIL]') {
 207                      $value = $email;
 208                      $request[$key] = $value;
 209                  } else if ($value == '[PASSWORD]') {
 210                      $value = $password;
 211                      $request[$key] = $value;
 212                  } else if ($value == '[PARAM]') {
 213                      $request[$key] = is_array($param) ? $param[0] : $param;
 214                  }
 215              }
 216          }
 217          $context = context_system::instance();
 218          $exporter = $this->requestexporter;
 219          $output = $PAGE->get_renderer('core', 'badges');
 220          if (!empty($exporter)) {
 221              $exporterinstance = new $exporter($value, ['context' => $context]);
 222              $request = $exporterinstance->export($output);
 223          }
 224          if ($this->json) {
 225              return json_encode($request);
 226          }
 227          return $request;
 228      }
 229  
 230      /**
 231       * Read the response from a V1 user request and save the userID.
 232       *
 233       * @param string $response The request response.
 234       * @param integer $backpackid The backpack id.
 235       * @return mixed
 236       */
 237      private function convert_email_response($response, $backpackid) {
 238          global $SESSION;
 239  
 240          if (isset($response->status) && $response->status == 'okay') {
 241  
 242              // Remember the tokens.
 243              $useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN);
 244              $backpackidkey = $this->get_token_key(BADGE_BACKPACK_ID_TOKEN);
 245  
 246              $SESSION->$useridkey = $response->userId;
 247              $SESSION->$backpackidkey = $backpackid;
 248              return $response->userId;
 249          }
 250          if (!empty($response->error)) {
 251              self::set_authentication_error($response->error);
 252          }
 253          return false;
 254      }
 255  
 256      /**
 257       * Get the user id from a previous user request.
 258       *
 259       * @return integer
 260       */
 261      private function get_auth_user_id() {
 262          global $USER;
 263  
 264          if ($this->isuserbackpack) {
 265              return $USER->id;
 266          } else {
 267              // The access tokens for the system backpack are shared.
 268              return -1;
 269          }
 270      }
 271  
 272      /**
 273       * Parse the response from an openbadges 2 login.
 274       *
 275       * @param string $response The request response data.
 276       * @param integer $backpackid The id of the backpack.
 277       * @return mixed
 278       */
 279      private function oauth_token_response($response, $backpackid) {
 280          global $SESSION;
 281  
 282          if (isset($response->access_token) && isset($response->refresh_token)) {
 283              // Remember the tokens.
 284              $accesskey = $this->get_token_key(BADGE_ACCESS_TOKEN);
 285              $refreshkey = $this->get_token_key(BADGE_REFRESH_TOKEN);
 286              $expireskey = $this->get_token_key(BADGE_EXPIRES_TOKEN);
 287              $useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN);
 288              $backpackidkey = $this->get_token_key(BADGE_BACKPACK_ID_TOKEN);
 289              if (isset($response->expires_in)) {
 290                  $timeout = $response->expires_in;
 291              } else {
 292                  $timeout = 15 * 60; // 15 minute timeout if none set.
 293              }
 294              $expires = $timeout + time();
 295  
 296              $SESSION->$expireskey = $expires;
 297              $SESSION->$useridkey = $this->get_auth_user_id();
 298              $SESSION->$accesskey = $response->access_token;
 299              $SESSION->$refreshkey = $response->refresh_token;
 300              $SESSION->$backpackidkey = $backpackid;
 301              return -1;
 302          } else if (isset($response->error_description)) {
 303              self::set_authentication_error($response->error_description);
 304          }
 305          return $response;
 306      }
 307  
 308      /**
 309       * Standard options used for all curl requests.
 310       *
 311       * @return array
 312       */
 313      private function get_curl_options() {
 314          return array(
 315              'FRESH_CONNECT'     => true,
 316              'RETURNTRANSFER'    => true,
 317              'FOLLOWLOCATION'    => true,
 318              'FORBID_REUSE'      => true,
 319              'HEADER'            => 0,
 320              'CONNECTTIMEOUT'    => 3,
 321              'CONNECTTIMEOUT'    => 3,
 322              // Follow redirects with the same type of request when sent 301, or 302 redirects.
 323              'CURLOPT_POSTREDIR' => 3,
 324          );
 325      }
 326  
 327      /**
 328       * Make an api request and parse the response.
 329       *
 330       * @param string $apiurl Raw request url.
 331       * @param string $urlparam1 Parameter for the request.
 332       * @param string $urlparam2 Parameter for the request.
 333       * @param string $email User email for authentication.
 334       * @param string $password for authentication.
 335       * @param mixed $postparam Raw data for the post body.
 336       * @param string $backpackid the id of the backpack to use.
 337       * @return mixed
 338       */
 339      public function request($apiurl, $urlparam1, $urlparam2, $email, $password, $postparam, $backpackid) {
 340          global $SESSION, $PAGE;
 341  
 342          $curl = new curl();
 343  
 344          $url = $this->get_url($apiurl, $urlparam1, $urlparam2);
 345  
 346          if ($this->authrequired) {
 347              $accesskey = $this->get_token_key(BADGE_ACCESS_TOKEN);
 348              if (isset($SESSION->$accesskey)) {
 349                  $token = $SESSION->$accesskey;
 350                  $curl->setHeader('Authorization: Bearer ' . $token);
 351              }
 352          }
 353          if ($this->json) {
 354              $curl->setHeader(array('Content-type: application/json'));
 355          }
 356          $curl->setHeader(array('Accept: application/json', 'Expect:'));
 357          $options = $this->get_curl_options();
 358  
 359          $post = $this->get_post_params($email, $password, $postparam);
 360  
 361          if ($this->method == 'get') {
 362              $response = $curl->get($url, $post, $options);
 363          } else if ($this->method == 'post') {
 364              $response = $curl->post($url, $post, $options);
 365          } else if ($this->method == 'put') {
 366              $response = $curl->put($url, $post, $options);
 367          }
 368          $response = json_decode($response);
 369          if (isset($response->result)) {
 370              $response = $response->result;
 371          }
 372          $context = context_system::instance();
 373          $exporter = $this->responseexporter;
 374          if (class_exists($exporter)) {
 375              $output = $PAGE->get_renderer('core', 'badges');
 376              if (!$this->multiple) {
 377                  if (count($response)) {
 378                      $response = $response[0];
 379                  }
 380                  if (empty($response)) {
 381                      return null;
 382                  }
 383                  $apidata = $exporter::map_external_data($response, $this->backpackapiversion);
 384                  $exporterinstance = new $exporter($apidata, ['context' => $context]);
 385                  $data = $exporterinstance->export($output);
 386                  return $data;
 387              } else {
 388                  $multiple = [];
 389                  if (empty($response)) {
 390                      return $multiple;
 391                  }
 392                  foreach ($response as $data) {
 393                      $apidata = $exporter::map_external_data($data, $this->backpackapiversion);
 394                      $exporterinstance = new $exporter($apidata, ['context' => $context]);
 395                      $multiple[] = $exporterinstance->export($output);
 396                  }
 397                  return $multiple;
 398              }
 399          } else if (method_exists($this, $exporter)) {
 400              return $this->$exporter($response, $backpackid);
 401          }
 402          return $response;
 403      }
 404  }