. /** * Badge assertion library. * * @package core * @subpackage badges * @copyright 2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Yuliya Bozhko */ namespace core_badges; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot.'/lib/badgeslib.php'); use context_system; use context_course; use context_user; use moodle_exception; use moodle_url; use core_text; use award_criteria; use core_php_time_limit; use html_writer; use stdClass; /** * Class that represents badge. * * @copyright 2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class badge { /** @var int Badge id */ public $id; /** @var string Badge name */ public $name; /** @var string Badge description */ public $description; /** @var integer Timestamp this badge was created */ public $timecreated; /** @var integer Timestamp this badge was modified */ public $timemodified; /** @var int The user who created this badge */ public $usercreated; /** @var int The user who modified this badge */ public $usermodified; /** @var string The name of the issuer of this badge */ public $issuername; /** @var string The url of the issuer of this badge */ public $issuerurl; /** @var string The email of the issuer of this badge */ public $issuercontact; /** @var integer Timestamp this badge will expire */ public $expiredate; /** @var integer Duration this badge is valid for */ public $expireperiod; /** @var integer Site or course badge */ public $type; /** @var integer The course this badge belongs to */ public $courseid; /** @var string The message this badge includes. */ public $message; /** @var string The subject of the message for this badge */ public $messagesubject; /** @var int Is this badge image baked. */ public $attachment; /** @var int Send a message when this badge is awarded. */ public $notification; /** @var int Lifecycle status for this badge. */ public $status = 0; /** @var int Timestamp to next run cron for this badge. */ public $nextcron; /** @var int What backpack api version to use for this badge. */ public $version; /** @var string What language is this badge written in. */ public $language; /** @var string The author of the image for this badge. */ public $imageauthorname; /** @var string The email of the author of the image for this badge. */ public $imageauthoremail; /** @var string The url of the author of the image for this badge. */ public $imageauthorurl; /** @var string The caption of the image for this badge. */ public $imagecaption; /** @var array Badge criteria */ public $criteria = array(); /** * Constructs with badge details. * * @param int $badgeid badge ID. */ public function __construct($badgeid) { global $DB; $this->id = $badgeid; $data = $DB->get_record('badge', array('id' => $badgeid)); if (empty($data)) { print_error('error:nosuchbadge', 'badges', $badgeid); } foreach ((array)$data as $field => $value) { if (property_exists($this, $field)) { $this->{$field} = $value; } } if (badges_open_badges_backpack_api() != OPEN_BADGES_V1) { // For Open Badges 2 we need to use a single site issuer with no exceptions. $issuer = badges_get_default_issuer(); $this->issuername = $issuer['name']; $this->issuercontact = $issuer['email']; $this->issuerurl = $issuer['url']; } $this->criteria = self::get_criteria(); } /** * Use to get context instance of a badge. * * @return context instance. */ public function get_context() { if ($this->type == BADGE_TYPE_SITE) { return context_system::instance(); } else if ($this->type == BADGE_TYPE_COURSE) { return context_course::instance($this->courseid); } else { debugging('Something is wrong...'); } } /** * Return array of aggregation methods * * @return array */ public static function get_aggregation_methods() { return array( BADGE_CRITERIA_AGGREGATION_ALL => get_string('all', 'badges'), BADGE_CRITERIA_AGGREGATION_ANY => get_string('any', 'badges'), ); } /** * Return array of accepted criteria types for this badge * * @return array */ public function get_accepted_criteria() { global $CFG; $criteriatypes = array(); if ($this->type == BADGE_TYPE_COURSE) { $criteriatypes = array( BADGE_CRITERIA_TYPE_OVERALL, BADGE_CRITERIA_TYPE_MANUAL, BADGE_CRITERIA_TYPE_COURSE, BADGE_CRITERIA_TYPE_BADGE, BADGE_CRITERIA_TYPE_ACTIVITY, BADGE_CRITERIA_TYPE_COMPETENCY ); } else if ($this->type == BADGE_TYPE_SITE) { $criteriatypes = array( BADGE_CRITERIA_TYPE_OVERALL, BADGE_CRITERIA_TYPE_MANUAL, BADGE_CRITERIA_TYPE_COURSESET, BADGE_CRITERIA_TYPE_BADGE, BADGE_CRITERIA_TYPE_PROFILE, BADGE_CRITERIA_TYPE_COHORT, BADGE_CRITERIA_TYPE_COMPETENCY ); } $alltypes = badges_list_criteria(); foreach ($criteriatypes as $index => $type) { if (!isset($alltypes[$type])) { unset($criteriatypes[$index]); } } return $criteriatypes; } /** * Save/update badge information in 'badge' table only. * Cannot be used for updating awards and criteria settings. * * @return boolean Returns true on success. */ public function save() { global $DB; $fordb = new stdClass(); foreach (get_object_vars($this) as $k => $v) { $fordb->{$k} = $v; } unset($fordb->criteria); $fordb->timemodified = time(); if ($DB->update_record_raw('badge', $fordb)) { // Trigger event, badge updated. $eventparams = array('objectid' => $this->id, 'context' => $this->get_context()); $event = \core\event\badge_updated::create($eventparams); $event->trigger(); return true; } else { throw new moodle_exception('error:save', 'badges'); return false; } } /** * Creates and saves a clone of badge with all its properties. * Clone is not active by default and has 'Copy of' attached to its name. * * @return int ID of new badge. */ public function make_clone() { global $DB, $USER, $PAGE; $fordb = new stdClass(); foreach (get_object_vars($this) as $k => $v) { $fordb->{$k} = $v; } $fordb->name = get_string('copyof', 'badges', $this->name); $fordb->status = BADGE_STATUS_INACTIVE; $fordb->usercreated = $USER->id; $fordb->usermodified = $USER->id; $fordb->timecreated = time(); $fordb->timemodified = time(); unset($fordb->id); if ($fordb->notification > 1) { $fordb->nextcron = badges_calculate_message_schedule($fordb->notification); } $criteria = $fordb->criteria; unset($fordb->criteria); if ($new = $DB->insert_record('badge', $fordb, true)) { $newbadge = new badge($new); // Copy badge image. $fs = get_file_storage(); if ($file = $fs->get_file($this->get_context()->id, 'badges', 'badgeimage', $this->id, '/', 'f1.png')) { if ($imagefile = $file->copy_content_to_temp()) { badges_process_badge_image($newbadge, $imagefile); } } // Copy badge criteria. foreach ($this->criteria as $crit) { $crit->make_clone($new); } // Trigger event, badge duplicated. $eventparams = array('objectid' => $new, 'context' => $PAGE->context); $event = \core\event\badge_duplicated::create($eventparams); $event->trigger(); return $new; } else { throw new moodle_exception('error:clone', 'badges'); return false; } } /** * Checks if badges is active. * Used in badge award. * * @return boolean A status indicating badge is active */ public function is_active() { if (($this->status == BADGE_STATUS_ACTIVE) || ($this->status == BADGE_STATUS_ACTIVE_LOCKED)) { return true; } return false; } /** * Use to get the name of badge status. * * @return string */ public function get_status_name() { return get_string('badgestatus_' . $this->status, 'badges'); } /** * Use to set badge status. * Only active badges can be earned/awarded/issued. * * @param int $status Status from BADGE_STATUS constants */ public function set_status($status = 0) { $this->status = $status; $this->save(); if ($status == BADGE_STATUS_ACTIVE) { // Trigger event, badge enabled. $eventparams = array('objectid' => $this->id, 'context' => $this->get_context()); $event = \core\event\badge_enabled::create($eventparams); $event->trigger(); } else if ($status == BADGE_STATUS_INACTIVE) { // Trigger event, badge disabled. $eventparams = array('objectid' => $this->id, 'context' => $this->get_context()); $event = \core\event\badge_disabled::create($eventparams); $event->trigger(); } } /** * Checks if badges is locked. * Used in badge award and editing. * * @return boolean A status indicating badge is locked */ public function is_locked() { if (($this->status == BADGE_STATUS_ACTIVE_LOCKED) || ($this->status == BADGE_STATUS_INACTIVE_LOCKED)) { return true; } return false; } /** * Checks if badge has been awarded to users. * Used in badge editing. * * @return boolean A status indicating badge has been awarded at least once */ public function has_awards() { global $DB; $awarded = $DB->record_exists_sql('SELECT b.uniquehash FROM {badge_issued} b INNER JOIN {user} u ON b.userid = u.id WHERE b.badgeid = :badgeid AND u.deleted = 0', array('badgeid' => $this->id)); return $awarded; } /** * Gets list of users who have earned an instance of this badge. * * @return array An array of objects with information about badge awards. */ public function get_awards() { global $DB; $awards = $DB->get_records_sql( 'SELECT b.userid, b.dateissued, b.uniquehash, u.firstname, u.lastname FROM {badge_issued} b INNER JOIN {user} u ON b.userid = u.id WHERE b.badgeid = :badgeid AND u.deleted = 0', array('badgeid' => $this->id)); return $awards; } /** * Indicates whether badge has already been issued to a user. * * @param int $userid User to check * @return boolean */ public function is_issued($userid) { global $DB; return $DB->record_exists('badge_issued', array('badgeid' => $this->id, 'userid' => $userid)); } /** * Issue a badge to user. * * @param int $userid User who earned the badge * @param boolean $nobake Not baking actual badges (for testing purposes) */ public function issue($userid, $nobake = false) { global $DB, $CFG; $now = time(); $issued = new stdClass(); $issued->badgeid = $this->id; $issued->userid = $userid; $issued->uniquehash = sha1(rand() . $userid . $this->id . $now); $issued->dateissued = $now; if ($this->can_expire()) { $issued->dateexpire = $this->calculate_expiry($now); } else { $issued->dateexpire = null; } // Take into account user badges privacy settings. // If none set, badges default visibility is set to public. $issued->visible = get_user_preferences('badgeprivacysetting', 1, $userid); $result = $DB->insert_record('badge_issued', $issued, true); if ($result) { // Trigger badge awarded event. $eventdata = array ( 'context' => $this->get_context(), 'objectid' => $this->id, 'relateduserid' => $userid, 'other' => array('dateexpire' => $issued->dateexpire, 'badgeissuedid' => $result) ); \core\event\badge_awarded::create($eventdata)->trigger(); // Lock the badge, so that its criteria could not be changed any more. if ($this->status == BADGE_STATUS_ACTIVE) { $this->set_status(BADGE_STATUS_ACTIVE_LOCKED); } // Update details in criteria_met table. $compl = $this->get_criteria_completions($userid); foreach ($compl as $c) { $obj = new stdClass(); $obj->id = $c->id; $obj->issuedid = $result; $DB->update_record('badge_criteria_met', $obj, true); } if (!$nobake) { // Bake a badge image. $pathhash = badges_bake($issued->uniquehash, $this->id, $userid, true); // Notify recipients and badge creators. badges_notify_badge_award($this, $userid, $issued->uniquehash, $pathhash); } } } /** * Reviews all badge criteria and checks if badge can be instantly awarded. * * @return int Number of awards */ public function review_all_criteria() { global $DB, $CFG; $awards = 0; // Raise timelimit as this could take a while for big web sites. core_php_time_limit::raise(); raise_memory_limit(MEMORY_HUGE); foreach ($this->criteria as $crit) { // Overall criterion is decided when other criteria are reviewed. if ($crit->criteriatype == BADGE_CRITERIA_TYPE_OVERALL) { continue; } list($extrajoin, $extrawhere, $extraparams) = $crit->get_completed_criteria_sql(); // For site level badges, get all active site users who can earn this badge and haven't got it yet. if ($this->type == BADGE_TYPE_SITE) { $sql = "SELECT DISTINCT u.id, bi.badgeid FROM {user} u {$extrajoin} LEFT JOIN {badge_issued} bi ON u.id = bi.userid AND bi.badgeid = :badgeid WHERE bi.badgeid IS NULL AND u.id != :guestid AND u.deleted = 0 " . $extrawhere; $params = array_merge(array('badgeid' => $this->id, 'guestid' => $CFG->siteguest), $extraparams); $toearn = $DB->get_fieldset_sql($sql, $params); } else { // For course level badges, get all users who already earned the badge in this course. // Then find the ones who are enrolled in the course and don't have a badge yet. $earned = $DB->get_fieldset_select( 'badge_issued', 'userid AS id', 'badgeid = :badgeid', array('badgeid' => $this->id) ); $wheresql = ''; $earnedparams = array(); if (!empty($earned)) { list($earnedsql, $earnedparams) = $DB->get_in_or_equal($earned, SQL_PARAMS_NAMED, 'u', false); $wheresql = ' WHERE u.id ' . $earnedsql; } list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->get_context(), 'moodle/badges:earnbadge', 0, true); $sql = "SELECT DISTINCT u.id FROM {user} u {$extrajoin} JOIN ({$enrolledsql}) je ON je.id = u.id " . $wheresql . $extrawhere; $params = array_merge($enrolledparams, $earnedparams, $extraparams); $toearn = $DB->get_fieldset_sql($sql, $params); } foreach ($toearn as $uid) { $reviewoverall = false; if ($crit->review($uid, true)) { $crit->mark_complete($uid); if ($this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->method == BADGE_CRITERIA_AGGREGATION_ANY) { $this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->mark_complete($uid); $this->issue($uid); $awards++; } else { $reviewoverall = true; } } else { // Will be reviewed some other time. $reviewoverall = false; } // Review overall if it is required. if ($reviewoverall && $this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->review($uid)) { $this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->mark_complete($uid); $this->issue($uid); $awards++; } } } return $awards; } /** * Gets an array of completed criteria from 'badge_criteria_met' table. * * @param int $userid Completions for a user * @return array Records of criteria completions */ public function get_criteria_completions($userid) { global $DB; $completions = array(); $sql = "SELECT bcm.id, bcm.critid FROM {badge_criteria_met} bcm INNER JOIN {badge_criteria} bc ON bcm.critid = bc.id WHERE bc.badgeid = :badgeid AND bcm.userid = :userid "; $completions = $DB->get_records_sql($sql, array('badgeid' => $this->id, 'userid' => $userid)); return $completions; } /** * Checks if badges has award criteria set up. * * @return boolean A status indicating badge has at least one criterion */ public function has_criteria() { if (count($this->criteria) > 0) { return true; } return false; } /** * Returns badge award criteria * * @return array An array of badge criteria */ public function get_criteria() { global $DB; $criteria = array(); if ($records = (array)$DB->get_records('badge_criteria', array('badgeid' => $this->id))) { foreach ($records as $record) { $criteria[$record->criteriatype] = award_criteria::build((array)$record); } } return $criteria; } /** * Get aggregation method for badge criteria * * @param int $criteriatype If none supplied, get overall aggregation method (optional) * @return int One of BADGE_CRITERIA_AGGREGATION_ALL or BADGE_CRITERIA_AGGREGATION_ANY */ public function get_aggregation_method($criteriatype = 0) { global $DB; $params = array('badgeid' => $this->id, 'criteriatype' => $criteriatype); $aggregation = $DB->get_field('badge_criteria', 'method', $params, IGNORE_MULTIPLE); if (!$aggregation) { return BADGE_CRITERIA_AGGREGATION_ALL; } return $aggregation; } /** * Checks if badge has expiry period or date set up. * * @return boolean A status indicating badge can expire */ public function can_expire() { if ($this->expireperiod || $this->expiredate) { return true; } return false; } /** * Calculates badge expiry date based on either expirydate or expiryperiod. * * @param int $timestamp Time of badge issue * @return int A timestamp */ public function calculate_expiry($timestamp) { $expiry = null; if (isset($this->expiredate)) { $expiry = $this->expiredate; } else if (isset($this->expireperiod)) { $expiry = $timestamp + $this->expireperiod; } return $expiry; } /** * Checks if badge has manual award criteria set. * * @return boolean A status indicating badge can be awarded manually */ public function has_manual_award_criteria() { foreach ($this->criteria as $criterion) { if ($criterion->criteriatype == BADGE_CRITERIA_TYPE_MANUAL) { return true; } } return false; } /** * Fully deletes the badge or marks it as archived. * * @param boolean $archive Achive a badge without actual deleting of any data. */ public function delete($archive = true) { global $DB; if ($archive) { $this->status = BADGE_STATUS_ARCHIVED; $this->save(); // Trigger event, badge archived. $eventparams = array('objectid' => $this->id, 'context' => $this->get_context()); $event = \core\event\badge_archived::create($eventparams); $event->trigger(); return; } $fs = get_file_storage(); // Remove all issued badge image files and badge awards. // Cannot bulk remove area files here because they are issued in user context. $awards = $this->get_awards(); foreach ($awards as $award) { $usercontext = context_user::instance($award->userid); $fs->delete_area_files($usercontext->id, 'badges', 'userbadge', $this->id); } $DB->delete_records('badge_issued', array('badgeid' => $this->id)); // Remove all badge criteria. $criteria = $this->get_criteria(); foreach ($criteria as $criterion) { $criterion->delete(); } // Delete badge images. $badgecontext = $this->get_context(); $fs->delete_area_files($badgecontext->id, 'badges', 'badgeimage', $this->id); // Delete endorsements, competencies and related badges. $DB->delete_records('badge_endorsement', array('badgeid' => $this->id)); $relatedsql = 'badgeid = :badgeid OR relatedbadgeid = :relatedbadgeid'; $relatedparams = array( 'badgeid' => $this->id, 'relatedbadgeid' => $this->id ); $DB->delete_records_select('badge_related', $relatedsql, $relatedparams); $DB->delete_records('badge_alignment', array('badgeid' => $this->id)); // Finally, remove badge itself. $DB->delete_records('badge', array('id' => $this->id)); // Trigger event, badge deleted. $eventparams = array('objectid' => $this->id, 'context' => $this->get_context(), 'other' => array('badgetype' => $this->type, 'courseid' => $this->courseid) ); $event = \core\event\badge_deleted::create($eventparams); $event->trigger(); } /** * Add multiple related badges. * * @param array $relatedids Id of badges. */ public function add_related_badges($relatedids) { global $DB; $relatedbadges = array(); foreach ($relatedids as $relatedid) { $relatedbadge = new stdClass(); $relatedbadge->badgeid = $this->id; $relatedbadge->relatedbadgeid = $relatedid; $relatedbadges[] = $relatedbadge; } $DB->insert_records('badge_related', $relatedbadges); } /** * Delete an related badge. * * @param int $relatedid Id related badge. * @return boolean A status for delete an related badge. */ public function delete_related_badge($relatedid) { global $DB; $sql = "(badgeid = :badgeid AND relatedbadgeid = :relatedid) OR " . "(badgeid = :relatedid2 AND relatedbadgeid = :badgeid2)"; $params = ['badgeid' => $this->id, 'badgeid2' => $this->id, 'relatedid' => $relatedid, 'relatedid2' => $relatedid]; return $DB->delete_records_select('badge_related', $sql, $params); } /** * Checks if badge has related badges. * * @return boolean A status related badge. */ public function has_related() { global $DB; $sql = "SELECT DISTINCT b.id FROM {badge_related} br JOIN {badge} b ON (br.relatedbadgeid = b.id OR br.badgeid = b.id) WHERE (br.badgeid = :badgeid OR br.relatedbadgeid = :badgeid2) AND b.id != :badgeid3"; return $DB->record_exists_sql($sql, ['badgeid' => $this->id, 'badgeid2' => $this->id, 'badgeid3' => $this->id]); } /** * Get related badges of badge. * * @param boolean $activeonly Do not get the inactive badges when is true. * @return array Related badges information. */ public function get_related_badges($activeonly = false) { global $DB; $params = array('badgeid' => $this->id, 'badgeid2' => $this->id, 'badgeid3' => $this->id); $query = "SELECT DISTINCT b.id, b.name, b.version, b.language, b.type FROM {badge_related} br JOIN {badge} b ON (br.relatedbadgeid = b.id OR br.badgeid = b.id) WHERE (br.badgeid = :badgeid OR br.relatedbadgeid = :badgeid2) AND b.id != :badgeid3"; if ($activeonly) { $query .= " AND b.status <> :status"; $params['status'] = BADGE_STATUS_INACTIVE; } $relatedbadges = $DB->get_records_sql($query, $params); return $relatedbadges; } /** * Insert/update alignment information of badge. * * @param stdClass $alignment Data of a alignment. * @param int $alignmentid ID alignment. * @return bool|int A status/ID when insert or update data. */ public function save_alignment($alignment, $alignmentid = 0) { global $DB; $record = $DB->record_exists('badge_alignment', array('id' => $alignmentid)); if ($record) { $alignment->id = $alignmentid; return $DB->update_record('badge_alignment', $alignment); } else { return $DB->insert_record('badge_alignment', $alignment, true); } } /** * Delete a alignment of badge. * * @param int $alignmentid ID alignment. * @return boolean A status for delete a alignment. */ public function delete_alignment($alignmentid) { global $DB; return $DB->delete_records('badge_alignment', array('badgeid' => $this->id, 'id' => $alignmentid)); } /** * Get alignments of badge. * * @return array List content alignments. */ public function get_alignments() { global $DB; return $DB->get_records('badge_alignment', array('badgeid' => $this->id)); } /** * Insert/update Endorsement information of badge. * * @param stdClass $endorsement Data of an endorsement. * @return bool|int A status/ID when insert or update data. */ public function save_endorsement($endorsement) { global $DB; $record = $DB->get_record('badge_endorsement', array('badgeid' => $this->id)); if ($record) { $endorsement->id = $record->id; return $DB->update_record('badge_endorsement', $endorsement); } else { return $DB->insert_record('badge_endorsement', $endorsement, true); } } /** * Get endorsement of badge. * * @return array|stdClass Endorsement information. */ public function get_endorsement() { global $DB; return $DB->get_record('badge_endorsement', array('badgeid' => $this->id)); } /** * Markdown language support for criteria. * * @return string $output Markdown content to output. */ public function markdown_badge_criteria() { $agg = $this->get_aggregation_methods(); if (empty($this->criteria)) { return get_string('nocriteria', 'badges'); } $overalldescr = ''; $overall = $this->criteria[BADGE_CRITERIA_TYPE_OVERALL]; if (!empty($overall->description)) { $overalldescr = format_text($overall->description, $overall->descriptionformat, array('context' => $this->get_context())) . '\n'; } // Get the condition string. if (count($this->criteria) == 2) { $condition = get_string('criteria_descr', 'badges'); } else { $condition = get_string('criteria_descr_' . BADGE_CRITERIA_TYPE_OVERALL, 'badges', core_text::strtoupper($agg[$this->get_aggregation_method()])); } unset($this->criteria[BADGE_CRITERIA_TYPE_OVERALL]); $items = array(); // If only one criterion left, make sure its description goe to the top. if (count($this->criteria) == 1) { $c = reset($this->criteria); if (!empty($c->description)) { $overalldescr = $c->description . '\n'; } if (count($c->params) == 1) { $items[] = ' * ' . get_string('criteria_descr_single_' . $c->criteriatype, 'badges') . $c->get_details(); } else { $items[] = '* ' . get_string('criteria_descr_' . $c->criteriatype, 'badges', core_text::strtoupper($agg[$this->get_aggregation_method($c->criteriatype)])) . $c->get_details(); } } else { foreach ($this->criteria as $type => $c) { $criteriadescr = ''; if (!empty($c->description)) { $criteriadescr = $c->description; } if (count($c->params) == 1) { $items[] = ' * ' . get_string('criteria_descr_single_' . $type, 'badges') . $c->get_details() . $criteriadescr; } else { $items[] = '* ' . get_string('criteria_descr_' . $type, 'badges', core_text::strtoupper($agg[$this->get_aggregation_method($type)])) . $c->get_details() . $criteriadescr; } } } return strip_tags($overalldescr . $condition . html_writer::alist($items, array(), 'ul')); } /** * Define issuer information by format Open Badges specification version 2. * * @return array Issuer informations of the badge. */ public function get_badge_issuer() { $issuer = array(); $issuerurl = new moodle_url('/badges/badge_json.php', array('id' => $this->id, 'action' => 0)); $issuer['name'] = $this->issuername; $issuer['url'] = $this->issuerurl; $issuer['email'] = $this->issuercontact; $issuer['@context'] = OPEN_BADGES_V2_CONTEXT; $issuer['id'] = $this->issuerurl; $issuer['type'] = OPEN_BADGES_V2_TYPE_ISSUER; return $issuer; } }