Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

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

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