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
26namespace core_badges;
27
28defined('MOODLE_INTERNAL') || die();
29
30global $CFG;
31require_once($CFG->libdir . '/filelib.php');
32
33use context_system;
34use core_badges\external\assertion_exporter;
35use core_badges\external\collection_exporter;
36use core_badges\external\issuer_exporter;
37use core_badges\external\badgeclass_exporter;
38use 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 */
47class 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}
400