Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 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                  }
 213              }
 214          }
 215          $context = context_system::instance();
 216          $exporter = $this->requestexporter;
 217          $output = $PAGE->get_renderer('core', 'badges');
 218          if (!empty($exporter)) {
 219              $exporterinstance = new $exporter($value, ['context' => $context]);
 220              $request = $exporterinstance->export($output);
 221          }
 222          if ($this->json) {
 223              return json_encode($request);
 224          }
 225          return $request;
 226      }
 227  
 228      /**
 229       * Read the response from a V1 user request and save the userID.
 230       *
 231       * @param string $response The request response.
 232       * @param integer $backpackid The backpack id.
 233       * @return mixed
 234       */
 235      private function convert_email_response($response, $backpackid) {
 236          global $SESSION;
 237  
 238          if (isset($response->status) && $response->status == 'okay') {
 239  
 240              // Remember the tokens.
 241              $useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN);
 242              $backpackidkey = $this->get_token_key(BADGE_BACKPACK_ID_TOKEN);
 243  
 244              $SESSION->$useridkey = $response->userId;
 245              $SESSION->$backpackidkey = $backpackid;
 246              return $response->userId;
 247          }
 248          if (!empty($response->error)) {
 249              self::set_authentication_error($response->error);
 250          }
 251          return false;
 252      }
 253  
 254      /**
 255       * Get the user id from a previous user request.
 256       *
 257       * @return integer
 258       */
 259      private function get_auth_user_id() {
 260          global $USER;
 261  
 262          if ($this->isuserbackpack) {
 263              return $USER->id;
 264          } else {
 265              // The access tokens for the system backpack are shared.
 266              return -1;
 267          }
 268      }
 269  
 270      /**
 271       * Parse the response from an openbadges 2 login.
 272       *
 273       * @param string $response The request response data.
 274       * @param integer $backpackid The id of the backpack.
 275       * @return mixed
 276       */
 277      private function oauth_token_response($response, $backpackid) {
 278          global $SESSION;
 279  
 280          if (isset($response->access_token) && isset($response->refresh_token)) {
 281              // Remember the tokens.
 282              $accesskey = $this->get_token_key(BADGE_ACCESS_TOKEN);
 283              $refreshkey = $this->get_token_key(BADGE_REFRESH_TOKEN);
 284              $expireskey = $this->get_token_key(BADGE_EXPIRES_TOKEN);
 285              $useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN);
 286              $backpackidkey = $this->get_token_key(BADGE_BACKPACK_ID_TOKEN);
 287              if (isset($response->expires_in)) {
 288                  $timeout = $response->expires_in;
 289              } else {
 290                  $timeout = 15 * 60; // 15 minute timeout if none set.
 291              }
 292              $expires = $timeout + time();
 293  
 294              $SESSION->$expireskey = $expires;
 295              $SESSION->$useridkey = $this->get_auth_user_id();
 296              $SESSION->$accesskey = $response->access_token;
 297              $SESSION->$refreshkey = $response->refresh_token;
 298              $SESSION->$backpackidkey = $backpackid;
 299              return -1;
 300          } else if (isset($response->error_description)) {
 301              self::set_authentication_error($response->error_description);
 302          }
 303          return $response;
 304      }
 305  
 306      /**
 307       * Standard options used for all curl requests.
 308       *
 309       * @return array
 310       */
 311      private function get_curl_options() {
 312          return array(
 313              'FRESH_CONNECT'     => true,
 314              'RETURNTRANSFER'    => true,
 315              'FORBID_REUSE'      => true,
 316              'HEADER'            => 0,
 317              'CONNECTTIMEOUT'    => 3,
 318              'CONNECTTIMEOUT'    => 3,
 319              // Follow redirects with the same type of request when sent 301, or 302 redirects.
 320              'CURLOPT_POSTREDIR' => 3,
 321          );
 322      }
 323  
 324      /**
 325       * Make an api request and parse the response.
 326       *
 327       * @param string $apiurl Raw request url.
 328       * @param string $urlparam1 Parameter for the request.
 329       * @param string $urlparam2 Parameter for the request.
 330       * @param string $email User email for authentication.
 331       * @param string $password for authentication.
 332       * @param mixed $postparam Raw data for the post body.
 333       * @param string $backpackid the id of the backpack to use.
 334       * @return mixed
 335       */
 336      public function request($apiurl, $urlparam1, $urlparam2, $email, $password, $postparam, $backpackid) {
 337          global $SESSION, $PAGE;
 338  
 339          $curl = new curl();
 340  
 341          $url = $this->get_url($apiurl, $urlparam1, $urlparam2);
 342  
 343          if ($this->authrequired) {
 344              $accesskey = $this->get_token_key(BADGE_ACCESS_TOKEN);
 345              if (isset($SESSION->$accesskey)) {
 346                  $token = $SESSION->$accesskey;
 347                  $curl->setHeader('Authorization: Bearer ' . $token);
 348              }
 349          }
 350          if ($this->json) {
 351              $curl->setHeader(array('Content-type: application/json'));
 352          }
 353          $curl->setHeader(array('Accept: application/json', 'Expect:'));
 354          $options = $this->get_curl_options();
 355  
 356          $post = $this->get_post_params($email, $password, $postparam);
 357  
 358          if ($this->method == 'get') {
 359              $response = $curl->get($url, $post, $options);
 360          } else if ($this->method == 'post') {
 361              $response = $curl->post($url, $post, $options);
 362          }
 363          $response = json_decode($response);
 364          if (isset($response->result)) {
 365              $response = $response->result;
 366          }
 367          $context = context_system::instance();
 368          $exporter = $this->responseexporter;
 369          if (class_exists($exporter)) {
 370              $output = $PAGE->get_renderer('core', 'badges');
 371              if (!$this->multiple) {
 372                  if (count($response)) {
 373                      $response = $response[0];
 374                  }
 375                  if (empty($response)) {
 376                      return null;
 377                  }
 378                  $apidata = $exporter::map_external_data($response, $this->backpackapiversion);
 379                  $exporterinstance = new $exporter($apidata, ['context' => $context]);
 380                  $data = $exporterinstance->export($output);
 381                  return $data;
 382              } else {
 383                  $multiple = [];
 384                  if (empty($response)) {
 385                      return $multiple;
 386                  }
 387                  foreach ($response as $data) {
 388                      $apidata = $exporter::map_external_data($data, $this->backpackapiversion);
 389                      $exporterinstance = new $exporter($apidata, ['context' => $context]);
 390                      $multiple[] = $exporterinstance->export($output);
 391                  }
 392                  return $multiple;
 393              }
 394          } else if (method_exists($this, $exporter)) {
 395              return $this->$exporter($response, $backpackid);
 396          }
 397          return $response;
 398      }
 399  }