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
27namespace core_badges;
28
29defined('MOODLE_INTERNAL') || die();
30
31require_once($CFG->dirroot.'/lib/badgeslib.php');
32
33use context_system;
34use context_course;
35use context_user;
36use moodle_exception;
37use moodle_url;
38use core_text;
39use award_criteria;
40use core_php_time_limit;
41use html_writer;
42use stdClass;
43
44/**
45 * Class that represents badge.
46 *
47 * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
48 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
49 */
50class badge {
51    /** @var int Badge id */
52    public $id;
53
54    /** @var string Badge name */
55    public $name;
56
57    /** @var string Badge description */
58    public $description;
59
60    /** @var integer Timestamp this badge was created */
61    public $timecreated;
62
63    /** @var integer Timestamp this badge was modified */
64    public $timemodified;
65
66    /** @var int The user who created this badge */
67    public $usercreated;
68
69    /** @var int The user who modified this badge */
70    public $usermodified;
71
72    /** @var string The name of the issuer of this badge */
73    public $issuername;
74
75    /** @var string The url of the issuer of this badge */
76    public $issuerurl;
77
78    /** @var string The email of the issuer of this badge */
79    public $issuercontact;
80
81    /** @var integer Timestamp this badge will expire */
82    public $expiredate;
83
84    /** @var integer Duration this badge is valid for */
85    public $expireperiod;
86
87    /** @var integer Site or course badge */
88    public $type;
89
90    /** @var integer The course this badge belongs to */
91    public $courseid;
92
93    /** @var string The message this badge includes. */
94    public $message;
95
96    /** @var string The subject of the message for this badge */
97    public $messagesubject;
98
99    /** @var int Is this badge image baked. */
100    public $attachment;
101
102    /** @var int Send a message when this badge is awarded. */
103    public $notification;
104
105    /** @var int Lifecycle status for this badge. */
106    public $status = 0;
107
108    /** @var int Timestamp to next run cron for this badge. */
109    public $nextcron;
110
111    /** @var int What backpack api version to use for this badge. */
112    public $version;
113
114    /** @var string What language is this badge written in. */
115    public $language;
116
117    /** @var string The author of the image for this badge. */
118    public $imageauthorname;
119
120    /** @var string The email of the author of the image for this badge. */
121    public $imageauthoremail;
122
123    /** @var string The url of the author of the image for this badge. */
124    public $imageauthorurl;
125
126    /** @var string The caption of the image for this badge. */
127    public $imagecaption;
128
129    /** @var array Badge criteria */
130    public $criteria = array();
131
132    /**
133     * Constructs with badge details.
134     *
135     * @param int $badgeid badge ID.
136     */
137    public function __construct($badgeid) {
138        global $DB;
139        $this->id = $badgeid;
140
141        $data = $DB->get_record('badge', array('id' => $badgeid));
142
143        if (empty($data)) {
144            print_error('error:nosuchbadge', 'badges', $badgeid);
145        }
146
147        foreach ((array)$data as $field => $value) {
148            if (property_exists($this, $field)) {
149                $this->{$field} = $value;
150            }
151        }
152
153        if (badges_open_badges_backpack_api() != OPEN_BADGES_V1) {
154            // For Open Badges 2 we need to use a single site issuer with no exceptions.
155            $issuer = badges_get_default_issuer();
156            $this->issuername = $issuer['name'];
157            $this->issuercontact = $issuer['email'];
158            $this->issuerurl = $issuer['url'];
159        }
160
161        $this->criteria = self::get_criteria();
162    }
163
164    /**
165     * Use to get context instance of a badge.
166     *
167     * @return context instance.
168     */
169    public function get_context() {
170        if ($this->type == BADGE_TYPE_SITE) {
171            return context_system::instance();
172        } else if ($this->type == BADGE_TYPE_COURSE) {
173            return context_course::instance($this->courseid);
174        } else {
175            debugging('Something is wrong...');
176        }
177    }
178
179    /**
180     * Return array of aggregation methods
181     *
182     * @return array
183     */
184    public static function get_aggregation_methods() {
185        return array(
186                BADGE_CRITERIA_AGGREGATION_ALL => get_string('all', 'badges'),
187                BADGE_CRITERIA_AGGREGATION_ANY => get_string('any', 'badges'),
188        );
189    }
190
191    /**
192     * Return array of accepted criteria types for this badge
193     *
194     * @return array
195     */
196    public function get_accepted_criteria() {
197        global $CFG;
198        $criteriatypes = array();
199
200        if ($this->type == BADGE_TYPE_COURSE) {
201            $criteriatypes = array(
202                    BADGE_CRITERIA_TYPE_OVERALL,
203                    BADGE_CRITERIA_TYPE_MANUAL,
204                    BADGE_CRITERIA_TYPE_COURSE,
205                    BADGE_CRITERIA_TYPE_BADGE,
206                    BADGE_CRITERIA_TYPE_ACTIVITY,
207                    BADGE_CRITERIA_TYPE_COMPETENCY
208            );
209        } else if ($this->type == BADGE_TYPE_SITE) {
210            $criteriatypes = array(
211                    BADGE_CRITERIA_TYPE_OVERALL,
212                    BADGE_CRITERIA_TYPE_MANUAL,
213                    BADGE_CRITERIA_TYPE_COURSESET,
214                    BADGE_CRITERIA_TYPE_BADGE,
215                    BADGE_CRITERIA_TYPE_PROFILE,
216                    BADGE_CRITERIA_TYPE_COHORT,
217                    BADGE_CRITERIA_TYPE_COMPETENCY
218            );
219        }
220        $alltypes = badges_list_criteria();
221        foreach ($criteriatypes as $index => $type) {
222            if (!isset($alltypes[$type])) {
223                unset($criteriatypes[$index]);
224            }
225        }
226
227        return $criteriatypes;
228    }
229
230    /**
231     * Save/update badge information in 'badge' table only.
232     * Cannot be used for updating awards and criteria settings.
233     *
234     * @return boolean Returns true on success.
235     */
236    public function save() {
237        global $DB;
238
239        $fordb = new stdClass();
240        foreach (get_object_vars($this) as $k => $v) {
241            $fordb->{$k} = $v;
242        }
243        unset($fordb->criteria);
244
245        $fordb->timemodified = time();
246        if ($DB->update_record_raw('badge', $fordb)) {
247            // Trigger event, badge updated.
248            $eventparams = array('objectid' => $this->id, 'context' => $this->get_context());
249            $event = \core\event\badge_updated::create($eventparams);
250            $event->trigger();
251            return true;
252        } else {
253            throw new moodle_exception('error:save', 'badges');
254            return false;
255        }
256    }
257
258    /**
259     * Creates and saves a clone of badge with all its properties.
260     * Clone is not active by default and has 'Copy of' attached to its name.
261     *
262     * @return int ID of new badge.
263     */
264    public function make_clone() {
265        global $DB, $USER, $PAGE;
266
267        $fordb = new stdClass();
268        foreach (get_object_vars($this) as $k => $v) {
269            $fordb->{$k} = $v;
270        }
271
272        $fordb->name = get_string('copyof', 'badges', $this->name);
273        $fordb->status = BADGE_STATUS_INACTIVE;
274        $fordb->usercreated = $USER->id;
275        $fordb->usermodified = $USER->id;
276        $fordb->timecreated = time();
277        $fordb->timemodified = time();
278        unset($fordb->id);
279
280        if ($fordb->notification > 1) {
281            $fordb->nextcron = badges_calculate_message_schedule($fordb->notification);
282        }
283
284        $criteria = $fordb->criteria;
285        unset($fordb->criteria);
286
287        if ($new = $DB->insert_record('badge', $fordb, true)) {
288            $newbadge = new badge($new);
289
290            // Copy badge image.
291            $fs = get_file_storage();
292            if ($file = $fs->get_file($this->get_context()->id, 'badges', 'badgeimage', $this->id, '/', 'f3.png')) {
293                if ($imagefile = $file->copy_content_to_temp()) {
294                    badges_process_badge_image($newbadge, $imagefile);
295                }
296            }
297
298            // Copy badge criteria.
299            foreach ($this->criteria as $crit) {
300                $crit->make_clone($new);
301            }
302
303            // Trigger event, badge duplicated.
304            $eventparams = array('objectid' => $new, 'context' => $PAGE->context);
305            $event = \core\event\badge_duplicated::create($eventparams);
306            $event->trigger();
307
308            return $new;
309        } else {
310            throw new moodle_exception('error:clone', 'badges');
311            return false;
312        }
313    }
314
315    /**
316     * Checks if badges is active.
317     * Used in badge award.
318     *
319     * @return boolean A status indicating badge is active
320     */
321    public function is_active() {
322        if (($this->status == BADGE_STATUS_ACTIVE) ||
323            ($this->status == BADGE_STATUS_ACTIVE_LOCKED)) {
324            return true;
325        }
326        return false;
327    }
328
329    /**
330     * Use to get the name of badge status.
331     *
332     * @return string
333     */
334    public function get_status_name() {
335        return get_string('badgestatus_' . $this->status, 'badges');
336    }
337
338    /**
339     * Use to set badge status.
340     * Only active badges can be earned/awarded/issued.
341     *
342     * @param int $status Status from BADGE_STATUS constants
343     */
344    public function set_status($status = 0) {
345        $this->status = $status;
346        $this->save();
347        if ($status == BADGE_STATUS_ACTIVE) {
348            // Trigger event, badge enabled.
349            $eventparams = array('objectid' => $this->id, 'context' => $this->get_context());
350            $event = \core\event\badge_enabled::create($eventparams);
351            $event->trigger();
352        } else if ($status == BADGE_STATUS_INACTIVE) {
353            // Trigger event, badge disabled.
354            $eventparams = array('objectid' => $this->id, 'context' => $this->get_context());
355            $event = \core\event\badge_disabled::create($eventparams);
356            $event->trigger();
357        }
358    }
359
360    /**
361     * Checks if badges is locked.
362     * Used in badge award and editing.
363     *
364     * @return boolean A status indicating badge is locked
365     */
366    public function is_locked() {
367        if (($this->status == BADGE_STATUS_ACTIVE_LOCKED) ||
368                ($this->status == BADGE_STATUS_INACTIVE_LOCKED)) {
369            return true;
370        }
371        return false;
372    }
373
374    /**
375     * Checks if badge has been awarded to users.
376     * Used in badge editing.
377     *
378     * @return boolean A status indicating badge has been awarded at least once
379     */
380    public function has_awards() {
381        global $DB;
382        $awarded = $DB->record_exists_sql('SELECT b.uniquehash
383                    FROM {badge_issued} b INNER JOIN {user} u ON b.userid = u.id
384                    WHERE b.badgeid = :badgeid AND u.deleted = 0', array('badgeid' => $this->id));
385
386        return $awarded;
387    }
388
389    /**
390     * Gets list of users who have earned an instance of this badge.
391     *
392     * @return array An array of objects with information about badge awards.
393     */
394    public function get_awards() {
395        global $DB;
396
397        $awards = $DB->get_records_sql(
398                'SELECT b.userid, b.dateissued, b.uniquehash, u.firstname, u.lastname
399                    FROM {badge_issued} b INNER JOIN {user} u
400                        ON b.userid = u.id
401                    WHERE b.badgeid = :badgeid AND u.deleted = 0', array('badgeid' => $this->id));
402
403        return $awards;
404    }
405
406    /**
407     * Indicates whether badge has already been issued to a user.
408     *
409     * @param int $userid User to check
410     * @return boolean
411     */
412    public function is_issued($userid) {
413        global $DB;
414        return $DB->record_exists('badge_issued', array('badgeid' => $this->id, 'userid' => $userid));
415    }
416
417    /**
418     * Issue a badge to user.
419     *
420     * @param int $userid User who earned the badge
421     * @param boolean $nobake Not baking actual badges (for testing purposes)
422     */
423    public function issue($userid, $nobake = false) {
424        global $DB, $CFG;
425
426        $now = time();
427        $issued = new stdClass();
428        $issued->badgeid = $this->id;
429        $issued->userid = $userid;
430        $issued->uniquehash = sha1(rand() . $userid . $this->id . $now);
431        $issued->dateissued = $now;
432
433        if ($this->can_expire()) {
434            $issued->dateexpire = $this->calculate_expiry($now);
435        } else {
436            $issued->dateexpire = null;
437        }
438
439        // Take into account user badges privacy settings.
440        // If none set, badges default visibility is set to public.
441        $issued->visible = get_user_preferences('badgeprivacysetting', 1, $userid);
442
443        $result = $DB->insert_record('badge_issued', $issued, true);
444
445        if ($result) {
446            // Trigger badge awarded event.
447            $eventdata = array (
448                'context' => $this->get_context(),
449                'objectid' => $this->id,
450                'relateduserid' => $userid,
451                'other' => array('dateexpire' => $issued->dateexpire, 'badgeissuedid' => $result)
452            );
453            \core\event\badge_awarded::create($eventdata)->trigger();
454
455            // Lock the badge, so that its criteria could not be changed any more.
456            if ($this->status == BADGE_STATUS_ACTIVE) {
457                $this->set_status(BADGE_STATUS_ACTIVE_LOCKED);
458            }
459
460            // Update details in criteria_met table.
461            $compl = $this->get_criteria_completions($userid);
462            foreach ($compl as $c) {
463                $obj = new stdClass();
464                $obj->id = $c->id;
465                $obj->issuedid = $result;
466                $DB->update_record('badge_criteria_met', $obj, true);
467            }
468
469            if (!$nobake) {
470                // Bake a badge image.
471                $pathhash = badges_bake($issued->uniquehash, $this->id, $userid, true);
472
473                // Notify recipients and badge creators.
474                badges_notify_badge_award($this, $userid, $issued->uniquehash, $pathhash);
475            }
476        }
477    }
478
479    /**
480     * Reviews all badge criteria and checks if badge can be instantly awarded.
481     *
482     * @return int Number of awards
483     */
484    public function review_all_criteria() {
485        global $DB, $CFG;
486        $awards = 0;
487
488        // Raise timelimit as this could take a while for big web sites.
489        core_php_time_limit::raise();
490        raise_memory_limit(MEMORY_HUGE);
491
492        foreach ($this->criteria as $crit) {
493            // Overall criterion is decided when other criteria are reviewed.
494            if ($crit->criteriatype == BADGE_CRITERIA_TYPE_OVERALL) {
495                continue;
496            }
497
498            list($extrajoin, $extrawhere, $extraparams) = $crit->get_completed_criteria_sql();
499            // For site level badges, get all active site users who can earn this badge and haven't got it yet.
500            if ($this->type == BADGE_TYPE_SITE) {
501                $sql = "SELECT DISTINCT u.id, bi.badgeid
502                        FROM {user} u
503                        {$extrajoin}
504                        LEFT JOIN {badge_issued} bi
505                            ON u.id = bi.userid AND bi.badgeid = :badgeid
506                        WHERE bi.badgeid IS NULL AND u.id != :guestid AND u.deleted = 0 " . $extrawhere;
507                $params = array_merge(array('badgeid' => $this->id, 'guestid' => $CFG->siteguest), $extraparams);
508                $toearn = $DB->get_fieldset_sql($sql, $params);
509            } else {
510                // For course level badges, get all users who already earned the badge in this course.
511                // Then find the ones who are enrolled in the course and don't have a badge yet.
512                $earned = $DB->get_fieldset_select(
513                    'badge_issued',
514                    'userid AS id',
515                    'badgeid = :badgeid',
516                    array('badgeid' => $this->id)
517                );
518
519                $wheresql = '';
520                $earnedparams = array();
521                if (!empty($earned)) {
522                    list($earnedsql, $earnedparams) = $DB->get_in_or_equal($earned, SQL_PARAMS_NAMED, 'u', false);
523                    $wheresql = ' WHERE u.id ' . $earnedsql;
524                }
525                list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->get_context(), 'moodle/badges:earnbadge', 0, true);
526                $sql = "SELECT DISTINCT u.id
527                        FROM {user} u
528                        {$extrajoin}
529                        JOIN ({$enrolledsql}) je ON je.id = u.id " . $wheresql . $extrawhere;
530                $params = array_merge($enrolledparams, $earnedparams, $extraparams);
531                $toearn = $DB->get_fieldset_sql($sql, $params);
532            }
533
534            foreach ($toearn as $uid) {
535                $reviewoverall = false;
536                if ($crit->review($uid, true)) {
537                    $crit->mark_complete($uid);
538                    if ($this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->method == BADGE_CRITERIA_AGGREGATION_ANY) {
539                        $this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->mark_complete($uid);
540                        $this->issue($uid);
541                        $awards++;
542                    } else {
543                        $reviewoverall = true;
544                    }
545                } else {
546                    // Will be reviewed some other time.
547                    $reviewoverall = false;
548                }
549                // Review overall if it is required.
550                if ($reviewoverall && $this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->review($uid)) {
551                    $this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->mark_complete($uid);
552                    $this->issue($uid);
553                    $awards++;
554                }
555            }
556        }
557
558        return $awards;
559    }
560
561    /**
562     * Gets an array of completed criteria from 'badge_criteria_met' table.
563     *
564     * @param int $userid Completions for a user
565     * @return array Records of criteria completions
566     */
567    public function get_criteria_completions($userid) {
568        global $DB;
569        $completions = array();
570        $sql = "SELECT bcm.id, bcm.critid
571                FROM {badge_criteria_met} bcm
572                    INNER JOIN {badge_criteria} bc ON bcm.critid = bc.id
573                WHERE bc.badgeid = :badgeid AND bcm.userid = :userid ";
574        $completions = $DB->get_records_sql($sql, array('badgeid' => $this->id, 'userid' => $userid));
575
576        return $completions;
577    }
578
579    /**
580     * Checks if badges has award criteria set up.
581     *
582     * @return boolean A status indicating badge has at least one criterion
583     */
584    public function has_criteria() {
585        if (count($this->criteria) > 0) {
586            return true;
587        }
588        return false;
589    }
590
591    /**
592     * Returns badge award criteria
593     *
594     * @return array An array of badge criteria
595     */
596    public function get_criteria() {
597        global $DB;
598        $criteria = array();
599
600        if ($records = (array)$DB->get_records('badge_criteria', array('badgeid' => $this->id))) {
601            foreach ($records as $record) {
602                $criteria[$record->criteriatype] = award_criteria::build((array)$record);
603            }
604        }
605
606        return $criteria;
607    }
608
609    /**
610     * Get aggregation method for badge criteria
611     *
612     * @param int $criteriatype If none supplied, get overall aggregation method (optional)
613     * @return int One of BADGE_CRITERIA_AGGREGATION_ALL or BADGE_CRITERIA_AGGREGATION_ANY
614     */
615    public function get_aggregation_method($criteriatype = 0) {
616        global $DB;
617        $params = array('badgeid' => $this->id, 'criteriatype' => $criteriatype);
618        $aggregation = $DB->get_field('badge_criteria', 'method', $params, IGNORE_MULTIPLE);
619
620        if (!$aggregation) {
621            return BADGE_CRITERIA_AGGREGATION_ALL;
622        }
623
624        return $aggregation;
625    }
626
627    /**
628     * Checks if badge has expiry period or date set up.
629     *
630     * @return boolean A status indicating badge can expire
631     */
632    public function can_expire() {
633        if ($this->expireperiod || $this->expiredate) {
634            return true;
635        }
636        return false;
637    }
638
639    /**
640     * Calculates badge expiry date based on either expirydate or expiryperiod.
641     *
642     * @param int $timestamp Time of badge issue
643     * @return int A timestamp
644     */
645    public function calculate_expiry($timestamp) {
646        $expiry = null;
647
648        if (isset($this->expiredate)) {
649            $expiry = $this->expiredate;
650        } else if (isset($this->expireperiod)) {
651            $expiry = $timestamp + $this->expireperiod;
652        }
653
654        return $expiry;
655    }
656
657    /**
658     * Checks if badge has manual award criteria set.
659     *
660     * @return boolean A status indicating badge can be awarded manually
661     */
662    public function has_manual_award_criteria() {
663        foreach ($this->criteria as $criterion) {
664            if ($criterion->criteriatype == BADGE_CRITERIA_TYPE_MANUAL) {
665                return true;
666            }
667        }
668        return false;
669    }
670
671    /**
672     * Fully deletes the badge or marks it as archived.
673     *
674     * @param boolean $archive Achive a badge without actual deleting of any data.
675     */
676    public function delete($archive = true) {
677        global $DB;
678
679        if ($archive) {
680            $this->status = BADGE_STATUS_ARCHIVED;
681            $this->save();
682
683            // Trigger event, badge archived.
684            $eventparams = array('objectid' => $this->id, 'context' => $this->get_context());
685            $event = \core\event\badge_archived::create($eventparams);
686            $event->trigger();
687            return;
688        }
689
690        $fs = get_file_storage();
691
692        // Remove all issued badge image files and badge awards.
693        // Cannot bulk remove area files here because they are issued in user context.
694        $awards = $this->get_awards();
695        foreach ($awards as $award) {
696            $usercontext = context_user::instance($award->userid);
697            $fs->delete_area_files($usercontext->id, 'badges', 'userbadge', $this->id);
698        }
699        $DB->delete_records('badge_issued', array('badgeid' => $this->id));
700
701        // Remove all badge criteria.
702        $criteria = $this->get_criteria();
703        foreach ($criteria as $criterion) {
704            $criterion->delete();
705        }
706
707        // Delete badge images.
708        $badgecontext = $this->get_context();
709        $fs->delete_area_files($badgecontext->id, 'badges', 'badgeimage', $this->id);
710
711        // Delete endorsements, competencies and related badges.
712        $DB->delete_records('badge_endorsement', array('badgeid' => $this->id));
713        $relatedsql = 'badgeid = :badgeid OR relatedbadgeid = :relatedbadgeid';
714        $relatedparams = array(
715            'badgeid' => $this->id,
716            'relatedbadgeid' => $this->id
717        );
718        $DB->delete_records_select('badge_related', $relatedsql, $relatedparams);
719        $DB->delete_records('badge_alignment', array('badgeid' => $this->id));
720
721        // Finally, remove badge itself.
722        $DB->delete_records('badge', array('id' => $this->id));
723
724        // Trigger event, badge deleted.
725        $eventparams = array('objectid' => $this->id,
726            'context' => $this->get_context(),
727            'other' => array('badgetype' => $this->type, 'courseid' => $this->courseid)
728            );
729        $event = \core\event\badge_deleted::create($eventparams);
730        $event->trigger();
731    }
732
733    /**
734     * Add multiple related badges.
735     *
736     * @param array $relatedids Id of badges.
737     */
738    public function add_related_badges($relatedids) {
739        global $DB;
740        $relatedbadges = array();
741        foreach ($relatedids as $relatedid) {
742            $relatedbadge = new stdClass();
743            $relatedbadge->badgeid = $this->id;
744            $relatedbadge->relatedbadgeid = $relatedid;
745            $relatedbadges[] = $relatedbadge;
746        }
747        $DB->insert_records('badge_related', $relatedbadges);
748    }
749
750    /**
751     * Delete an related badge.
752     *
753     * @param int $relatedid Id related badge.
754     * @return boolean A status for delete an related badge.
755     */
756    public function delete_related_badge($relatedid) {
757        global $DB;
758        $sql = "(badgeid = :badgeid AND relatedbadgeid = :relatedid) OR " .
759               "(badgeid = :relatedid2 AND relatedbadgeid = :badgeid2)";
760        $params = ['badgeid' => $this->id, 'badgeid2' => $this->id, 'relatedid' => $relatedid, 'relatedid2' => $relatedid];
761        return $DB->delete_records_select('badge_related', $sql, $params);
762    }
763
764    /**
765     * Checks if badge has related badges.
766     *
767     * @return boolean A status related badge.
768     */
769    public function has_related() {
770        global $DB;
771        $sql = "SELECT DISTINCT b.id
772                    FROM {badge_related} br
773                    JOIN {badge} b ON (br.relatedbadgeid = b.id OR br.badgeid = b.id)
774                   WHERE (br.badgeid = :badgeid OR br.relatedbadgeid = :badgeid2) AND b.id != :badgeid3";
775        return $DB->record_exists_sql($sql, ['badgeid' => $this->id, 'badgeid2' => $this->id, 'badgeid3' => $this->id]);
776    }
777
778    /**
779     * Get related badges of badge.
780     *
781     * @param boolean $activeonly Do not get the inactive badges when is true.
782     * @return array Related badges information.
783     */
784    public function get_related_badges($activeonly = false) {
785        global $DB;
786
787        $params = array('badgeid' => $this->id, 'badgeid2' => $this->id, 'badgeid3' => $this->id);
788        $query = "SELECT DISTINCT b.id, b.name, b.version, b.language, b.type
789                    FROM {badge_related} br
790                    JOIN {badge} b ON (br.relatedbadgeid = b.id OR br.badgeid = b.id)
791                   WHERE (br.badgeid = :badgeid OR br.relatedbadgeid = :badgeid2) AND b.id != :badgeid3";
792        if ($activeonly) {
793            $query .= " AND b.status <> :status";
794            $params['status'] = BADGE_STATUS_INACTIVE;
795        }
796        $relatedbadges = $DB->get_records_sql($query, $params);
797        return $relatedbadges;
798    }
799
800    /**
801     * Insert/update alignment information of badge.
802     *
803     * @param stdClass $alignment Data of a alignment.
804     * @param int $alignmentid ID alignment.
805     * @return bool|int A status/ID when insert or update data.
806     */
807    public function save_alignment($alignment, $alignmentid = 0) {
808        global $DB;
809
810        $record = $DB->record_exists('badge_alignment', array('id' => $alignmentid));
811        if ($record) {
812            $alignment->id = $alignmentid;
813            return $DB->update_record('badge_alignment', $alignment);
814        } else {
815            return $DB->insert_record('badge_alignment', $alignment, true);
816        }
817    }
818
819    /**
820     * Delete a alignment of badge.
821     *
822     * @param int $alignmentid ID alignment.
823     * @return boolean A status for delete a alignment.
824     */
825    public function delete_alignment($alignmentid) {
826        global $DB;
827        return $DB->delete_records('badge_alignment', array('badgeid' => $this->id, 'id' => $alignmentid));
828    }
829
830    /**
831     * Get alignments of badge.
832     *
833     * @return array List content alignments.
834     */
835    public function get_alignments() {
836        global $DB;
837        return $DB->get_records('badge_alignment', array('badgeid' => $this->id));
838    }
839
840    /**
841     * Insert/update Endorsement information of badge.
842     *
843     * @param stdClass $endorsement Data of an endorsement.
844     * @return bool|int A status/ID when insert or update data.
845     */
846    public function save_endorsement($endorsement) {
847        global $DB;
848        $record = $DB->get_record('badge_endorsement', array('badgeid' => $this->id));
849        if ($record) {
850            $endorsement->id = $record->id;
851            return $DB->update_record('badge_endorsement', $endorsement);
852        } else {
853            return $DB->insert_record('badge_endorsement', $endorsement, true);
854        }
855    }
856
857    /**
858     * Get endorsement of badge.
859     *
860     * @return array|stdClass Endorsement information.
861     */
862    public function get_endorsement() {
863        global $DB;
864        return $DB->get_record('badge_endorsement', array('badgeid' => $this->id));
865    }
866
867    /**
868     * Markdown language support for criteria.
869     *
870     * @return string $output Markdown content to output.
871     */
872    public function markdown_badge_criteria() {
873        $agg = $this->get_aggregation_methods();
874        if (empty($this->criteria)) {
875            return get_string('nocriteria', 'badges');
876        }
877        $overalldescr = '';
878        $overall = $this->criteria[BADGE_CRITERIA_TYPE_OVERALL];
879        if (!empty($overall->description)) {
880                $overalldescr = format_text($overall->description, $overall->descriptionformat,
881                    array('context' => $this->get_context())) . '\n';
882        }
883        // Get the condition string.
884        if (count($this->criteria) == 2) {
885            $condition = get_string('criteria_descr', 'badges');
886        } else {
887            $condition = get_string('criteria_descr_' . BADGE_CRITERIA_TYPE_OVERALL, 'badges',
888                core_text::strtoupper($agg[$this->get_aggregation_method()]));
889        }
890        unset($this->criteria[BADGE_CRITERIA_TYPE_OVERALL]);
891        $items = array();
892        // If only one criterion left, make sure its description goe to the top.
893        if (count($this->criteria) == 1) {
894            $c = reset($this->criteria);
895            if (!empty($c->description)) {
896                $overalldescr = $c->description . '\n';
897            }
898            if (count($c->params) == 1) {
899                $items[] = ' * ' . get_string('criteria_descr_single_' . $c->criteriatype, 'badges') .
900                    $c->get_details();
901            } else {
902                $items[] = '* ' . get_string('criteria_descr_' . $c->criteriatype, 'badges',
903                        core_text::strtoupper($agg[$this->get_aggregation_method($c->criteriatype)])) .
904                    $c->get_details();
905            }
906        } else {
907            foreach ($this->criteria as $type => $c) {
908                $criteriadescr = '';
909                if (!empty($c->description)) {
910                    $criteriadescr = $c->description;
911                }
912                if (count($c->params) == 1) {
913                    $items[] = ' * ' . get_string('criteria_descr_single_' . $type, 'badges') .
914                        $c->get_details() . $criteriadescr;
915                } else {
916                    $items[] = '* ' . get_string('criteria_descr_' . $type, 'badges',
917                            core_text::strtoupper($agg[$this->get_aggregation_method($type)])) .
918                        $c->get_details() . $criteriadescr;
919                }
920            }
921        }
922        return strip_tags($overalldescr . $condition . html_writer::alist($items, array(), 'ul'));
923    }
924
925    /**
926     * Define issuer information by format Open Badges specification version 2.
927     *
928     * @param int $obversion OB version to use.
929     * @return array Issuer informations of the badge.
930     */
931    public function get_badge_issuer(?int $obversion = null) {
932        global $DB;
933
934        $issuer = [];
935        if ($obversion == OPEN_BADGES_V1) {
936            $data = $DB->get_record('badge', ['id' => $this->id]);
937            $issuer['name'] = $data->issuername;
938            $issuer['url'] = $data->issuerurl;
939            $issuer['email'] = $data->issuercontact;
940        } else {
941            $issuer['name'] = $this->issuername;
942            $issuer['url'] = $this->issuerurl;
943            $issuer['email'] = $this->issuercontact;
944            $issuer['@context'] = OPEN_BADGES_V2_CONTEXT;
945            $issueridurl = new moodle_url('/badges/issuer_json.php', array('id' => $this->id));
946            $issuer['id'] = $issueridurl->out(false);
947            $issuer['type'] = OPEN_BADGES_V2_TYPE_ISSUER;
948        }
949
950        return $issuer;
951    }
952}
953