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 * Badge assertion library.
19 *
20 * @package    core
21 * @subpackage badges
22 * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
23 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 * @author     Yuliya Bozhko <yuliya.bozhko@totaralms.com>
25 */
26
27defined('MOODLE_INTERNAL') || die();
28
29/**
30 * Open Badges Assertions specification 1.0 {@link https://github.com/mozilla/openbadges/wiki/Assertions}
31 *
32 * Badge asserion is defined by three parts:
33 * - Badge Assertion (information regarding a specific badge that was awarded to a badge earner)
34 * - Badge Class (general information about a badge and what it is intended to represent)
35 * - Issuer Class (general information of an issuing organisation)
36 */
37require_once($CFG->libdir . '/badgeslib.php');
38require_once($CFG->dirroot . '/badges/renderer.php');
39
40/**
41 * Class that represents badge assertion.
42 *
43 */
44class core_badges_assertion {
45    /** @var object Issued badge information from database */
46    private $_data;
47
48    /** @var moodle_url Issued badge url */
49    private $_url;
50
51    /** @var int $obversion to control version JSON-LD. */
52    private $_obversion = OPEN_BADGES_V2;
53
54    /**
55     * Constructs with issued badge unique hash.
56     *
57     * @param string $hash Badge unique hash from badge_issued table.
58     * @param int $obversion to control version JSON-LD.
59     */
60    public function __construct($hash, $obversion = OPEN_BADGES_V2) {
61        global $DB;
62
63        $this->_data = $DB->get_record_sql('
64            SELECT
65                bi.dateissued,
66                bi.dateexpire,
67                bi.uniquehash,
68                u.email,
69                b.*,
70                bb.email as backpackemail
71            FROM
72                {badge} b
73                JOIN {badge_issued} bi
74                    ON b.id = bi.badgeid
75                JOIN {user} u
76                    ON u.id = bi.userid
77                LEFT JOIN {badge_backpack} bb
78                    ON bb.userid = bi.userid
79            WHERE ' . $DB->sql_compare_text('bi.uniquehash', 40) . ' = ' . $DB->sql_compare_text(':hash', 40),
80            array('hash' => $hash), IGNORE_MISSING);
81
82        if ($this->_data) {
83            $this->_url = new moodle_url('/badges/badge.php', array('hash' => $this->_data->uniquehash));
84        } else {
85            $this->_url = new moodle_url('/badges/badge.php');
86        }
87        $this->_obversion = $obversion;
88    }
89
90    /**
91     * Get the local id for this badge.
92     *
93     * @return int
94     */
95    public function get_badge_id() {
96        $badgeid = 0;
97        if ($this->_data) {
98            $badgeid = $this->_data->id;
99        }
100        return $badgeid;
101    }
102
103    /**
104     * Get the local id for this badge assertion.
105     *
106     * @return string
107     */
108    public function get_assertion_hash() {
109        $hash = '';
110        if ($this->_data) {
111            $hash = $this->_data->uniquehash;
112        }
113        return $hash;
114    }
115
116    /**
117     * Get badge assertion.
118     *
119     * @param boolean $issued Include the nested badge issued information.
120     * @param boolean $usesalt Hash the identity and include the salt information for the hash.
121     * @return array Badge assertion.
122     */
123    public function get_badge_assertion($issued = true, $usesalt = true) {
124        global $CFG;
125        $assertion = array();
126        if ($this->_data) {
127            $hash = $this->_data->uniquehash;
128            $email = empty($this->_data->backpackemail) ? $this->_data->email : $this->_data->backpackemail;
129            $assertionurl = new moodle_url('/badges/assertion.php', array('b' => $hash, 'obversion' => $this->_obversion));
130
131            if ($this->_obversion >= OPEN_BADGES_V2) {
132                $classurl = new moodle_url('/badges/badge_json.php', array('id' => $this->get_badge_id()));
133            } else {
134                $classurl = new moodle_url('/badges/assertion.php', array('b' => $hash, 'action' => 1));
135            }
136
137            // Required.
138            $assertion['uid'] = $hash;
139            $assertion['recipient'] = array();
140            if ($usesalt) {
141                $assertion['recipient']['identity'] = 'sha256$' . hash('sha256', $email . $CFG->badges_badgesalt);
142            } else {
143                $assertion['recipient']['identity'] = $email;
144            }
145            $assertion['recipient']['type'] = 'email'; // Currently the only supported type.
146            $assertion['recipient']['hashed'] = true; // We are always hashing recipient.
147            if ($usesalt) {
148                $assertion['recipient']['salt'] = $CFG->badges_badgesalt;
149            }
150            if ($issued) {
151                $assertion['badge'] = $classurl->out(false);
152            }
153            $assertion['verify'] = array();
154            $assertion['verify']['type'] = 'hosted'; // 'Signed' is not implemented yet.
155            $assertion['verify']['url'] = $assertionurl->out(false);
156            $assertion['issuedOn'] = $this->_data->dateissued;
157            if ($issued) {
158                $assertion['evidence'] = $this->_url->out(false); // Currently issued badge URL.
159            }
160            // Optional.
161            if (!empty($this->_data->dateexpire)) {
162                $assertion['expires'] = $this->_data->dateexpire;
163            }
164            $this->embed_data_badge_version2($assertion, OPEN_BADGES_V2_TYPE_ASSERTION);
165        }
166        return $assertion;
167    }
168
169    /**
170     * Get badge class information.
171     *
172     * @param boolean $issued Include the nested badge issuer information.
173     * @return array Badge Class information.
174     */
175    public function get_badge_class($issued = true) {
176        $class = [];
177        if ($this->_data) {
178            if (empty($this->_data->courseid)) {
179                $context = context_system::instance();
180            } else {
181                $context = context_course::instance($this->_data->courseid);
182            }
183            // Required.
184            $class['name'] = $this->_data->name;
185            $class['description'] = $this->_data->description;
186            $storage = get_file_storage();
187            $imagefile = $storage->get_file($context->id, 'badges', 'badgeimage', $this->_data->id, '/', 'f3.png');
188            if ($imagefile) {
189                $imagedata = base64_encode($imagefile->get_content());
190            } else {
191                if (defined('PHPUNIT_TEST') && PHPUNIT_TEST) {
192                    // Unit tests the file might not exist yet.
193                    $imagedata = '';
194                } else {
195                    throw new coding_exception('Image file does not exist.');
196                }
197            }
198            $class['image'] = 'data:image/png;base64,' . $imagedata;
199            $class['criteria'] = $this->_url->out(false); // Currently issued badge URL.
200            if ($issued) {
201                $params = ['id' => $this->get_badge_id(), 'obversion' => $this->_obversion];
202                $issuerurl = new moodle_url('/badges/issuer_json.php', $params);
203                $class['issuer'] = $issuerurl->out(false);
204            }
205            $this->embed_data_badge_version2($class, OPEN_BADGES_V2_TYPE_BADGE);
206            if (!$issued) {
207                unset($class['issuer']);
208            }
209        }
210        return $class;
211    }
212
213    /**
214     * Get badge issuer information.
215     *
216     * @return array Issuer information.
217     */
218    public function get_issuer() {
219        global $CFG;
220        $issuer = array();
221        if ($this->_data) {
222            // Required.
223            if ($this->_obversion == OPEN_BADGES_V1) {
224                $issuer['name'] = $this->_data->issuername;
225                $issuer['url'] = $this->_data->issuerurl;
226                // Optional.
227                if (!empty($this->_data->issuercontact)) {
228                    $issuer['email'] = $this->_data->issuercontact;
229                } else {
230                    $issuer['email'] = $CFG->badges_defaultissuercontact;
231                }
232            } else {
233                $badge = new badge($this->get_badge_id());
234                $issuer = $badge->get_badge_issuer();
235            }
236        }
237        $this->embed_data_badge_version2($issuer, OPEN_BADGES_V2_TYPE_ISSUER);
238        return $issuer;
239    }
240
241    /**
242     * Get related badges of the badge.
243     *
244     * @param badge $badge Badge object.
245     * @return array|bool List related badges.
246     */
247    public function get_related_badges(badge $badge) {
248        global $DB;
249        $arraybadges = array();
250        $relatedbadges = $badge->get_related_badges(true);
251        if ($relatedbadges) {
252            foreach ($relatedbadges as $rb) {
253                $url = new moodle_url('/badges/badge_json.php', array('id' => $rb->id));
254                $arraybadges[] = array(
255                    'id'        => $url->out(false),
256                    'version'   => $rb->version,
257                    '@language' => $rb->language
258                );
259            }
260        }
261        return $arraybadges;
262    }
263
264    /**
265     * Get endorsement of the badge.
266     *
267     * @return false|stdClass Endorsement information.
268     */
269    public function get_endorsement() {
270        global $DB;
271        $endorsement = array();
272        $record = $DB->get_record_select('badge_endorsement', 'badgeid = ?', array($this->_data->id));
273        return $record;
274    }
275
276    /**
277     * Get criteria of badge class.
278     *
279     * @return array|string Criteria information.
280     */
281    public function get_criteria_badge_class() {
282        $badge = new badge($this->_data->id);
283        $narrative = $badge->markdown_badge_criteria();
284        if (!empty($narrative)) {
285            $criteria = array();
286            $criteria['id'] = $this->_url->out(false);
287            $criteria['narrative'] = $narrative;
288            return $criteria;
289        } else {
290            return $this->_url->out(false);
291        }
292    }
293
294    /**
295     * Get alignment of the badge.
296     *
297     * @return array information.
298     */
299    public function get_alignments() {
300        global $DB;
301        $badgeid = $this->_data->id;
302        $alignments = array();
303        $items = $DB->get_records_select('badge_alignment', 'badgeid = ?', array($badgeid));
304        foreach ($items as $item) {
305            $alignment = array('targetName' => $item->targetname, 'targetUrl' => $item->targeturl);
306            if ($item->targetdescription) {
307                $alignment['targetDescription'] = $item->targetdescription;
308            }
309            if ($item->targetframework) {
310                $alignment['targetFramework'] = $item->targetframework;
311            }
312            if ($item->targetcode) {
313                $alignment['targetCode'] = $item->targetcode;
314            }
315            $alignments[] = $alignment;
316        }
317        return $alignments;
318    }
319
320    /**
321     * Embed data of Open Badges Specification Version 2.0 to json.
322     *
323     * @param array $json for assertion, badges, issuer.
324     * @param string $type Content type.
325     */
326    protected function embed_data_badge_version2 (&$json, $type = OPEN_BADGES_V2_TYPE_ASSERTION) {
327        // Specification Version 2.0.
328        if ($this->_obversion >= OPEN_BADGES_V2) {
329            $badge = new badge($this->_data->id);
330            if (empty($this->_data->courseid)) {
331                $context = context_system::instance();
332            } else {
333                $context = context_course::instance($this->_data->courseid);
334            }
335
336            $hash = $this->_data->uniquehash;
337            $assertionsurl = new moodle_url('/badges/assertion.php', array('b' => $hash, 'obversion' => $this->_obversion));
338            $classurl = new moodle_url(
339                '/badges/badge_json.php',
340                array('id' => $this->get_badge_id())
341            );
342            $issuerurl = new moodle_url('/badges/issuer_json.php', ['id' => $this->get_badge_id()]);
343            // For assertion.
344            if ($type == OPEN_BADGES_V2_TYPE_ASSERTION) {
345                $json['@context'] = OPEN_BADGES_V2_CONTEXT;
346                $json['type'] = OPEN_BADGES_V2_TYPE_ASSERTION;
347                $json['id'] = $assertionsurl->out(false);
348                $json['badge'] = $this->get_badge_class();
349                $json['issuedOn'] = date('c', $this->_data->dateissued);
350                if (!empty($this->_data->dateexpire)) {
351                    $json['expires'] = date('c', $this->_data->dateexpire);
352                }
353                unset($json['uid']);
354            }
355            // For Badge.
356            if ($type == OPEN_BADGES_V2_TYPE_BADGE) {
357                $json['@context'] = OPEN_BADGES_V2_CONTEXT;
358                $json['id'] = $classurl->out(false);
359                $json['type'] = OPEN_BADGES_V2_TYPE_BADGE;
360                $json['version'] = $this->_data->version;
361                $json['criteria'] = $this->get_criteria_badge_class();
362                $json['issuer'] = $this->get_issuer();
363                $json['@language'] = $this->_data->language;
364                if (!empty($relatedbadges = $this->get_related_badges($badge))) {
365                    $json['related'] = $relatedbadges;
366                }
367                if ($endorsement = $this->get_endorsement()) {
368                    $endorsementurl = new moodle_url('/badges/endorsement_json.php', array('id' => $this->_data->id));
369                    $json['endorsement'] = $endorsementurl->out(false);
370                }
371                if ($alignments = $this->get_alignments()) {
372                    $json['alignments'] = $alignments;
373                }
374                if ($this->_data->imageauthorname ||
375                        $this->_data->imageauthoremail ||
376                        $this->_data->imageauthorurl ||
377                        $this->_data->imagecaption) {
378                    $storage = get_file_storage();
379                    $imagefile = $storage->get_file($context->id, 'badges', 'badgeimage', $this->_data->id, '/', 'f1.png');
380                    if ($imagefile) {
381                        $imagedata = base64_encode($imagefile->get_content());
382                    } else {
383                        // The file might not exist in unit tests.
384                        if (defined('PHPUNIT_TEST') && PHPUNIT_TEST) {
385                            $imagedata = '';
386                        } else {
387                            throw new coding_exception('Image file does not exist.');
388                        }
389                    }
390                    $json['image'] = 'data:image/png;base64,' . $imagedata;
391                }
392            }
393
394            // For issuer.
395            if ($type == OPEN_BADGES_V2_TYPE_ISSUER) {
396                $json['@context'] = OPEN_BADGES_V2_CONTEXT;
397                $json['id'] = $issuerurl->out(false);
398                $json['type'] = OPEN_BADGES_V2_TYPE_ISSUER;
399            }
400        }
401    }
402}
403