diff --git a/admin/settings/badges.php b/admin/settings/badges.php index 276582c3ccc..24333ba7260 100644 --- a/admin/settings/badges.php +++ b/admin/settings/badges.php @@ -57,9 +57,11 @@ if (($hassiteconfig || has_any_capability(array( new lang_string('badgesalt_desc', 'badges'), 'badges' . $SITE->timecreated, PARAM_ALPHANUM)); - $globalsettings->add(new admin_setting_configcheckbox('badges_allowexternalbackpack', - new lang_string('allowexternalbackpack', 'badges'), - new lang_string('allowexternalbackpack_desc', 'badges'), 1)); + $backpacks = badges_get_site_backpacks(); + $choices = array(); + foreach ($backpacks as $backpack) { + $choices[$backpack->id] = $backpack->backpackweburl; + } $globalsettings->add(new admin_setting_configcheckbox('badges_allowcoursebadges', new lang_string('allowcoursebadges', 'badges'), @@ -91,4 +93,25 @@ if (($hassiteconfig || has_any_capability(array( array('moodle/badges:createbadge'), empty($CFG->enablebadges) ) ); + $backpacksettings = new admin_settingpage('backpacksettings', new lang_string('backpacksettings', 'badges'), + array('moodle/badges:manageglobalsettings'), empty($CFG->enablebadges)); + + $backpacksettings->add(new admin_setting_configcheckbox('badges_allowexternalbackpack', + new lang_string('allowexternalbackpack', 'badges'), + new lang_string('allowexternalbackpack_desc', 'badges'), 1)); + + $backpacksettings->add(new admin_setting_configselect('badges_site_backpack', + new lang_string('sitebackpack', 'badges'), + new lang_string('sitebackpack_help', 'badges'), + 1, $choices)); + + $ADMIN->add('badges', $backpacksettings); + + $ADMIN->add('badges', + new admin_externalpage('managebackpacks', + new lang_string('managebackpacks', 'badges'), + new moodle_url('/badges/backpacks.php'), + array('moodle/badges:manageglobalsettings'), empty($CFG->enablebadges) || empty($CFG->badges_allowexternalbackpack) + ) + ); } diff --git a/badges/action.php b/badges/action.php index 05dafddb744..a3bdb568bed 100644 --- a/badges/action.php +++ b/badges/action.php @@ -67,7 +67,7 @@ if ($copy) { $cloneid = $badge->make_clone(); // If a user can edit badge details, they will be redirected to the edit page. if (has_capability('moodle/badges:configuredetails', $context)) { - redirect(new moodle_url('/badges/edit.php', array('id' => $cloneid, 'action' => 'details'))); + redirect(new moodle_url('/badges/edit.php', array('id' => $cloneid, 'action' => 'badge'))); } redirect(new moodle_url('/badges/overview.php', array('id' => $cloneid))); } diff --git a/badges/alignment.php b/badges/alignment.php index f67a3045b92..cf5b4336582 100644 --- a/badges/alignment.php +++ b/badges/alignment.php @@ -99,7 +99,7 @@ if ($alignmentid || $action == 'add' || $action == 'edit') { } $alignments = $badge->get_alignments(); if (count($alignments) > 0) { - $renderrelated = new badge_alignments($alignments, $badgeid); + $renderrelated = new \core_badges\output\badge_alignments($alignments, $badgeid); echo $output->render($renderrelated); } else { echo $output->notification(get_string('noalignment', 'badges')); diff --git a/badges/backpack-add.php b/badges/backpack-add.php new file mode 100644 index 00000000000..79ebd1dc2ab --- /dev/null +++ b/badges/backpack-add.php @@ -0,0 +1,97 @@ +. + +/** + * Display details of an issued badge with criteria and evidence + * + * @package core_badges + * @copyright 2019 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../config.php'); +require_once($CFG->libdir . '/badgeslib.php'); + +if (badges_open_badges_backpack_api() != OPEN_BADGES_V2) { + throw new coding_exception('No backpacks support Open Badges V2.'); +} + +require_login(); + +$id = required_param('hash', PARAM_ALPHANUM); + +$PAGE->set_url('/badges/backpack-add.php', array('hash' => $id)); +$PAGE->set_context(context_system::instance()); +$output = $PAGE->get_renderer('core', 'badges'); + +$issuedbadge = new \core_badges\output\issued_badge($id); +if (!empty($issuedbadge->recipient->id)) { + // The flow for issuing a badge is: + // * Create issuer + // * Create badge + // * Create assertion (Award the badge!) + + // Get the backpack. + $badgeid = $issuedbadge->badgeid; + $badge = new badge($badgeid); + $backpack = $DB->get_record('badge_backpack', array('userid' => $USER->id)); + $sitebackpack = badges_get_site_backpack(0, $backpack->backpackurl); + $assertion = new core_badges_assertion($id, $backpack->apiversion); + $api = new \core_badges\backpack_api($sitebackpack); + $api->authenticate(); + + // Create issuer. + $issuer = $assertion->get_issuer(); + if (!($issuerentityid = badges_external_get_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_ISSUER, $issuer['email']))) { + $response = $api->put_issuer($issuer); + if (!$response) { + throw new moodle_exception('invalidrequest', 'error'); + } + $issuerentityid = $response->id; + badges_external_create_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_ISSUER, $issuer['email'], $issuerentityid); + } + // Create badge. + $badge = $assertion->get_badge_class(false); + $badgeid = $assertion->get_badge_id(); + if (!($badgeentityid = badges_external_get_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_BADGE, $badgeid))) { + $response = $api->put_badgeclass($issuerentityid, $badge); + if (!$response) { + throw new moodle_exception('invalidrequest', 'error'); + } + $badgeentityid = $response->id; + badges_external_create_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_BADGE, $badgeid, $badgeentityid); + } + + // Create assertion (Award the badge!). + $assertiondata = $assertion->get_badge_assertion(false, false); + + $assertionid = $assertion->get_assertion_hash(); + + if (!($assertionentityid = badges_external_get_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_ASSERTION, $assertionid))) { + $response = $api->put_badgeclass_assertion($badgeentityid, $assertiondata); + if (!$response) { + throw new moodle_exception('invalidrequest', 'error'); + } + $assertionentityid = $response->id; + badges_external_create_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_ASSERTION, $assertionid, $assertionentityid); + $response = ['success' => 'addedtobackpack']; + } else { + $response = ['warning' => 'existsinbackpack']; + } + redirect(new moodle_url('/badges/mybadges.php', $response)); +} else { + redirect(new moodle_url('/badges/mybadges.php')); +} diff --git a/badges/backpackemailverify.php b/badges/backpackemailverify.php index d9dfda25309..7e4de466c9b 100644 --- a/badges/backpackemailverify.php +++ b/badges/backpackemailverify.php @@ -24,11 +24,10 @@ */ require_once(__DIR__ . '/../config.php'); require_once($CFG->libdir . '/badgeslib.php'); -require_once(__DIR__ . '/lib/backpacklib.php'); $data = optional_param('data', '', PARAM_RAW); require_login(); -$PAGE->set_url('/badges/openbackpackemailverify.php'); +$PAGE->set_url('/badges/backpackemailverify.php'); $PAGE->set_context(context_user::instance($USER->id)); $redirect = '/badges/mybackpack.php'; @@ -37,17 +36,21 @@ $storedsecret = get_user_preferences('badges_email_verify_secret'); if (!is_null($storedsecret)) { if ($data === $storedsecret) { $storedemail = get_user_preferences('badges_email_verify_address'); + $backpackid = get_user_preferences('badges_email_verify_backpackid'); + $password = get_user_preferences('badges_email_verify_password'); + + $backpack = badges_get_site_backpack($backpackid); $data = new stdClass(); - $data->backpackurl = BADGE_BACKPACKURL; + $data->backpackurl = $backpack->backpackapiurl; + $data->apiversion = $backpack->apiversion; $data->email = $storedemail; - $bp = new OpenBadgesBackpackHandler($data); + $data->password = $password; + $bp = new \core_badges\backpack_api($backpack, $data); // Make sure we have all the required information before trying to save the connection. - $backpackuser = $bp->curl_request('user'); - if (isset($backpackuser->status) && $backpackuser->status === 'okay' && isset($backpackuser->userId)) { - $backpackuid = $backpackuser->userId; - } else { + $backpackuid = $bp->authenticate(); + if (empty($backpackuid) || !empty($backpackuid->error)) { redirect(new moodle_url($redirect), get_string('backpackconnectionunexpectedresult', 'badges'), null, \core\output\notification::NOTIFY_ERROR); } @@ -55,10 +58,12 @@ if (!is_null($storedsecret)) { $obj = new stdClass(); $obj->userid = $USER->id; $obj->email = $data->email; - $obj->backpackurl = $data->backpackurl; + $obj->backpackurl = $backpack->backpackapiurl; + $obj->apiversion = $backpack->apiversion; $obj->backpackuid = $backpackuid; $obj->autosync = 0; - $obj->password = ''; + $obj->password = $password; + $DB->insert_record('badge_backpack', $obj); // Remove the verification vars and redirect to the mypackpack page. diff --git a/badges/backpacks.php b/badges/backpacks.php new file mode 100644 index 00000000000..341d511f47b --- /dev/null +++ b/badges/backpacks.php @@ -0,0 +1,78 @@ +. + +/** + * Display a list of badge backpacks for the site. + * + * @package core_badges + * @copyright 2019 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../config.php'); +require_once($CFG->libdir . '/badgeslib.php'); +$context = context_system::instance(); +$PAGE->set_context($context); + +require_login(0, false); +require_capability('moodle/badges:manageglobalsettings', $context); +// There should be an admin setting to completely turn off badges. +$output = $PAGE->get_renderer('core', 'badges'); + +$id = optional_param('id', 0, PARAM_INT); +$action = optional_param('action', '', PARAM_ALPHA); + +$PAGE->set_pagelayout('admin'); +$url = new moodle_url('/badges/backpacks.php'); + +if (empty($CFG->badges_allowexternalbackpack)) { + redirect($CFG->wwwroot); +} + +$PAGE->set_url($url); +$PAGE->set_title(get_string('managebackpacks', 'badges')); +$PAGE->set_heading($SITE->fullname); +if ($action == 'edit') { + $backpack = null; + if (!empty($id)) { + $backpack = badges_get_site_backpack($id); + } + $form = new \core_badges\form\external_backpack(null, ['externalbackpack' => $backpack]); + if ($form->is_cancelled()) { + redirect($url); + } else if ($data = $form->get_data()) { + require_sesskey(); + if (!empty($data->id)) { + badges_update_site_backpack($data->id, $data); + } else { + badges_create_site_backpack($data); + } + redirect($url); + } + + echo $OUTPUT->header(); + echo $output->heading(get_string('managebackpacks', 'badges')); + + $form->display(); +} else { + echo $OUTPUT->header(); + echo $output->heading(get_string('managebackpacks', 'badges')); + + $page = new \core_badges\output\external_backpacks_page($url); + echo $output->render($page); +} + +echo $OUTPUT->footer(); diff --git a/badges/badge.php b/badges/badge.php index 560bb88867a..23a59a6bc36 100644 --- a/badges/badge.php +++ b/badges/badge.php @@ -38,7 +38,7 @@ $PAGE->set_url('/badges/badge.php', array('hash' => $id)); $PAGE->set_pagelayout('base'); $PAGE->set_title(get_string('issuedbadge', 'badges')); -$badge = new issued_badge($id); +$badge = new \core_badges\output\issued_badge($id); if (!empty($badge->recipient->id)) { if ($bake && ($badge->recipient->id == $USER->id)) { $name = str_replace(' ', '_', $badge->badgeclass['name']) . '.png'; diff --git a/badges/badge_json.php b/badges/badge_json.php index 2362bade70d..0f0906464b6 100644 --- a/badges/badge_json.php +++ b/badges/badge_json.php @@ -108,7 +108,7 @@ if ($badge->status != BADGE_STATUS_INACTIVE) { if ($item->targetcode) { $alignment['targetCode'] = $item->targetcode; } - $json['alignment'][] = $alignment; + $json['alignments'][] = $alignment; } } } else if ($action == 0) { diff --git a/badges/classes/assertion.php b/badges/classes/assertion.php index e1e0451906d..3e7055b32de 100644 --- a/badges/classes/assertion.php +++ b/badges/classes/assertion.php @@ -87,12 +87,40 @@ class core_badges_assertion { $this->_obversion = $obversion; } + /** + * Get the local id for this badge. + * + * @return int + */ + public function get_badge_id() { + $badgeid = 0; + if ($this->_data) { + $badgeid = $this->_data->id; + } + return $badgeid; + } + + /** + * Get the local id for this badge assertion. + * + * @return string + */ + public function get_assertion_hash() { + $hash = ''; + if ($this->_data) { + $hash = $this->_data->uniquehash; + } + return $hash; + } + /** * Get badge assertion. * + * @param boolean $issued Include the nested badge issued information. + * @param boolean $usesalt Hash the identity and include the salt information for the hash. * @return array Badge assertion. */ - public function get_badge_assertion() { + public function get_badge_assertion($issued = true, $usesalt = true) { global $CFG; $assertion = array(); if ($this->_data) { @@ -104,17 +132,27 @@ class core_badges_assertion { // Required. $assertion['uid'] = $hash; $assertion['recipient'] = array(); - $assertion['recipient']['identity'] = 'sha256$' . hash('sha256', $email . $CFG->badges_badgesalt); + if ($usesalt) { + $assertion['recipient']['identity'] = 'sha256$' . hash('sha256', $email . $CFG->badges_badgesalt); + } else { + $assertion['recipient']['identity'] = $email; + } $assertion['recipient']['type'] = 'email'; // Currently the only supported type. $assertion['recipient']['hashed'] = true; // We are always hashing recipient. - $assertion['recipient']['salt'] = $CFG->badges_badgesalt; - $assertion['badge'] = $classurl->out(false); + if ($usesalt) { + $assertion['recipient']['salt'] = $CFG->badges_badgesalt; + } + if ($issued) { + $assertion['badge'] = $classurl->out(false); + } $assertion['verify'] = array(); $assertion['verify']['type'] = 'hosted'; // 'Signed' is not implemented yet. $assertion['verify']['url'] = $assertionurl->out(false); $assertion['issuedOn'] = $this->_data->dateissued; + if ($issued) { + $assertion['evidence'] = $this->_url->out(false); // Currently issued badge URL. + } // Optional. - $assertion['evidence'] = $this->_url->out(false); // Currently issued badge URL. if (!empty($this->_data->dateexpire)) { $assertion['expires'] = $this->_data->dateexpire; } @@ -126,25 +164,42 @@ class core_badges_assertion { /** * Get badge class information. * + * @param boolean $issued Include the nested badge issuer information. * @return array Badge Class information. */ - public function get_badge_class() { - $class = array(); + public function get_badge_class($issued = true) { + $class = []; if ($this->_data) { if (empty($this->_data->courseid)) { $context = context_system::instance(); } else { $context = context_course::instance($this->_data->courseid); } - $issuerurl = new moodle_url('/badges/assertion.php', array('b' => $this->_data->uniquehash, 'action' => 0)); - // Required. $class['name'] = $this->_data->name; $class['description'] = $this->_data->description; - $class['image'] = moodle_url::make_pluginfile_url($context->id, 'badges', 'badgeimage', $this->_data->id, '/', 'f1')->out(false); + $storage = get_file_storage(); + $imagefile = $storage->get_file($context->id, 'badges', 'badgeimage', $this->_data->id, '/', 'f1.png'); + if ($imagefile) { + $imagedata = base64_encode($imagefile->get_content()); + } else { + if (defined('PHPUNIT_TEST') && PHPUNIT_TEST) { + // Unit tests the file might not exist yet. + $imagedata = ''; + } else { + throw new coding_exception('Image file does not exist.'); + } + } + $class['image'] = 'data:image/png;base64,' . $imagedata; $class['criteria'] = $this->_url->out(false); // Currently issued badge URL. - $class['issuer'] = $issuerurl->out(false); + if ($issued) { + $issuerurl = new moodle_url('/badges/assertion.php', array('b' => $this->_data->uniquehash, 'action' => 0)); + $class['issuer'] = $issuerurl->out(false); + } $this->embed_data_badge_version2($class, OPEN_BADGES_V2_TYPE_BADGE); + if (!$issued) { + unset($class['issuer']); + } } return $class; } @@ -155,6 +210,7 @@ class core_badges_assertion { * @return array Issuer information. */ public function get_issuer() { + global $CFG; $issuer = array(); if ($this->_data) { // Required. @@ -163,6 +219,8 @@ class core_badges_assertion { // Optional. if (!empty($this->_data->issuercontact)) { $issuer['email'] = $this->_data->issuercontact; + } else { + $issuer['email'] = $CFG->badges_defaultissuercontact; } } $this->embed_data_badge_version2($issuer, OPEN_BADGES_V2_TYPE_ISSUER); @@ -284,7 +342,6 @@ class core_badges_assertion { } unset($json['uid']); } - // For Badge. if ($type == OPEN_BADGES_V2_TYPE_BADGE) { $json['@context'] = OPEN_BADGES_V2_CONTEXT; @@ -302,23 +359,25 @@ class core_badges_assertion { $json['endorsement'] = $endorsementurl->out(false); } if ($alignments = $this->get_alignments()) { - $json['alignment'] = $alignments; + $json['alignments'] = $alignments; } if ($this->_data->imageauthorname || $this->_data->imageauthoremail || $this->_data->imageauthorurl || $this->_data->imagecaption) { - $urlimage = moodle_url::make_pluginfile_url($context->id, - 'badges', 'badgeimage', $this->_data->id, '/', 'f1')->out(false); - $json['image'] = array(); - $json['image']['id'] = $urlimage; - if ($this->_data->imageauthorname || $this->_data->imageauthoremail || $this->_data->imageauthorurl) { - $authorimage = new moodle_url('/badges/image_author_json.php', array('id' => $this->_data->id)); - $json['image']['author'] = $authorimage->out(false); - } - if ($this->_data->imagecaption) { - $json['image']['caption'] = $this->_data->imagecaption; + $storage = get_file_storage(); + $imagefile = $storage->get_file($context->id, 'badges', 'badgeimage', $this->_data->id, '/', 'f1.png'); + if ($imagefile) { + $imagedata = base64_encode($imagefile->get_content()); + } else { + // The file might not exist in unit tests. + if (defined('PHPUNIT_TEST') && PHPUNIT_TEST) { + $imagedata = ''; + } else { + throw new coding_exception('Image file does not exist.'); + } } + $json['image'] = 'data:image/png;base64,' . $imagedata; } } diff --git a/badges/classes/backpack_api.php b/badges/classes/backpack_api.php new file mode 100644 index 00000000000..94f95ec4c5b --- /dev/null +++ b/badges/classes/backpack_api.php @@ -0,0 +1,678 @@ +. + +/** + * Communicate with backpacks. + * + * @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->libdir . '/filelib.php'); + +use cache; +use coding_exception; +use core_badges\external\assertion_exporter; +use core_badges\external\collection_exporter; +use core_badges\external\issuer_exporter; +use core_badges\external\badgeclass_exporter; +use curl; +use stdClass; + +define('BADGE_ACCESS_TOKEN', 'access'); +define('BADGE_USER_ID_TOKEN', 'user_id'); +define('BADGE_BACKPACK_ID_TOKEN', 'backpack_id'); +define('BADGE_REFRESH_TOKEN', 'refresh'); +define('BADGE_EXPIRES_TOKEN', 'expires'); + +/** + * Class for communicating with backpacks. + */ +class backpack_api { + + /** @var string The email address of the issuer or the backpack owner. */ + private $email; + + /** @var string The base url used for api requests to this backpack. */ + private $backpackapiurl; + + /** @var integer The backpack api version to use. */ + private $backpackapiversion; + + /** @var string The password to authenticate requests. */ + private $password; + + /** @var boolean User or site api requests. */ + private $isuserbackpack; + + /** @var integer The id of the backpack we are talking to. */ + private $backpackid; + + /** @var \backpack_api_mapping[] List of apis for the user or site using api version 1 or 2. */ + private $mappings = []; + + /** + * Create a wrapper to communicate with the backpack. + * + * The resulting class can only do either site backpack communication or + * user backpack communication. + * + * @param stdClass $sitebackpack The site backpack record + * @param mixed $userbackpack Optional - if passed it represents the users backpack. + */ + public function __construct($sitebackpack, $userbackpack = false) { + global $CFG; + $admin = get_admin(); + + $this->backpackapiurl = $sitebackpack->backpackapiurl; + $this->backpackapiurl = $sitebackpack->backpackapiurl; + $this->backpackapiversion = $sitebackpack->apiversion; + $this->password = $sitebackpack->password; + $this->email = !empty($CFG->badges_defaultissuercontact) ? $CFG->badges_defaultissuercontact : $admin->email; + $this->isuserbackpack = false; + $this->backpackid = $sitebackpack->id; + if (!empty($userbackpack)) { + $this->isuserbackpack = true; + $this->backpackapiurl = $userbackpack->backpackurl; + $this->backpackapiversion = $userbackpack->apiversion; + $this->password = $userbackpack->password; + $this->email = $userbackpack->email; + } + + $this->define_mappings(); + } + + /** + * Define the mappings supported by this usage and api version. + */ + private function define_mappings() { + if ($this->backpackapiversion == OPEN_BADGES_V2) { + if ($this->isuserbackpack) { + $mapping = []; + $mapping[] = [ + 'collections', // Action. + '[URL]/backpack/collections', // URL + [], // Post params. + '', // Request exporter. + 'core_badges\external\collection_exporter', // Response exporter. + true, // Multiple. + 'get', // Method. + true, // JSON Encoded. + true // Auth required. + ]; + $mapping[] = [ + 'user', // Action. + '[SCHEME]://[HOST]/o/token', // URL + ['username' => '[EMAIL]', 'password' => '[PASSWORD]'], // Post params. + '', // Request exporter. + 'oauth_token_response', // Response exporter. + false, // Multiple. + 'post', // Method. + false, // JSON Encoded. + false, // Auth required. + ]; + $mapping[] = [ + 'issuer', // Action. + '[URL]/issuers/[PARAM2]', // URL + [], // Post params. + '', // Request exporter. + 'core_badges\external\issuer_exporter', // Response exporter. + false, // Multiple. + 'get', // Method. + true, // JSON Encoded. + true // Auth required. + ]; + $mapping[] = [ + 'badgeclass', // Action. + '[URL]/badgeclasses/[PARAM2]', // URL + [], // Post params. + '', // Request exporter. + 'core_badges\external\badgeclass_exporter', // Response exporter. + false, // Multiple. + 'get', // Method. + true, // JSON Encoded. + true // Auth required. + ]; + $mapping[] = [ + 'assertion', // Action. + '[URL]/backpack/assertions/[PARAM2]', // URL + [], // Post params. + '', // Request exporter. + 'core_badges\external\assertion_exporter', // Response exporter. + false, // Multiple. + 'get', // Method. + true, // JSON Encoded. + true // Auth required. + ]; + $mapping[] = [ + 'badges', // Action. + '[URL]/backpack/collections/[PARAM1]', // URL + [], // Post params. + '', // Request exporter. + 'core_badges\external\collection_exporter', // Response exporter. + true, // Multiple. + 'get', // Method. + true, // JSON Encoded. + true // Auth required. + ]; + foreach ($mapping as $map) { + $map[] = true; // User api function. + $map[] = OPEN_BADGES_V2; // V2 function. + $this->mappings[] = new backpack_api_mapping(...$map); + } + } else { + $mapping = []; + $mapping[] = [ + 'user', // Action. + '[SCHEME]://[HOST]/o/token', // URL + ['username' => '[EMAIL]', 'password' => '[PASSWORD]'], // Post params. + '', // Request exporter. + 'oauth_token_response', // Response exporter. + false, // Multiple. + 'post', // Method. + false, // JSON Encoded. + false // Auth required. + ]; + $mapping[] = [ + 'issuers', // Action. + '[URL]/issuers', // URL + '[PARAM]', // Post params. + 'core_badges\external\issuer_exporter', // Request exporter. + 'core_badges\external\issuer_exporter', // Response exporter. + false, // Multiple. + 'post', // Method. + true, // JSON Encoded. + true // Auth required. + ]; + $mapping[] = [ + 'badgeclasses', // Action. + '[URL]/issuers/[PARAM2]/badgeclasses', // URL + '[PARAM]', // Post params. + 'core_badges\external\badgeclass_exporter', // Request exporter. + 'core_badges\external\badgeclass_exporter', // Response exporter. + false, // Multiple. + 'post', // Method. + true, // JSON Encoded. + true // Auth required. + ]; + $mapping[] = [ + 'assertions', // Action. + '[URL]/badgeclasses/[PARAM2]/assertions', // URL + '[PARAM]', // Post params. + 'core_badges\external\assertion_exporter', // Request exporter. + 'core_badges\external\assertion_exporter', // Response exporter. + false, // Multiple. + 'post', // Method. + true, // JSON Encoded. + true // Auth required. + ]; + foreach ($mapping as $map) { + $map[] = false; // Site api function. + $map[] = OPEN_BADGES_V2; // V2 function. + $this->mappings[] = new backpack_api_mapping(...$map); + } + } + } else { + if ($this->isuserbackpack) { + $mapping = []; + $mapping[] = [ + 'user', // Action. + '[URL]/displayer/convert/email', // URL + ['email' => '[EMAIL]'], // Post params. + '', // Request exporter. + 'convert_email_response', // Response exporter. + false, // Multiple. + 'post', // Method. + false, // JSON Encoded. + false // Auth required. + ]; + $mapping[] = [ + 'groups', // Action. + '[URL]/displayer/[PARAM1]/groups.json', // URL + [], // Post params. + '', // Request exporter. + '', // Response exporter. + false, // Multiple. + 'get', // Method. + true, // JSON Encoded. + true // Auth required. + ]; + $mapping[] = [ + 'badges', // Action. + '[URL]/displayer/[PARAM2]/group/[PARAM1].json', // URL + [], // Post params. + '', // Request exporter. + '', // Response exporter. + false, // Multiple. + 'get', // Method. + true, // JSON Encoded. + true // Auth required. + ]; + foreach ($mapping as $map) { + $map[] = true; // User api function. + $map[] = OPEN_BADGES_V1; // V1 function. + $this->mappings[] = new backpack_api_mapping(...$map); + } + } else { + $mapping = []; + $mapping[] = [ + 'user', // Action. + '[URL]/displayer/convert/email', // URL + ['email' => '[EMAIL]'], // Post params. + '', // Request exporter. + 'convert_email_response', // Response exporter. + false, // Multiple. + 'post', // Method. + false, // JSON Encoded. + false // Auth required. + ]; + foreach ($mapping as $map) { + $map[] = false; // Site api function. + $map[] = OPEN_BADGES_V1; // V1 function. + $this->mappings[] = new backpack_api_mapping(...$map); + } + } + } + } + + /** + * Make an api request + * + * @param string $action The api function. + * @param string $collection An api parameter + * @param string $entityid An api parameter + * @param string $postdata The body of the api request. + * @return mixed + */ + private function curl_request($action, $collection = null, $entityid = null, $postdata = null) { + global $CFG, $SESSION; + + $curl = new curl(); + $authrequired = false; + if ($this->backpackapiversion == OPEN_BADGES_V1) { + $useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN); + if (isset($SESSION->$useridkey)) { + if ($collection == null) { + $collection = $SESSION->$useridkey; + } else { + $entityid = $SESSION->$useridkey; + } + } + } + foreach ($this->mappings as $mapping) { + if ($mapping->is_match($action)) { + return $mapping->request( + $this->backpackapiurl, + $collection, + $entityid, + $this->email, + $this->password, + $postdata, + $this->backpackid + ); + } + } + + throw new coding_exception('Unknown request'); + } + + /** + * Get the id to use for requests with this api. + * + * @return integer + */ + private function get_auth_user_id() { + global $USER; + + if ($this->isuserbackpack) { + return $USER->id; + } else { + // The access tokens for the system backpack are shared. + return -1; + } + } + + /** + * Get the name of the key to store this access token type. + * + * @param string $type + * @return string + */ + private function get_token_key($type) { + // This should be removed when everything has a mapping. + $prefix = 'badges_'; + if ($this->isuserbackpack) { + $prefix .= 'user_backpack_'; + } else { + $prefix .= 'site_backpack_'; + } + $prefix .= $type . '_token'; + return $prefix; + } + + /** + * Normalise the return from a missing user request. + * + * @param string $status + * @return mixed + */ + private function check_status($status) { + // V1 ONLY. + switch($status) { + case "missing": + $response = array( + 'status' => $status, + 'message' => get_string('error:nosuchuser', 'badges') + ); + return $response; + } + return false; + } + + /** + * Make an api request to get an assertion + * + * @param string $entityid The id of the assertion. + * @return mixed + */ + public function get_assertion($entityid) { + // V2 Only. + if ($this->backpackapiversion == OPEN_BADGES_V1) { + throw new coding_exception('Not supported in this backpack API'); + } + + return $this->curl_request('assertion', null, $entityid); + } + + /** + * Get a badge class. + * + * @param string $entityid The id of the badge class. + * @return mixed + */ + public function get_badgeclass($entityid) { + // V2 Only. + if ($this->backpackapiversion == OPEN_BADGES_V1) { + throw new coding_exception('Not supported in this backpack API'); + } + + return $this->curl_request('badgeclass', null, $entityid); + } + + /** + * Create a badgeclass assertion. + * + * @param string $entityid The id of the badge class. + * @param string $data The structure of the badge class assertion. + * @return mixed + */ + public function put_badgeclass_assertion($entityid, $data) { + // V2 Only. + if ($this->backpackapiversion == OPEN_BADGES_V1) { + throw new coding_exception('Not supported in this backpack API'); + } + + return $this->curl_request('assertions', null, $entityid, $data); + } + + /** + * Select collections from a backpack. + * + * @param string $backpackid The id of the backpack + * @param stdClass[] $collections List of collections with collectionid or entityid. + * @return boolean + */ + public function set_backpack_collections($backpackid, $collections) { + global $DB, $USER; + + // Delete any previously selected collections. + $sqlparams = array('backpack' => $backpackid); + $select = 'backpackid = :backpack '; + $DB->delete_records_select('badge_external', $select, $sqlparams); + $badgescache = cache::make('core', 'externalbadges'); + + // Insert selected collections if they are not in database yet. + foreach ($collections as $collection) { + $obj = new stdClass(); + $obj->backpackid = $backpackid; + if ($this->backpackapiversion == OPEN_BADGES_V1) { + $obj->collectionid = (int) $collection; + } else { + $obj->entityid = $collection; + $obj->collectionid = -1; + } + if (!$DB->record_exists('badge_external', (array) $obj)) { + $DB->insert_record('badge_external', $obj); + } + } + $badgescache->delete($USER->id); + return true; + } + + /** + * Create a badgeclass + * + * @param string $entityid The id of the entity. + * @param string $data The structure of the badge class. + * @return mixed + */ + public function put_badgeclass($entityid, $data) { + // V2 Only. + if ($this->backpackapiversion == OPEN_BADGES_V1) { + throw new coding_exception('Not supported in this backpack API'); + } + + return $this->curl_request('badgeclasses', null, $entityid, $data); + } + + /** + * Create an issuer + * + * @param string $data The structure of the issuer. + * @return mixed + */ + public function put_issuer($data) { + // V2 Only. + if ($this->backpackapiversion == OPEN_BADGES_V1) { + throw new coding_exception('Not supported in this backpack API'); + } + + return $this->curl_request('issuers', null, null, $data); + } + + /** + * Get an issuer by id + * + * @param string $entityid the id of the issuer. + * @return mixed + */ + public function get_issuer($entityid) { + // V2 Only. + if ($this->backpackapiversion == OPEN_BADGES_V1) { + throw new coding_exception('Not supported in this backpack API'); + } + + $result = $this->curl_request('issuer', null, $entityid); + return $result; + } + + /** + * Authenticate using the stored email and password and save the valid access tokens. + * + * @return integer The id of the authenticated user. + */ + public function authenticate() { + global $SESSION; + + $backpackidkey = $this->get_token_key(BADGE_BACKPACK_ID_TOKEN); + $backpackid = isset($SESSION->$backpackidkey) ? $SESSION->$backpackidkey : 0; + // If the backpack is changed we need to expire sessions. + if ($backpackid == $this->backpackid) { + if ($this->backpackapiversion == OPEN_BADGES_V2) { + $useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN); + $authuserid = isset($SESSION->$useridkey) ? $SESSION->$useridkey : 0; + if ($authuserid == $this->get_auth_user_id()) { + $expireskey = $this->get_token_key(BADGE_EXPIRES_TOKEN); + if (isset($SESSION->$expireskey)) { + $expires = $SESSION->$expireskey; + if ($expires > time()) { + // We have a current access token for this user + // that has not expired. + return -1; + } + } + } + } else { + $useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN); + $authuserid = isset($SESSION->$useridkey) ? $SESSION->$useridkey : 0; + if (!empty($authuserid)) { + return $authuserid; + } + } + } + return $this->curl_request('user', $this->email); + } + + /** + * Get all collections in this backpack. + * + * @return stdClass[] The collections. + */ + public function get_collections() { + global $PAGE; + + if ($this->authenticate()) { + if ($this->backpackapiversion == OPEN_BADGES_V1) { + $result = $this->curl_request('groups'); + if (isset($result->groups)) { + $result = $result->groups; + } + } else { + $result = $this->curl_request('collections'); + } + if ($result) { + return $result; + } + } + return []; + } + + /** + * Get one collection by id. + * + * @param integer $collectionid + * @return stdClass The collection. + */ + public function get_collection_record($collectionid) { + global $DB; + + if ($this->backpackapiversion == OPEN_BADGES_V1) { + return $DB->get_fieldset_select('badge_external', 'collectionid', 'backpackid = :bid', array('bid' => $collectionid)); + } else { + return $DB->get_fieldset_select('badge_external', 'entityid', 'backpackid = :bid', array('bid' => $collectionid)); + } + } + + /** + * Disconnect the backpack from this user. + * + * @param integer $userid The user in Moodle + * @param integer $backpackid The backpack to disconnect + * @return boolean + */ + public function disconnect_backpack($userid, $backpackid) { + global $DB, $USER; + + if (\core\session\manager::is_loggedinas() || $userid != $USER->id) { + // Can't change someone elses backpack settings. + return false; + } + + $badgescache = cache::make('core', 'externalbadges'); + + $DB->delete_records('badge_external', array('backpackid' => $backpackid)); + $DB->delete_records('badge_backpack', array('userid' => $userid)); + $badgescache->delete($userid); + return true; + } + + /** + * Handle the response from getting a collection to map to an id. + * + * @param stdClass $data The response data. + * @return string The collection id. + */ + public function get_collection_id_from_response($data) { + if ($this->backpackapiversion == OPEN_BADGES_V1) { + return $data->groupId; + } else { + return $data->entityId; + } + } + + /** + * Get the list of badges in a collection. + * + * @param stdClass $collection The collection to deal with. + * @param boolean $expanded Fetch all the sub entities. + * @return stdClass[] + */ + public function get_badges($collection, $expanded = false) { + if ($this->authenticate()) { + if ($this->backpackapiversion == OPEN_BADGES_V1) { + if (empty($collection->collectionid)) { + return []; + } + $result = $this->curl_request('badges', $collection->collectionid); + return $result->badges; + } else { + if (empty($collection->entityid)) { + return []; + } + // Now we can make requests. + $badges = $this->curl_request('badges', $collection->entityid); + if (count($badges) == 0) { + return []; + } + $badges = $badges[0]; + if ($expanded) { + $publicassertions = []; + foreach ($badges->assertions as $assertion) { + $remoteassertion = $this->get_assertion($assertion); + $remotebadge = $this->get_badgeclass($remoteassertion->badgeclass); + if (!$remotebadge) { + continue; + } + $remoteissuer = $this->get_issuer($remotebadge->issuer); + $badgeclone = clone $remotebadge; + $badgeclone->issuer = $remoteissuer; + $remoteassertion->badge = $badgeclone; + $remotebadge->assertion = $remoteassertion; + + $publicassertions[] = $remotebadge; + } + $badges = $publicassertions; + } + return $badges; + } + } + } +} diff --git a/badges/classes/backpack_api_mapping.php b/badges/classes/backpack_api_mapping.php new file mode 100644 index 00000000000..1ff19f1c72b --- /dev/null +++ b/badges/classes/backpack_api_mapping.php @@ -0,0 +1,369 @@ +. + +/** + * Represent the url for each method and the encoding of the parameters and response. + * + * @package core_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(); + +global $CFG; +require_once($CFG->libdir . '/filelib.php'); + +use context_system; +use core_badges\external\assertion_exporter; +use core_badges\external\collection_exporter; +use core_badges\external\issuer_exporter; +use core_badges\external\badgeclass_exporter; +use curl; + +/** + * Represent a single method for the remote api. + */ +class backpack_api_mapping { + + /** @var string The action of this method. */ + public $action; + + /** @var string The base url of this backpack. */ + private $url; + + /** @var array List of parameters for this method. */ + public $params; + + /** @var string Name of a class to export parameters for this method. */ + public $requestexporter; + + /** @var string Name of a class to export response for this method. */ + public $responseexporter; + + /** @var boolean This method returns an array of responses. */ + public $multiple; + + /** @var string get or post methods. */ + public $method; + + /** @var boolean json decode the response. */ + public $json; + + /** @var boolean Authentication is required for this request. */ + public $authrequired; + + /** @var boolean Differentiate the function that can be called on a user backpack or a site backpack. */ + private $isuserbackpack; + + /** + * Create a mapping. + * + * @param string $action The action of this method. + * @param string $url The base url of this backpack. + * @param mixed $postparams List of parameters for this method. + * @param string $requestexporter Name of a class to export parameters for this method. + * @param string $responseexporter Name of a class to export response for this method. + * @param boolean $multiple This method returns an array of responses. + * @param string $method get or post methods. + * @param boolean $json json decode the response. + * @param boolean $authrequired Authentication is required for this request. + * @param boolean $isuserbackpack user backpack or a site backpack. + * @param integer $backpackapiversion OpenBadges version 1 or 2. + */ + public function __construct($action, $url, $postparams, $requestexporter, $responseexporter, + $multiple, $method, $json, $authrequired, $isuserbackpack, $backpackapiversion) { + $this->action = $action; + $this->url = $url; + $this->postparams = $postparams; + $this->requestexporter = $requestexporter; + $this->responseexporter = $responseexporter; + $this->multiple = $multiple; + $this->method = $method; + $this->json = $json; + $this->authrequired = $authrequired; + $this->isuserbackpack = $isuserbackpack; + $this->backpackapiversion = $backpackapiversion; + } + + /** + * Get the unique key for the token. + * + * @param string $type The type of token. + * @return string + */ + private function get_token_key($type) { + $prefix = 'badges_'; + if ($this->isuserbackpack) { + $prefix .= 'user_backpack_'; + } else { + $prefix .= 'site_backpack_'; + } + $prefix .= $type . '_token'; + return $prefix; + } + + /** + * Does the action match this mapping? + * + * @param string $action The action. + * @return boolean + */ + public function is_match($action) { + return $this->action == $action; + } + + /** + * Parse the method url and insert parameters. + * + * @param string $apiurl The raw apiurl. + * @param string $param1 The first parameter. + * @param string $param2 The second parameter. + * @return string + */ + private function get_url($apiurl, $param1, $param2) { + $urlscheme = parse_url($apiurl, PHP_URL_SCHEME); + $urlhost = parse_url($apiurl, PHP_URL_HOST); + + $url = $this->url; + $url = str_replace('[SCHEME]', $urlscheme, $url); + $url = str_replace('[HOST]', $urlhost, $url); + $url = str_replace('[URL]', $apiurl, $url); + $url = str_replace('[PARAM1]', $param1, $url); + $url = str_replace('[PARAM2]', $param2, $url); + + return $url; + } + + /** + * Parse the post parameters and insert replacements. + * + * @param string $email The api username. + * @param string $password The api password. + * @param string $param The parameter. + * @return mixed + */ + private function get_post_params($email, $password, $param) { + global $PAGE; + + if ($this->method == 'get') { + return ''; + } + + $request = $this->postparams; + if ($request === '[PARAM]') { + $value = $param; + foreach ($value as $key => $keyvalue) { + if (gettype($value[$key]) == 'array') { + $newkey = 'related_' . $key; + $value[$newkey] = $value[$key]; + unset($value[$key]); + } + } + } else if (is_array($request)) { + foreach ($request as $key => $value) { + if ($value == '[EMAIL]') { + $value = $email; + $request[$key] = $value; + } else if ($value == '[PASSWORD]') { + $value = $password; + $request[$key] = $value; + } + } + } + $context = context_system::instance(); + $exporter = $this->requestexporter; + $output = $PAGE->get_renderer('core', 'badges'); + if (!empty($exporter)) { + $exporterinstance = new $exporter($value, ['context' => $context]); + $request = $exporterinstance->export($output); + } + if ($this->json) { + return json_encode($request); + } + return $request; + } + + /** + * Read the response from a V1 user request and save the userID. + * + * @param string $response The request response. + * @param integer $backpackid The backpack id. + * @return boolean + */ + private function convert_email_response($response, $backpackid) { + global $SESSION; + + if (isset($response->status) && $response->status == 'okay') { + + // Remember the tokens. + $useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN); + $backpackidkey = $this->get_token_key(BADGE_BACKPACK_ID_TOKEN); + + $SESSION->$useridkey = $response->userId; + $SESSION->$backpackidkey = $backpackid; + return $response->userId; + } + return false; + } + + /** + * Get the user id from a previous user request. + * + * @return integer + */ + private function get_auth_user_id() { + global $USER; + + if ($this->isuserbackpack) { + return $USER->id; + } else { + // The access tokens for the system backpack are shared. + return -1; + } + } + + /** + * Parse the response from an openbadges 2 login. + * + * @param string $response The request response data. + * @param integer $backpackid The id of the backpack. + * @return mixed + */ + private function oauth_token_response($response, $backpackid) { + global $SESSION; + + if (isset($response->access_token) && isset($response->refresh_token)) { + // Remember the tokens. + $accesskey = $this->get_token_key(BADGE_ACCESS_TOKEN); + $refreshkey = $this->get_token_key(BADGE_REFRESH_TOKEN); + $expireskey = $this->get_token_key(BADGE_EXPIRES_TOKEN); + $useridkey = $this->get_token_key(BADGE_USER_ID_TOKEN); + $backpackidkey = $this->get_token_key(BADGE_BACKPACK_ID_TOKEN); + if (isset($response->expires_in)) { + $timeout = $response->expires_in; + } else { + $timeout = 15 * 60; // 15 minute timeout if none set. + } + $expires = $timeout + time(); + + $SESSION->$expireskey = $expires; + $SESSION->$useridkey = $this->get_auth_user_id(); + $SESSION->$accesskey = $response->access_token; + $SESSION->$refreshkey = $response->refresh_token; + $SESSION->$backpackidkey = $backpackid; + return -1; + } + return $response; + } + + /** + * Standard options used for all curl requests. + * + * @return array + */ + private function get_curl_options() { + return array( + 'FRESH_CONNECT' => true, + 'RETURNTRANSFER' => true, + 'FORBID_REUSE' => true, + 'HEADER' => 0, + 'CONNECTTIMEOUT' => 3, + 'CONNECTTIMEOUT' => 3, + // Follow redirects with the same type of request when sent 301, or 302 redirects. + 'CURLOPT_POSTREDIR' => 3, + ); + } + + /** + * Make an api request and parse the response. + * + * @param string $apiurl Raw request url. + * @param string $urlparam1 Parameter for the request. + * @param string $urlparam2 Parameter for the request. + * @param string $email User email for authentication. + * @param string $password for authentication. + * @param mixed $postparam Raw data for the post body. + * @param string $backpackid the id of the backpack to use. + * @return mixed + */ + public function request($apiurl, $urlparam1, $urlparam2, $email, $password, $postparam, $backpackid) { + global $SESSION, $PAGE; + + $curl = new curl(); + + $url = $this->get_url($apiurl, $urlparam1, $urlparam2); + + if ($this->authrequired) { + $accesskey = $this->get_token_key(BADGE_ACCESS_TOKEN); + if (isset($SESSION->$accesskey)) { + $token = $SESSION->$accesskey; + $curl->setHeader('Authorization: Bearer ' . $token); + } + } + if ($this->json) { + $curl->setHeader(array('Content-type: application/json')); + } + $curl->setHeader(array('Accept: application/json', 'Expect:')); + $options = $this->get_curl_options(); + + $post = $this->get_post_params($email, $password, $postparam); + if ($this->method == 'get') { + $response = $curl->get($url, $post, $options); + } else if ($this->method == 'post') { + $response = $curl->post($url, $post, $options); + } + $response = json_decode($response); + + if (isset($response->result)) { + $response = $response->result; + } + $context = context_system::instance(); + $exporter = $this->responseexporter; + if (class_exists($exporter)) { + $output = $PAGE->get_renderer('core', 'badges'); + if (!$this->multiple) { + if (count($response)) { + $response = $response[0]; + } + if (empty($response)) { + return null; + } + $apidata = $exporter::map_external_data($response, $this->backpackapiversion); + $exporterinstance = new $exporter($apidata, ['context' => $context]); + $data = $exporterinstance->export($output); + return $data; + } else { + $multiple = []; + if (empty($response)) { + return $multiple; + } + foreach ($response as $data) { + $apidata = $exporter::map_external_data($data, $this->backpackapiversion); + $exporterinstance = new $exporter($apidata, ['context' => $context]); + $multiple[] = $exporterinstance->export($output); + } + return $multiple; + } + } else if (method_exists($this, $exporter)) { + return $this->$exporter($response, $backpackid); + } + return $response; + } +} diff --git a/badges/classes/badge.php b/badges/classes/badge.php new file mode 100644 index 00000000000..1ad779aa62c --- /dev/null +++ b/badges/classes/badge.php @@ -0,0 +1,929 @@ +. + +/** + * 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. + * + */ +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; + } + } + + $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. + * + */ + 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 $archive boolean 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'] = $issuerurl->out(false); + $issuer['type'] = OPEN_BADGES_V2_TYPE_ISSUER; + return $issuer; + } +} diff --git a/badges/classes/external/alignment_exporter.php b/badges/classes/external/alignment_exporter.php index 91a431c2527..d0b24ef943d 100644 --- a/badges/classes/external/alignment_exporter.php +++ b/badges/classes/external/alignment_exporter.php @@ -47,32 +47,36 @@ class alignment_exporter extends exporter { 'id' => [ 'type' => PARAM_INT, 'description' => 'Alignment id', + 'optional' => true, ], 'badgeid' => [ 'type' => PARAM_INT, 'description' => 'Badge id', + 'optional' => true, ], - 'targetname' => [ + 'targetName' => [ 'type' => PARAM_TEXT, 'description' => 'Target name', + 'optional' => true, ], - 'targeturl' => [ + 'targetUrl' => [ 'type' => PARAM_URL, 'description' => 'Target URL', + 'optional' => true, ], - 'targetdescription' => [ + 'targetDescription' => [ 'type' => PARAM_TEXT, 'description' => 'Target description', 'null' => NULL_ALLOWED, 'optional' => true, ], - 'targetframework' => [ + 'targetFramework' => [ 'type' => PARAM_TEXT, 'description' => 'Target framework', 'null' => NULL_ALLOWED, 'optional' => true, ], - 'targetcode' => [ + 'targetCode' => [ 'type' => PARAM_TEXT, 'description' => 'Target code', 'null' => NULL_ALLOWED, diff --git a/badges/classes/external/assertion_exporter.php b/badges/classes/external/assertion_exporter.php new file mode 100644 index 00000000000..84edb4b6c2f --- /dev/null +++ b/badges/classes/external/assertion_exporter.php @@ -0,0 +1,160 @@ +. + +/** + * Contains class for displaying a assertion. + * + * @package core_badges + * @copyright 2019 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_badges\external; + +defined('MOODLE_INTERNAL') || die(); + +use core\external\exporter; +use renderer_base; +use stdClass; + +/** + * Class for displaying a badge competency. + * + * @package core_badges + * @copyright 2019 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class assertion_exporter extends exporter { + + /** + * Map from a request response data to the internal structure. + * + * @param stdClass $data The remote data. + * @param string $apiversion The backpack version used to communicate remotely. + * @return stdClass + */ + public static function map_external_data($data, $apiversion) { + $mapped = new \stdClass(); + if (isset($data->entityType)) { + $mapped->type = $data->entityType; + } else { + $mapped->type = $data->type; + } + if (isset($data->entityId)) { + $mapped->id = $data->entityId; + } else { + $mapped->id = $data->id; + } + if (isset($data->issuedOn)) { + $mapped->issuedOn = $data->issuedOn; + } + if (isset($data->recipient)) { + $mapped->recipient = $data->recipient; + } + if (isset($data->badgeclass)) { + $mapped->badgeclass = $data->badgeclass; + } + $propname = '@context'; + $mapped->$propname = 'https://w3id.org/openbadges/v2'; + return $mapped; + } + + protected static function define_other_properties() { + return array( + 'badge' => array( + 'type' => badgeclass_exporter::read_properties_definition(), + 'optional' => true + ), + 'recipient' => array( + 'type' => recipient_exporter::read_properties_definition(), + 'optional' => true + ), + 'verification' => array( + 'type' => verification_exporter::read_properties_definition(), + 'optional' => true + ) + ); + } + + /** + * We map from related data passed as data to this exporter to clean exportable values. + */ + protected function get_other_values(renderer_base $output) { + global $DB; + $result = []; + + if (array_key_exists('related_badge', $this->data)) { + $exporter = new badgeclass_exporter($this->data['related_badge'], $this->related); + $result['badge'] = $exporter->export($output); + } + if (array_key_exists('related_recipient', $this->data)) { + $exporter = new recipient_exporter($this->data['related_recipient'], $this->related); + $result['recipient'] = $exporter->export($output); + } + if (array_key_exists('related_verify', $this->data)) { + $exporter = new verification_exporter($this->data['related_verify'], $this->related); + $result['verification'] = $exporter->export($output); + } + return $result; + } + + /** + * Return the list of properties. + * + * @return array + */ + protected static function define_properties() { + return [ + 'type' => [ + 'type' => PARAM_ALPHA, + 'description' => 'Issuer', + ], + 'id' => [ + 'type' => PARAM_URL, + 'description' => 'Unique identifier for this assertion', + ], + 'badgeclass' => [ + 'type' => PARAM_RAW, + 'description' => 'Identifier of the badge for this assertion', + 'optional' => true, + ], + 'issuedOn' => [ + 'type' => PARAM_RAW, + 'description' => 'Date this badge was issued', + ], + 'expires' => [ + 'type' => PARAM_RAW, + 'description' => 'Date this badge will expire', + 'optional' => true, + ], + '@context' => [ + 'type' => PARAM_URL, + 'description' => 'Badge version', + ], + ]; + } + + /** + * Returns a list of objects that are related. + * + * @return array + */ + protected static function define_related() { + return array( + 'context' => 'context' + ); + } +} diff --git a/badges/classes/external/backpack_exporter.php b/badges/classes/external/backpack_exporter.php new file mode 100644 index 00000000000..0bc32da51d3 --- /dev/null +++ b/badges/classes/external/backpack_exporter.php @@ -0,0 +1,73 @@ +. + +/** + * Contains competency class for displaying a badge backpack. + * + * @package core_badges + * @copyright 2019 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_badges\external; + +defined('MOODLE_INTERNAL') || die(); + +use core\external\exporter; + +/** + * Class for displaying a badge competency. + * + * @package core_badges + * @copyright 2018 Dani Palou + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class backpack_exporter extends exporter { + + /** + * Return the list of properties. + * + * @return array + */ + protected static function define_properties() { + return [ + 'id' => [ + 'type' => PARAM_INT, + 'description' => 'Backpack id', + ], + 'backpackapiurl' => [ + 'type' => PARAM_URL, + 'description' => 'Backpack API URL', + ], + 'backpackweburl' => [ + 'type' => PARAM_URL, + 'description' => 'Backpack Website URL', + ], + 'sitebackpack' => [ + 'type' => PARAM_BOOL, + 'description' => 'Is this the current site backpack', + ], + 'apiversion' => [ + 'type' => PARAM_FLOAT, + 'description' => 'API version supported', + ], + 'sortorder' => [ + 'type' => PARAM_INT, + 'description' => 'Sort order' + ] + ]; + } +} diff --git a/badges/classes/external/badgeclass_exporter.php b/badges/classes/external/badgeclass_exporter.php new file mode 100644 index 00000000000..bfc19dd18f4 --- /dev/null +++ b/badges/classes/external/badgeclass_exporter.php @@ -0,0 +1,227 @@ +. + +/** + * Contains class for displaying a badgeclass. + * + * @package core_badges + * @copyright 2019 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_badges\external; + +defined('MOODLE_INTERNAL') || die(); + +use core\external\exporter; +use renderer_base; + +/** + * Class for displaying a badge competency. + * + * @package core_badges + * @copyright 2019 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class badgeclass_exporter extends exporter { + + /** + * Constructor - saves the persistent object, and the related objects. + * + * @param mixed $data - Either an stdClass or an array of values. + * @param array $related - An optional list of pre-loaded objects related to this object. + */ + public function __construct($data, $related = array()) { + $pick = $this->pick_related(); + foreach ($pick as $one) { + $isarray = false; + // Allow [] to mean an array of values. + if (substr($one, -2) === '[]') { + $one = substr($one, 0, -2); + $isarray = true; + } + $prefixed = 'related_' . $one; + if (array_key_exists($one, $data) && !array_key_exists($one, $related)) { + if ($isarray) { + $newrelated = []; + foreach ($data[$one] as $item) { + $newrelated[] = (object) $item; + } + $related[$one] = $newrelated; + } else { + $related[$one] = (object) $data[$one]; + } + unset($data[$one]); + } else if (array_key_exists($prefixed, $data) && !array_key_exists($one, $related)) { + if ($isarray) { + $newrelated = []; + foreach ($data[$prefixed] as $item) { + $newrelated[] = (object) $item; + } + $related[$one] = $newrelated; + } else { + $related[$one] = (object) $data[$prefixed]; + } + unset($data[$prefixed]); + } else if (!array_key_exists($one, $related)) { + $related[$one] = null; + } + } + parent::__construct($data, $related); + } + + /** + * List properties passed in $data that should be moved to $related in the constructor. + * + * @return array A list of properties to move from $data to $related. + */ + public static function pick_related() { + return ['alignments[]', 'criteria']; + } + + /** + * Map data from a request response to the internal structure. + * + * @param stdClass $data The remote data. + * @param string $apiversion The backpack version used to communicate remotely. + * @return stdClass + */ + public static function map_external_data($data, $apiversion) { + $mapped = new \stdClass(); + if (isset($data->entityType)) { + $mapped->type = $data->entityType; + } else { + $mapped->type = $data->type; + } + if (isset($data->entityId)) { + $mapped->id = $data->entityId; + } else { + $mapped->id = $data->id; + } + $mapped->name = $data->name; + $mapped->image = $data->image; + $mapped->issuer = $data->issuer; + $mapped->description = $data->description; + if (isset($data->openBadgeId)) { + $mapped->hostedUrl = $data->openBadgeId; + } + + return $mapped; + } + + /** + * Return the list of properties. + * + * @return array + */ + protected static function define_properties() { + return [ + 'type' => [ + 'type' => PARAM_ALPHA, + 'description' => 'BadgeClass', + ], + 'id' => [ + 'type' => PARAM_RAW, + 'description' => 'Unique identifier for this badgeclass', + ], + 'issuer' => [ + 'type' => PARAM_RAW, + 'description' => 'Unique identifier for this badgeclass', + 'optional' => true, + ], + 'name' => [ + 'type' => PARAM_TEXT, + 'description' => 'Name of the badgeclass', + ], + 'image' => [ + 'type' => PARAM_URL, + 'description' => 'URL to the image.', + ], + 'description' => [ + 'type' => PARAM_TEXT, + 'description' => 'Description of the badge class.', + ], + 'hostedUrl' => [ + 'type' => PARAM_RAW, + 'description' => 'Identifier of the open badge for this assertion', + 'optional' => true, + ], + ]; + } + + /** + * Returns a list of objects that are related. + * + * @return array + */ + protected static function define_related() { + return array( + 'context' => 'context', + 'alignments' => 'stdClass[]?', + 'criteria' => 'stdClass?', + ); + } + + /** + * Return the list of additional properties. + * + * @return array + */ + protected static function define_other_properties() { + return array( + 'alignments' => array( + 'type' => alignment_exporter::read_properties_definition(), + 'optional' => true, + 'multiple' => true + ), + 'criteriaUrl' => array( + 'type' => PARAM_URL, + 'optional' => true + ), + 'criteriaNarrative' => array( + 'type' => PARAM_TEXT, + 'optional' => true + ) + ); + } + + /** + * We map from related data passed as data to this exporter to clean exportable values. + */ + protected function get_other_values(renderer_base $output) { + global $DB; + $result = []; + + if (array_key_exists('alignments', $this->related) && $this->related['alignments'] !== null) { + $alignments = []; + foreach ($this->related['alignments'] as $alignment) { + $exporter = new alignment_exporter($alignment, $this->related); + $alignments[] = $exporter->export($output); + } + $result['alignments'] = $alignments; + } + if (array_key_exists('criteria', $this->related) && $this->related['criteria'] !== null) { + if (property_exists($this->related['criteria'], 'id') && $this->related['criteria']->id !== null) { + $result['criteriaUrl'] = $this->related['criteria']->id; + } + if (property_exists($this->related['criteria'], 'narrative') && $this->related['criteria']->narrative !== null) { + $result['criteriaNarrative'] = $this->related['criteria']->narrative; + } + } + + return $result; + } +} diff --git a/badges/classes/external/collection_exporter.php b/badges/classes/external/collection_exporter.php new file mode 100644 index 00000000000..ac79f98ee4c --- /dev/null +++ b/badges/classes/external/collection_exporter.php @@ -0,0 +1,110 @@ +. + +/** + * Contains class for displaying a collection. + * + * @package core_badges + * @copyright 2019 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_badges\external; + +defined('MOODLE_INTERNAL') || die(); + +use core\external\exporter; +use stdClass; + +/** + * Class for displaying a badge competency. + * + * @package core_badges + * @copyright 2019 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class collection_exporter extends exporter { + + /** + * Either map version 1 data to version 2 or return it untouched. + * + * @param stdClass $data The remote data. + * @param string $apiversion The backpack version used to communicate remotely. + * @return stdClass + */ + public static function map_external_data($data, $apiversion) { + if ($apiversion == OPEN_BADGES_V1) { + $result = new stdClass(); + $result->entityType = 'BackpackCollection'; + $result->entityId = $data->groupId; + $result->name = $data->name; + $result->description = $data->description; + $result->assertions = []; + return $result; + } + return $data; + } + + /** + * Return the list of properties. + * + * @return array + */ + protected static function define_properties() { + return [ + 'entityType' => [ + 'type' => PARAM_ALPHA, + 'description' => 'BackpackCollection', + ], + 'entityId' => [ + 'type' => PARAM_RAW, + 'description' => 'Unique identifier for this collection', + ], + 'name' => [ + 'type' => PARAM_TEXT, + 'description' => 'Collection name', + ], + 'description' => [ + 'type' => PARAM_TEXT, + 'description' => 'Collection description', + ], + 'share_url' => [ + 'type' => PARAM_URL, + 'description' => 'Url to view this collection', + ], + 'published' => [ + 'type' => PARAM_BOOL, + 'description' => 'True means this collection is public.', + ], + 'assertions' => [ + 'type' => PARAM_RAW, + 'description' => 'List of assertion ids in this collection', + 'multiple' => true, + ], + ]; + } + + /** + * Returns a list of objects that are related. + * + * @return array + */ + protected static function define_related() { + return array( + 'context' => 'context', + ); + } +} diff --git a/badges/classes/external/issuer_exporter.php b/badges/classes/external/issuer_exporter.php new file mode 100644 index 00000000000..d741031e58e --- /dev/null +++ b/badges/classes/external/issuer_exporter.php @@ -0,0 +1,109 @@ +. + +/** + * Contains class for displaying a issuer. + * + * @package core_badges + * @copyright 2019 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_badges\external; + +defined('MOODLE_INTERNAL') || die(); + +use core\external\exporter; + +/** + * Class for displaying a badge competency. + * + * @package core_badges + * @copyright 2019 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class issuer_exporter extends exporter { + + /** + * Either map version 1 data to version 2 or return it untouched. + * + * @param stdClass $data The remote data. + * @param string $apiversion The backpack version used to communicate remotely. + * @return stdClass + */ + public static function map_external_data($data, $apiversion) { + if ($apiversion == OPEN_BADGES_V1) { + $result = new \stdClass(); + return $result; + } + $mapped = new \stdClass(); + if (isset($data->entityType)) { + $mapped->type = $data->entityType; + } else { + $mapped->type = $data->type; + } + if (isset($data->entityId)) { + $mapped->id = $data->entityId; + } else { + $mapped->id = $data->id; + } + $mapped->name = $data->name; + $mapped->email = $data->email; + $mapped->url = $data->url; + return $mapped; + } + + /** + * Return the list of properties. + * + * @return array + */ + protected static function define_properties() { + return [ + 'type' => [ + 'type' => PARAM_ALPHA, + 'description' => 'Issuer', + ], + 'id' => [ + 'type' => PARAM_RAW, + 'description' => 'Unique identifier for this issuer', + ], + 'name' => [ + 'type' => PARAM_TEXT, + 'description' => 'Name of the issuer', + ], + 'email' => [ + 'type' => PARAM_EMAIL, + 'description' => 'Email of the issuer', + ], + 'url' => [ + 'type' => PARAM_URL, + 'description' => 'URL for this issuer', + ], + ]; + } + + /** + * Returns a list of objects that are related. + * + * @return array + */ + protected static function define_related() { + return array( + 'context' => 'context', + ); + } +} diff --git a/badges/classes/external/recipient_exporter.php b/badges/classes/external/recipient_exporter.php new file mode 100644 index 00000000000..79bbffa944a --- /dev/null +++ b/badges/classes/external/recipient_exporter.php @@ -0,0 +1,82 @@ +. + +/** + * Contains class for displaying a recipient. + * + * @package core_badges + * @copyright 2019 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_badges\external; + +defined('MOODLE_INTERNAL') || die(); + +use core\external\exporter; + +/** + * Class for displaying a badge competency. + * + * @package core_badges + * @copyright 2019 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class recipient_exporter extends exporter { + + /** + * Return the list of properties. + * + * @return array + */ + protected static function define_properties() { + return [ + 'identity' => [ + 'type' => PARAM_RAW, + 'description' => 'Hashed email address to issue badge to.', + ], + 'plaintextIdentity' => [ + 'type' => PARAM_RAW, + 'description' => 'Email address to issue badge to.', + 'optional' => true, + ], + 'salt' => [ + 'type' => PARAM_RAW, + 'description' => 'Salt used to hash email.', + 'optional' => true, + ], + 'type' => [ + 'type' => PARAM_ALPHA, + 'description' => 'Email', + ], + 'hashed' => [ + 'type' => PARAM_BOOL, + 'description' => 'Should be true', + ], + ]; + } + + /** + * Returns a list of objects that are related. + * + * @return array + */ + protected static function define_related() { + return array( + 'context' => 'context', + ); + } +} diff --git a/badges/classes/external/verification_exporter.php b/badges/classes/external/verification_exporter.php new file mode 100644 index 00000000000..2a2868e747c --- /dev/null +++ b/badges/classes/external/verification_exporter.php @@ -0,0 +1,53 @@ +. + +/** + * Contains class for displaying a recipient. + * + * @package core_badges + * @copyright 2019 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_badges\external; + +defined('MOODLE_INTERNAL') || die(); + +use core\external\exporter; + +/** + * Class for displaying a badge competency. + * + * @package core_badges + * @copyright 2019 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class verification_exporter extends exporter { + + /** + * Return the list of properties. + * + * @return array + */ + protected static function define_properties() { + return [ + 'type' => [ + 'type' => PARAM_ALPHA, + 'description' => 'Type of verification.', + ] + ]; + } +} diff --git a/badges/backpack_form.php b/badges/classes/form/backpack.php similarity index 60% rename from badges/backpack_form.php rename to badges/classes/form/backpack.php index 391b556a611..447734fd677 100644 --- a/badges/backpack_form.php +++ b/badges/classes/form/backpack.php @@ -24,36 +24,43 @@ * @author Yuliya Bozhko */ +namespace core_badges\form; + defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/formslib.php'); require_once($CFG->libdir . '/badgeslib.php'); +use html_writer; +use moodleform; +use stdClass; + /** * Form to edit backpack initial details. * */ -class edit_backpack_form extends moodleform { +class backpack extends moodleform { /** * Defines the form */ public function definition() { - global $USER, $PAGE, $OUTPUT; + global $USER, $PAGE, $OUTPUT, $CFG; $mform = $this->_form; $mform->addElement('html', html_writer::tag('span', '', array('class' => 'notconnected', 'id' => 'connection-error'))); $mform->addElement('header', 'backpackheader', get_string('backpackconnection', 'badges')); $mform->addHelpButton('backpackheader', 'backpackconnection', 'badges'); - $mform->addElement('static', 'url', get_string('url'), BADGE_BACKPACKURL); - $mform->addElement('hidden', 'userid', $USER->id); $mform->setType('userid', PARAM_INT); + $sitebackpack = badges_get_site_backpack($CFG->badges_site_backpack); if (isset($this->_customdata['email'])) { // Email will be passed in when we're in the process of verifying the user's email address, // so set the connection status, lock the email field, and provide options to resend the verification // email or cancel the verification process entirely and start over. + $mform->addElement('hidden', 'backpackid', $sitebackpack->id); + $mform->setType('backpackid', PARAM_INT); $status = html_writer::tag('span', get_string('backpackemailverificationpending', 'badges'), array('class' => 'notconnected', 'id' => 'connection-status')); $mform->addElement('static', 'status', get_string('status'), $status); @@ -62,6 +69,8 @@ class edit_backpack_form extends moodleform { $mform->hardFreeze(['email']); $emailverify = html_writer::tag('span', s($this->_customdata['email']), []); $mform->addElement('static', 'emailverify', get_string('email'), $emailverify); + $mform->addElement('hidden', 'backpackpassword', $this->_customdata['backpackpassword']); + $mform->setType('backpackpassword', PARAM_RAW); $buttonarray = []; $buttonarray[] = &$mform->createElement('submit', 'submitbutton', get_string('backpackconnectionresendemail', 'badges')); @@ -71,6 +80,11 @@ class edit_backpack_form extends moodleform { $mform->closeHeaderBefore('buttonar'); } else { // Email isn't present, so provide an input element to get it and a button to start the verification process. + + $mform->addElement('static', 'info', '', $sitebackpack->backpackweburl); + $mform->addElement('hidden', 'backpackid', $sitebackpack->id); + $mform->setType('backpackid', PARAM_INT); + $status = html_writer::tag('span', get_string('notconnected', 'badges'), array('class' => 'notconnected', 'id' => 'connection-status')); $mform->addElement('static', 'status', get_string('status'), $status); @@ -78,6 +92,8 @@ class edit_backpack_form extends moodleform { $mform->addHelpButton('email', 'backpackemail', 'badges'); $mform->addRule('email', get_string('required'), 'required', null, 'client'); $mform->setType('email', PARAM_EMAIL); + $mform->addElement('passwordunmask', 'backpackpassword', get_string('password')); + $mform->setType('backpackpassword', PARAM_RAW); $this->add_action_buttons(false, get_string('backpackconnectionconnect', 'badges')); } } @@ -87,79 +103,21 @@ class edit_backpack_form extends moodleform { */ public function validation($data, $files) { $errors = parent::validation($data, $files); - // We don't need to verify the email address if we're clearing a pending email verification attempt. if (!isset($data['revertbutton'])) { $check = new stdClass(); - $check->backpackurl = BADGE_BACKPACKURL; + $backpack = badges_get_site_backpack($data['backpackid'], false); + $check->backpackurl = $backpack->backpackapiurl; + $check->apiversion = $backpack->apiversion; $check->email = $data['email']; + $check->password = $data['backpackpassword']; - $bp = new OpenBadgesBackpackHandler($check); - $request = $bp->curl_request('user'); - if (isset($request->status) && $request->status == 'missing') { - $errors['email'] = get_string('error:nosuchuser', 'badges'); - } else if (!isset($request->status) || $request->status !== 'okay') { + $bp = new \core_badges\backpack_api($backpack, $check); + $result = $bp->authenticate(); + if ($result === false || !empty($result->error)) { $errors['email'] = get_string('backpackconnectionunexpectedresult', 'badges'); } } return $errors; } } - -/** - * Form to select backpack collections. - * - */ -class edit_collections_form extends moodleform { - - /** - * Defines the form - */ - public function definition() { - global $USER; - $mform = $this->_form; - $email = $this->_customdata['email']; - $bid = $this->_customdata['backpackid']; - $selected = $this->_customdata['selected']; - - if (isset($this->_customdata['groups'])) { - $groups = $this->_customdata['groups']; - $nogroups = null; - } else { - $groups = null; - $nogroups = $this->_customdata['nogroups']; - } - - $mform->addElement('header', 'backpackheader', get_string('backpackconnection', 'badges')); - $mform->addHelpButton('backpackheader', 'backpackconnection', 'badges'); - $mform->addElement('static', 'url', get_string('url'), BADGE_BACKPACKURL); - - $status = html_writer::tag('span', get_string('connected', 'badges'), array('class' => 'connected')); - $mform->addElement('static', 'status', get_string('status'), $status); - $mform->addElement('static', 'email', get_string('email'), $email); - $mform->addHelpButton('email', 'backpackemail', 'badges'); - $mform->addElement('submit', 'disconnect', get_string('disconnect', 'badges')); - - $mform->addElement('header', 'collectionheader', get_string('backpackimport', 'badges')); - $mform->addHelpButton('collectionheader', 'backpackimport', 'badges'); - - if (!empty($groups)) { - $mform->addElement('static', 'selectgroup', '', get_string('selectgroup_start', 'badges')); - foreach ($groups as $group) { - $name = $group->name . ' (' . $group->badges . ')'; - $mform->addElement('advcheckbox', 'group[' . $group->groupId . ']', null, $name, array('group' => 1), array(false, $group->groupId)); - if (in_array($group->groupId, $selected)) { - $mform->setDefault('group[' . $group->groupId . ']', $group->groupId); - } - } - $mform->addElement('static', 'selectgroup', '', get_string('selectgroup_end', 'badges')); - } else { - $mform->addElement('static', 'selectgroup', '', $nogroups); - } - - $mform->addElement('hidden', 'backpackid', $bid); - $mform->setType('backpackid', PARAM_INT); - - $this->add_action_buttons(); - } -} diff --git a/badges/edit_form.php b/badges/classes/form/badge.php similarity index 71% rename from badges/edit_form.php rename to badges/classes/form/badge.php index 434398529a9..f6d5e09d501 100644 --- a/badges/edit_form.php +++ b/badges/classes/form/badge.php @@ -24,17 +24,21 @@ * @author Yuliya Bozhko */ +namespace core_badges\form; + defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/formslib.php'); require_once($CFG->libdir . '/badgeslib.php'); require_once($CFG->libdir . '/filelib.php'); +use moodleform; + /** * Form to edit badge details. * */ -class edit_details_form extends moodleform { +class badge extends moodleform { /** * Defines the form @@ -90,20 +94,42 @@ class edit_details_form extends moodleform { $mform->addElement('header', 'issuerdetails', get_string('issuerdetails', 'badges')); - $mform->addElement('text', 'issuername', get_string('name'), array('size' => '70')); - $mform->setType('issuername', PARAM_NOTAGS); - $mform->addRule('issuername', null, 'required'); - if (isset($CFG->badges_defaultissuername)) { - $mform->setDefault('issuername', $CFG->badges_defaultissuername); - } - $mform->addHelpButton('issuername', 'issuername', 'badges'); + if (badges_open_badges_backpack_api() != OPEN_BADGES_V2) { + $mform->addElement('text', 'issuername', get_string('name'), array('size' => '70')); + $mform->setType('issuername', PARAM_NOTAGS); + $mform->addRule('issuername', null, 'required'); + if (isset($CFG->badges_defaultissuername)) { + $mform->setDefault('issuername', $CFG->badges_defaultissuername); + } + $mform->addHelpButton('issuername', 'issuername', 'badges'); - $mform->addElement('text', 'issuercontact', get_string('contact', 'badges'), array('size' => '70')); - if (isset($CFG->badges_defaultissuercontact)) { - $mform->setDefault('issuercontact', $CFG->badges_defaultissuercontact); + $mform->addElement('text', 'issuercontact', get_string('contact', 'badges'), array('size' => '70')); + if (isset($CFG->badges_defaultissuercontact)) { + $mform->setDefault('issuercontact', $CFG->badges_defaultissuercontact); + } + $mform->setType('issuercontact', PARAM_RAW); + $mform->addHelpButton('issuercontact', 'contact', 'badges'); + // Set issuer URL. + // Have to parse URL because badge issuer origin cannot be a subfolder in wwwroot. + $url = parse_url($CFG->wwwroot); + $mform->addElement('hidden', 'issuerurl', $url['scheme'] . '://' . $url['host']); + $mform->setType('issuerurl', PARAM_URL); + + } else { + $name = $CFG->badges_defaultissuername; + $mform->addElement('static', 'issuernamelabel', get_string('name'), $name); + $mform->addElement('hidden', 'issuername', $name); + $mform->setType('issuername', PARAM_NOTAGS); + + $contact = $CFG->badges_defaultissuercontact; + $mform->addElement('static', 'issuercontactlabel', get_string('contact', 'badges'), $contact); + $mform->addElement('hidden', 'issuercontact', $contact); + $mform->setType('issuercontact', PARAM_RAW); + + $url = parse_url($CFG->wwwroot); + $mform->addElement('hidden', 'issuerurl', $url['scheme'] . '://' . $url['host']); + $mform->setType('issuerurl', PARAM_URL); } - $mform->setType('issuercontact', PARAM_RAW); - $mform->addHelpButton('issuercontact', 'contact', 'badges'); $mform->addElement('header', 'issuancedetails', get_string('issuancedetails', 'badges')); @@ -127,12 +153,6 @@ class edit_details_form extends moodleform { $mform->disabledIf('expireperiod[number]', 'expiry', 'neq', 2); $mform->disabledIf('expireperiod[timeunit]', 'expiry', 'neq', 2); - // Set issuer URL. - // Have to parse URL because badge issuer origin cannot be a subfolder in wwwroot. - $url = parse_url($CFG->wwwroot); - $mform->addElement('hidden', 'issuerurl', $url['scheme'] . '://' . $url['host']); - $mform->setType('issuerurl', PARAM_URL); - $mform->addElement('hidden', 'action', $action); $mform->setType('action', PARAM_TEXT); @@ -182,8 +202,10 @@ class edit_details_form extends moodleform { global $DB; $errors = parent::validation($data, $files); - if (!empty($data['issuercontact']) && !validate_email($data['issuercontact'])) { - $errors['issuercontact'] = get_string('invalidemail'); + if (badges_open_badges_backpack_api() != OPEN_BADGES_V2) { + if (!empty($data['issuercontact']) && !validate_email($data['issuercontact'])) { + $errors['issuercontact'] = get_string('invalidemail'); + } } if ($data['expiry'] == 2 && $data['expireperiod'] <= 0) { @@ -219,61 +241,3 @@ class edit_details_form extends moodleform { } } -/** - * Form to edit badge message. - * - */ -class edit_message_form extends moodleform { - public function definition() { - global $CFG, $OUTPUT; - - $mform = $this->_form; - $badge = $this->_customdata['badge']; - $action = $this->_customdata['action']; - $editoroptions = $this->_customdata['editoroptions']; - - // Add hidden fields. - $mform->addElement('hidden', 'id', $badge->id); - $mform->setType('id', PARAM_INT); - - $mform->addElement('hidden', 'action', $action); - $mform->setType('action', PARAM_TEXT); - - $mform->addElement('header', 'badgemessage', get_string('configuremessage', 'badges')); - $mform->addHelpButton('badgemessage', 'variablesubstitution', 'badges'); - - $mform->addElement('text', 'messagesubject', get_string('subject', 'badges'), array('size' => '70')); - $mform->setType('messagesubject', PARAM_TEXT); - $mform->addRule('messagesubject', null, 'required'); - $mform->addRule('messagesubject', get_string('maximumchars', '', 255), 'maxlength', 255); - - $mform->addElement('editor', 'message_editor', get_string('message', 'badges'), null, $editoroptions); - $mform->setType('message_editor', PARAM_RAW); - $mform->addRule('message_editor', null, 'required'); - - $mform->addElement('advcheckbox', 'attachment', get_string('attachment', 'badges'), '', null, array(0, 1)); - $mform->addHelpButton('attachment', 'attachment', 'badges'); - if (empty($CFG->allowattachments)) { - $mform->freeze('attachment'); - } - - $options = array( - BADGE_MESSAGE_NEVER => get_string('never'), - BADGE_MESSAGE_ALWAYS => get_string('notifyevery', 'badges'), - BADGE_MESSAGE_DAILY => get_string('notifydaily', 'badges'), - BADGE_MESSAGE_WEEKLY => get_string('notifyweekly', 'badges'), - BADGE_MESSAGE_MONTHLY => get_string('notifymonthly', 'badges'), - ); - $mform->addElement('select', 'notification', get_string('notification', 'badges'), $options); - $mform->addHelpButton('notification', 'notification', 'badges'); - - $this->add_action_buttons(); - $this->set_data($badge); - } - - public function validation($data, $files) { - $errors = parent::validation($data, $files); - - return $errors; - } -} diff --git a/badges/classes/form/collections.php b/badges/classes/form/collections.php new file mode 100644 index 00000000000..b854734bde9 --- /dev/null +++ b/badges/classes/form/collections.php @@ -0,0 +1,109 @@ +. + +/** + * Form class for mybackpack.php + * + * @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\form; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/formslib.php'); +require_once($CFG->libdir . '/badgeslib.php'); + +use html_writer; +use moodleform; + +/** + * Form to select backpack collections. + * + */ +class collections extends moodleform { + + /** + * Defines the form + */ + public function definition() { + global $USER; + $mform = $this->_form; + $email = $this->_customdata['email']; + $backpackweburl = $this->_customdata['backpackweburl']; + $selected = $this->_customdata['selected']; + + if (isset($this->_customdata['groups'])) { + $groups = $this->_customdata['groups']; + $nogroups = null; + } else { + $groups = null; + $nogroups = $this->_customdata['nogroups']; + } + + $backpack = get_backpack_settings($USER->id); + $mform->addElement('header', 'backpackheader', get_string('backpackconnection', 'badges')); + $mform->addHelpButton('backpackheader', 'backpackconnection', 'badges'); + $mform->addElement('static', 'url', get_string('url'), $backpackweburl); + + $status = html_writer::tag('span', get_string('connected', 'badges'), array('class' => 'connected')); + $mform->addElement('static', 'status', get_string('status'), $status); + $mform->addElement('static', 'email', get_string('email'), $email); + $mform->addHelpButton('email', 'backpackemail', 'badges'); + $mform->addElement('submit', 'disconnect', get_string('disconnect', 'badges')); + + $mform->addElement('header', 'collectionheader', get_string('backpackimport', 'badges')); + $mform->addHelpButton('collectionheader', 'backpackimport', 'badges'); + + if (!empty($groups)) { + $mform->addElement('static', 'selectgroup', '', get_string('selectgroup_start', 'badges')); + foreach ($groups as $group) { + // Assertions or badges. + $count = 0; + if (!empty($group->assertions)) { + $count = count($group->assertions); + } + if (!empty($group->badges)) { + $count = count($group->badges); + } + if (!empty($group->groupId)) { + $group->entityId = $group->groupId; + } + $name = $group->name . ' (' . $count . ')'; + $mform->addElement( + 'advcheckbox', + 'group[' . $group->entityId . ']', + null, + $name, + array('group' => 1), + array(false, $group->entityId) + ); + if (in_array($group->entityId, $selected)) { + $mform->setDefault('group[' . $group->entityId . ']', $group->entityId); + } + } + $mform->addElement('static', 'selectgroup', '', get_string('selectgroup_end', 'badges', $backpackweburl)); + } else { + $mform->addElement('static', 'selectgroup', '', $nogroups); + } + + $this->add_action_buttons(); + } +} diff --git a/badges/classes/form/external_backpack.php b/badges/classes/form/external_backpack.php new file mode 100644 index 00000000000..7193df59033 --- /dev/null +++ b/badges/classes/form/external_backpack.php @@ -0,0 +1,94 @@ +. + +/** + * External backpack form + * + * @package core_badges + * @copyright 2019 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_badges\form; +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir.'/formslib.php'); + +/** + * Backpack form class. + * + * @package core_badges + * @copyright 2019 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class external_backpack extends \moodleform { + + public function definition() { + global $CFG; + + $mform = $this->_form; + $backpack = false; + + if (isset($this->_customdata['externalbackpack'])) { + $backpack = $this->_customdata['externalbackpack']; + } else { + throw new \coding_exception('backpack is required.'); + } + + $url = $backpack->backpackapiurl; + + $mform->addElement('static', 'backpackapiurlinfo', get_string('backpackapiurl', 'core_badges'), $url); + + $mform->addElement('hidden', 'backpackapiurl', $url); + $mform->setType('backpackapiurl', PARAM_URL); + + $url = $backpack->backpackweburl; + $mform->addElement('static', 'backpackweburlinfo', get_string('backpackweburl', 'core_badges'), $url); + $mform->addElement('hidden', 'backpackweburl', $url); + $mform->setType('backpackweburl', PARAM_URL); + + $options = badges_get_badge_api_versions(); + $label = $options[$backpack->apiversion]; + $mform->addElement('static', 'apiversioninfo', get_string('apiversion', 'core_badges'), $label); + $mform->addElement('hidden', 'apiversion', $backpack->apiversion); + $mform->setType('apiversion', PARAM_INTEGER); + + $mform->addElement('hidden', 'id', $backpack->id); + $mform->setType('id', PARAM_INTEGER); + + $mform->addElement('hidden', 'action', 'edit'); + $mform->setType('action', PARAM_ALPHA); + + $issuername = $CFG->badges_defaultissuername; + $mform->addElement('static', 'issuerinfo', get_string('defaultissuername', 'core_badges'), $issuername); + + $issuercontact = $CFG->badges_defaultissuercontact; + $mform->addElement('static', 'issuerinfo', get_string('defaultissuercontact', 'core_badges'), $issuercontact); + + $mform->addElement('passwordunmask', 'password', get_string('defaultissuerpassword', 'core_badges')); + $mform->setType('password', PARAM_RAW); + $mform->addHelpButton('password', 'defaultissuerpassword', 'badges'); + $mform->hideIf('password', 'apiversion', 'eq', 1); + + $this->set_data($backpack); + + // Disable short forms. + $mform->setDisableShortforms(); + + $this->add_action_buttons(); + } + +} diff --git a/badges/classes/form/message.php b/badges/classes/form/message.php new file mode 100644 index 00000000000..c50c01174d6 --- /dev/null +++ b/badges/classes/form/message.php @@ -0,0 +1,93 @@ +. + +/** + * Form class for badge message. + * + * @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\form; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/formslib.php'); +require_once($CFG->libdir . '/badgeslib.php'); +require_once($CFG->libdir . '/filelib.php'); + +use moodleform; + +/** + * Form to edit badge message. + */ +class message extends moodleform { + public function definition() { + global $CFG, $OUTPUT; + + $mform = $this->_form; + $badge = $this->_customdata['badge']; + $action = $this->_customdata['action']; + $editoroptions = $this->_customdata['editoroptions']; + + // Add hidden fields. + $mform->addElement('hidden', 'id', $badge->id); + $mform->setType('id', PARAM_INT); + + $mform->addElement('hidden', 'action', $action); + $mform->setType('action', PARAM_TEXT); + + $mform->addElement('header', 'badgemessage', get_string('configuremessage', 'badges')); + $mform->addHelpButton('badgemessage', 'variablesubstitution', 'badges'); + + $mform->addElement('text', 'messagesubject', get_string('subject', 'badges'), array('size' => '70')); + $mform->setType('messagesubject', PARAM_TEXT); + $mform->addRule('messagesubject', null, 'required'); + $mform->addRule('messagesubject', get_string('maximumchars', '', 255), 'maxlength', 255); + + $mform->addElement('editor', 'message_editor', get_string('message', 'badges'), null, $editoroptions); + $mform->setType('message_editor', PARAM_RAW); + $mform->addRule('message_editor', null, 'required'); + + $mform->addElement('advcheckbox', 'attachment', get_string('attachment', 'badges'), '', null, array(0, 1)); + $mform->addHelpButton('attachment', 'attachment', 'badges'); + if (empty($CFG->allowattachments)) { + $mform->freeze('attachment'); + } + + $options = array( + BADGE_MESSAGE_NEVER => get_string('never'), + BADGE_MESSAGE_ALWAYS => get_string('notifyevery', 'badges'), + BADGE_MESSAGE_DAILY => get_string('notifydaily', 'badges'), + BADGE_MESSAGE_WEEKLY => get_string('notifyweekly', 'badges'), + BADGE_MESSAGE_MONTHLY => get_string('notifymonthly', 'badges'), + ); + $mform->addElement('select', 'notification', get_string('notification', 'badges'), $options); + $mform->addHelpButton('notification', 'notification', 'badges'); + + $this->add_action_buttons(); + $this->set_data($badge); + } + + public function validation($data, $files) { + $errors = parent::validation($data, $files); + + return $errors; + } +} diff --git a/badges/classes/observer.php b/badges/classes/observer.php index 9452e912d2a..91cc51e913b 100644 --- a/badges/classes/observer.php +++ b/badges/classes/observer.php @@ -24,6 +24,7 @@ defined('MOODLE_INTERNAL') || die(); +use \core_badges\badge; /** * Event observer for badges. */ @@ -87,6 +88,9 @@ class core_badges_observer { $ucid = $event->other['usercompetencyid']; $cid = $event->other['competencyid']; $userid = $event->relateduserid; + if (!$eventdata) { + return; + } if ($rs = $DB->get_records('badge_criteria_param', array('name' => 'competency_' . $cid, 'value' => $cid))) { foreach ($rs as $r) { diff --git a/badges/classes/output/badge_alignments.php b/badges/classes/output/badge_alignments.php new file mode 100644 index 00000000000..4245b592c86 --- /dev/null +++ b/badges/classes/output/badge_alignments.php @@ -0,0 +1,74 @@ +. + +/** + * Issued badge renderable. + * + * @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\output; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/badgeslib.php'); + +use renderable; + +/** + * + * @copyright 2018 Tung Thai + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Tung Thai + */ +class badge_alignments implements renderable +{ + /** @var string how are the data sorted. */ + public $sort = 'name'; + + /** @var string how are the data sorted. */ + public $dir = 'ASC'; + + /** @var int page number to display. */ + public $page = 0; + + /** @var int number of badges to display per page. */ + public $perpage = BADGE_PERPAGE; + + /** @var int the total number of badges to display. */ + public $totalcount = null; + + /** @var array list of badges. */ + public $alignments = array(); + + /** @var array list of badges. */ + public $currentbadgeid = 0; + + /** + * Initializes the list of alignments to display. + * + * @param array $alignments List alignments to render. + * @param int $currentbadgeid ID current badge. + */ + public function __construct($alignments, $currentbadgeid) { + $this->alignments = $alignments; + $this->currentbadgeid = $currentbadgeid; + } +} diff --git a/badges/classes/output/badge_collection.php b/badges/classes/output/badge_collection.php new file mode 100644 index 00000000000..0662be3af26 --- /dev/null +++ b/badges/classes/output/badge_collection.php @@ -0,0 +1,67 @@ +. + +/** + * Issued badge renderable. + * + * @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\output; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/badgeslib.php'); + +use renderable; + +/** + * Collection of all badges for view.php page + */ +class badge_collection implements renderable { + + /** @var string how are the data sorted */ + public $sort = 'name'; + + /** @var string how are the data sorted */ + public $dir = 'ASC'; + + /** @var int page number to display */ + public $page = 0; + + /** @var int number of badges to display per page */ + public $perpage = BADGE_PERPAGE; + + /** @var int the total number of badges to display */ + public $totalcount = null; + + /** @var array list of badges */ + public $badges = array(); + + /** + * Initializes the list of badges to display + * + * @param array $badges Badges to render + */ + public function __construct($badges) { + $this->badges = $badges; + } +} + diff --git a/badges/classes/output/badge_management.php b/badges/classes/output/badge_management.php new file mode 100644 index 00000000000..89aa08ab4b5 --- /dev/null +++ b/badges/classes/output/badge_management.php @@ -0,0 +1,40 @@ +. + +/** + * Issued badge renderable. + * + * @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\output; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/badgeslib.php'); + +use renderable; + +/** + * Collection of badges used at the index.php page + */ +class badge_management extends badge_collection implements renderable { +} + diff --git a/badges/classes/output/badge_recipients.php b/badges/classes/output/badge_recipients.php new file mode 100644 index 00000000000..68ea23d7ec4 --- /dev/null +++ b/badges/classes/output/badge_recipients.php @@ -0,0 +1,67 @@ +. + +/** + * Issued badge renderable. + * + * @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\output; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/badgeslib.php'); + +use renderable; + +/** + * Badge recipients rendering class + */ +class badge_recipients implements renderable { + + /** @var string how are the data sorted */ + public $sort = 'lastname'; + + /** @var string how are the data sorted */ + public $dir = 'ASC'; + + /** @var int page number to display */ + public $page = 0; + + /** @var int number of badge recipients to display per page */ + public $perpage = 30; + + /** @var int the total number or badge recipients to display */ + public $totalcount = null; + + /** @var array internal list of badge recipients ids */ + public $userids = array(); + + /** + * Initializes the list of users to display + * + * @param array $holders List of badge holders + */ + public function __construct($holders) { + $this->userids = $holders; + } +} + diff --git a/badges/classes/output/badge_related.php b/badges/classes/output/badge_related.php new file mode 100644 index 00000000000..c4167049980 --- /dev/null +++ b/badges/classes/output/badge_related.php @@ -0,0 +1,76 @@ +. + +/** + * Collection of all related badges. + * + * @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\output; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/badgeslib.php'); + +use renderable; + +/** + * Collection of all related badges. + * + * @copyright 2018 Tung Thai + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Tung Thai + */ +class badge_related implements renderable { + + /** @var string how are the data sorted. */ + public $sort = 'name'; + + /** @var string how are the data sorted. */ + public $dir = 'ASC'; + + /** @var int page number to display. */ + public $page = 0; + + /** @var int number of badges to display per page. */ + public $perpage = BADGE_PERPAGE; + + /** @var int the total number of badges to display. */ + public $totalcount = null; + + /** @var int the current badge. */ + public $currentbadgeid = 0; + + /** @var array list of badges. */ + public $badges = array(); + + /** + * Initializes the list of badges to display. + * + * @param array $badges related badges to render. + * @param int $currentbadgeid ID current badge. + */ + public function __construct($badges, $currentbadgeid) { + $this->badges = $badges; + $this->currentbadgeid = $currentbadgeid; + } +} + diff --git a/badges/classes/output/badge_user_collection.php b/badges/classes/output/badge_user_collection.php new file mode 100644 index 00000000000..ead1ed46669 --- /dev/null +++ b/badges/classes/output/badge_user_collection.php @@ -0,0 +1,60 @@ +. + +/** + * Collection of use badges. + * + * @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\output; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/badgeslib.php'); + +use renderable; + +/** + * Collection of user badges used at the mybadges.php page + */ +class badge_user_collection extends badge_collection implements renderable { + /** @var array backpack settings */ + public $backpack = null; + + /** @var string search */ + public $search = ''; + + /** + * Initializes user badge collection. + * + * @param array $badges Badges to render + * @param int $userid Badges owner + */ + public function __construct($badges, $userid) { + global $CFG; + parent::__construct($badges); + + if (!empty($CFG->badges_allowexternalbackpack)) { + $this->backpack = get_backpack_settings($userid, true); + } + } +} + diff --git a/badges/classes/output/external_backpacks_page.php b/badges/classes/output/external_backpacks_page.php new file mode 100644 index 00000000000..90086e4218e --- /dev/null +++ b/badges/classes/output/external_backpacks_page.php @@ -0,0 +1,76 @@ +. + +/** + * Manage enabled backpacks for the site. + * + * @package core_badges + * @copyright 2019 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_badges\output; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/badgeslib.php'); + +use core_badges\external\backpack_exporter; + +/** + * Manage enabled backpacks renderable. + * + * @package core_badges + * @copyright 2019 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class external_backpacks_page implements \renderable { + + /** + * Constructor. + * @param \moodle_url $url + */ + public function __construct(\moodle_url $url) { + $this->url = $url; + + $this->backpacks = badges_get_site_backpacks(); + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output Renderer base. + * @return stdClass + */ + public function export_for_template(\renderer_base $output) { + $data = new \stdClass(); + $data->baseurl = $this->url; + $data->backpacks = array(); + $data->sesskey = sesskey(); + foreach ($this->backpacks as $backpack) { + $exporter = new backpack_exporter($backpack); + $backpack = $exporter->export($output); + if ($backpack->apiversion == OPEN_BADGES_V2) { + $backpack->canedit = true; + } else { + $backpack->canedit = false; + } + $data->backpacks[] = $backpack; + } + + return $data; + } +} diff --git a/badges/classes/output/external_backpacks_table.php b/badges/classes/output/external_backpacks_table.php new file mode 100644 index 00000000000..b442662aa06 --- /dev/null +++ b/badges/classes/output/external_backpacks_table.php @@ -0,0 +1,87 @@ +. + +/** + * List of enabled backpacks for the site. + * + * @package core_badges + * @copyright 2019 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_badges\output; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/tablelib.php'); +require_once($CFG->libdir . '/badgeslib.php'); + +use html_writer; +use moodle_url; +use table_sql; + +/** + * Backpacks table class. + * + * @package core_badges + * @copyright 2019 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class external_backpacks_table extends table_sql { + + /** + * Sets up the table. + * + * @param string $uniqueid Unique id of table. + * @param \core_competency\template $template The template. + */ + public function __construct() { + parent::__construct('backpacks'); + + $this->context = \context_system::instance(); + // This object should not be used without the right permissions. + require_capability('moodle/badges:manageglobalsettings', $this->context); + + // Define columns in the table. + $this->define_table_columns(); + + // Define configs. + $this->define_table_configs(); + } + + /** + * Setup the headers for the table. + */ + protected function define_table_columns() { + $cols = [ + 'backpackweburl' => get_string('backpackurl', 'core_badges'), + 'sortorder' => '', + ]; + + $this->define_columns(array_keys($cols)); + $this->define_headers(array_values($cols)); + } + + /** + * Define table configs. + */ + protected function define_table_configs() { + $this->collapsible(false); + $this->sortable(false); + $this->pageable(false); + } + +} diff --git a/badges/classes/output/external_badge.php b/badges/classes/output/external_badge.php new file mode 100644 index 00000000000..52ae6352891 --- /dev/null +++ b/badges/classes/output/external_badge.php @@ -0,0 +1,97 @@ +. + +/** + * External badge renderable. + * + * @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\output; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/badgeslib.php'); + +use renderable; + +/** + * An external badges for external.php page + */ +class external_badge implements renderable { + /** @var issued badge */ + public $issued; + + /** @var User ID */ + public $recipient; + + /** @var validation of external badge */ + public $valid = true; + + /** + * Initializes the badge to display + * + * @param object $badge External badge information. + * @param int $recipient User id. + */ + public function __construct($badge, $recipient) { + global $DB; + // At this point a user has connected a backpack. So, we are going to get + // their backpack email rather than their account email. + $namefields = get_all_user_name_fields(true, 'u'); + $user = $DB->get_record_sql("SELECT {$namefields}, b.email + FROM {user} u INNER JOIN {badge_backpack} b ON u.id = b.userid + WHERE b.userid = :userid", array('userid' => $recipient), IGNORE_MISSING); + + $this->issued = $badge; + $this->recipient = $user; + + // Check if recipient is valid. + // There is no way to be 100% sure that a badge belongs to a user. + // Backpack does not return any recipient information. + // All we can do is compare that backpack email hashed using salt + // provided in the assertion matches a badge recipient from the assertion. + if ($user) { + if (isset($badge->assertion->recipient->identity)) { + $badge->assertion->salt = $badge->assertion->recipient->salt; + $badge->assertion->recipient = $badge->assertion->recipient->identity; + } + // Open Badges V2 does not even include a recipient. + if (!isset($badge->assertion->recipient)) { + $this->valid = false; + } else if (validate_email($badge->assertion->recipient) && $badge->assertion->recipient == $user->email) { + // If we have email, compare emails. + $this->valid = true; + } else if ($badge->assertion->recipient == 'sha256$' . hash('sha256', $user->email)) { + // If recipient is hashed, but no salt, compare hashes without salt. + $this->valid = true; + } else if ($badge->assertion->recipient == 'sha256$' . hash('sha256', $user->email . $badge->assertion->salt)) { + // If recipient is hashed, compare hashes. + $this->valid = true; + } else { + // Otherwise, we cannot be sure that this user is a recipient. + $this->valid = false; + } + } else { + $this->valid = false; + } + } +} + diff --git a/badges/classes/output/issued_badge.php b/badges/classes/output/issued_badge.php new file mode 100644 index 00000000000..b2276f6524f --- /dev/null +++ b/badges/classes/output/issued_badge.php @@ -0,0 +1,81 @@ +. + +/** + * Issued badge renderable. + * + * @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\output; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/badgeslib.php'); + +use renderable; + +/** + * An issued badges for badge.php page + */ +class issued_badge implements renderable { + /** @var issued badge */ + public $issued; + + /** @var badge recipient */ + public $recipient; + + /** @var badge class */ + public $badgeclass; + + /** @var badge visibility to others */ + public $visible = 0; + + /** @var badge class */ + public $badgeid = 0; + + /** + * Initializes the badge to display + * + * @param string $hash Issued badge hash + */ + public function __construct($hash) { + global $DB; + + $assertion = new \core_badges_assertion($hash); + $this->issued = $assertion->get_badge_assertion(); + $this->badgeclass = $assertion->get_badge_class(); + + $rec = $DB->get_record_sql('SELECT userid, visible, badgeid + FROM {badge_issued} + WHERE ' . $DB->sql_compare_text('uniquehash', 40) . ' = ' . $DB->sql_compare_text(':hash', 40), + array('hash' => $hash), IGNORE_MISSING); + if ($rec) { + // Get a recipient from database. + $namefields = get_all_user_name_fields(true, 'u'); + $user = $DB->get_record_sql("SELECT u.id, $namefields, u.deleted, u.email + FROM {user} u WHERE u.id = :userid", array('userid' => $rec->userid)); + $this->recipient = $user; + $this->visible = $rec->visible; + $this->badgeid = $rec->badgeid; + } + } +} + diff --git a/badges/edit.php b/badges/edit.php index 2521dc1de79..2307d0d1e76 100644 --- a/badges/edit.php +++ b/badges/edit.php @@ -26,10 +26,10 @@ require_once(__DIR__ . '/../config.php'); require_once($CFG->libdir . '/badgeslib.php'); -require_once($CFG->dirroot . '/badges/edit_form.php'); +require_once($CFG->libdir . '/filelib.php'); $badgeid = required_param('id', PARAM_INT); -$action = optional_param('action', 'details', PARAM_TEXT); +$action = optional_param('action', 'badge', PARAM_TEXT); require_login(); @@ -84,13 +84,13 @@ $editoroptions = array( ); $badge = file_prepare_standard_editor($badge, 'message', $editoroptions, $context); -$formclass = 'edit_' . $action . '_form'; +$formclass = '\core_badges\form' . '\\' . $action; $form = new $formclass($currenturl, array('badge' => $badge, 'action' => $action, 'editoroptions' => $editoroptions)); if ($form->is_cancelled()) { redirect(new moodle_url('/badges/overview.php', array('id' => $badgeid))); } else if ($form->is_submitted() && $form->is_validated() && ($data = $form->get_data())) { - if ($action == 'details') { + if ($action == 'badge') { $badge->name = $data->name; $badge->version = trim($data->version); $badge->language = $data->language; @@ -157,4 +157,4 @@ $output->print_badge_tabs($badgeid, $context, $action); $form->display(); -echo $OUTPUT->footer(); \ No newline at end of file +echo $OUTPUT->footer(); diff --git a/badges/external.php b/badges/external.php index d05dbacd13d..39850a25cac 100644 --- a/badges/external.php +++ b/badges/external.php @@ -37,6 +37,7 @@ $hash = required_param('hash', PARAM_ALPHANUM); $userid = required_param('user', PARAM_INT); $PAGE->set_url(new moodle_url('/badges/external.php', array('hash' => $hash, 'user' => $userid))); +$PAGE->set_context(context_system::instance()); // Using the same setting as user profile page. if (!empty($CFG->forceloginforprofiles)) { @@ -75,15 +76,21 @@ if (empty($badge)) { print_error('error:externalbadgedoesntexist', 'badges'); } -$PAGE->set_context(context_system::instance()); $output = $PAGE->get_renderer('core', 'badges'); -$badge = new external_badge($badge, $userid); +$badge = new \core_badges\output\external_badge($badge, $userid); $PAGE->set_pagelayout('base'); $PAGE->set_title(get_string('issuedbadge', 'badges')); -$PAGE->set_heading(s($badge->issued->assertion->badge->name)); -$PAGE->navbar->add(s($badge->issued->assertion->badge->name)); +$badgename = ''; +if (!empty($badge->issued->name)) { + $badgename = s($badge->issued->name); +} +if (!empty($badge->issued->assertion->badge->name)) { + $badgename = s($badge->issued->assertion->badge->name); +} +$PAGE->set_heading($badgename); +$PAGE->navbar->add($badgename); if (isloggedin() && $USER->id == $userid) { $url = new moodle_url('/badges/mybadges.php'); } else { diff --git a/badges/index.php b/badges/index.php index e0b38bdf84a..27c560dc6c8 100644 --- a/badges/index.php +++ b/badges/index.php @@ -104,8 +104,7 @@ if (!has_any_capability(array( } $PAGE->set_title($hdr); -$PAGE->requires->js('/badges/backpack.js'); -$PAGE->requires->js_init_call('check_site_access', null, false); +badges_local_backpack_js(true); $output = $PAGE->get_renderer('core', 'badges'); if (($delete || $archive) && has_capability('moodle/badges:deletebadge', $PAGE->context)) { @@ -179,7 +178,7 @@ if ($totalcount) { echo $OUTPUT->notification(get_string($msg, 'badges'), 'notifysuccess'); } - $badges = new badge_management($records); + $badges = new \core_badges\output\badge_management($records); $badges->sort = $sortby; $badges->dir = $sorthow; $badges->page = $page; @@ -191,8 +190,8 @@ if ($totalcount) { echo $output->notification(get_string('nobadges', 'badges')); if (has_capability('moodle/badges:createbadge', $PAGE->context)) { - echo $OUTPUT->single_button(new moodle_url('newbadge.php', array('type' => $type, 'id' => $courseid)), - get_string('newbadge', 'badges')); + echo $OUTPUT->box($OUTPUT->single_button(new moodle_url('newbadge.php', array('type' => $type, 'id' => $courseid)), + get_string('newbadge', 'badges'))); } } diff --git a/badges/lib/backpacklib.php b/badges/lib/backpacklib.php deleted file mode 100644 index 1fe156f6a7e..00000000000 --- a/badges/lib/backpacklib.php +++ /dev/null @@ -1,161 +0,0 @@ -. - -/** - * External backpack 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 - */ - -defined('MOODLE_INTERNAL') || die(); - -global $CFG; -require_once($CFG->libdir . '/filelib.php'); - -// Adopted from https://github.com/jbkc85/openbadges-class-php. -// Author Jason Cameron . - -class OpenBadgesBackpackHandler { - private $backpack; - private $email; - private $backpackuid = 0; - - public function __construct($record) { - $this->backpack = $record->backpackurl; - $this->email = $record->email; - $this->backpackuid = isset($record->backpackuid) ? $record->backpackuid : 0; - } - - public function curl_request($action, $collection = null) { - $curl = new curl(); - - switch($action) { - case 'user': - $url = $this->backpack . "/displayer/convert/email"; - $param = array('email' => $this->email); - break; - case 'groups': - $url = $this->backpack . '/displayer/' . $this->backpackuid . '/groups.json'; - break; - case 'badges': - $url = $this->backpack . '/displayer/' . $this->backpackuid . '/group/' . $collection . '.json'; - break; - } - - $curl->setHeader(array('Accept: application/json', 'Expect:')); - $options = array( - 'FRESH_CONNECT' => true, - 'RETURNTRANSFER' => true, - 'FORBID_REUSE' => true, - 'HEADER' => 0, - 'CONNECTTIMEOUT' => 3, - // Follow redirects with the same type of request when sent 301, or 302 redirects. - 'CURLOPT_POSTREDIR' => 3 - ); - - if ($action == 'user') { - $out = $curl->post($url, $param, $options); - } else { - $out = $curl->get($url, array(), $options); - } - - return json_decode($out); - } - - private function check_status($status) { - switch($status) { - case "missing": - $response = array( - 'status' => $status, - 'message' => get_string('error:nosuchuser', 'badges') - ); - return $response; - } - } - - public function get_collections() { - $json = $this->curl_request('user', $this->email); - if (isset($json->status)) { - if ($json->status != 'okay') { - return $this->check_status($json->status); - } else { - $this->backpackuid = $json->userId; - return $this->curl_request('groups'); - } - } - } - - public function get_badges($collection) { - $json = $this->curl_request('user', $this->email); - if (isset($json->status)) { - if ($json->status != 'okay') { - return $this->check_status($json->status); - } else { - return $this->curl_request('badges', $collection); - } - } - } - - public function get_url() { - return $this->backpack; - } -} - -/** - * Create and send a verification email to the email address supplied. - * - * Since we're not sending this email to a user, email_to_user can't be used - * but this function borrows largely the code from that process. - * - * @param string $email the email address to send the verification email to. - * @return true if the email was sent successfully, false otherwise. - */ -function send_verification_email($email) { - global $DB, $USER; - - // Store a user secret (badges_email_verify_secret) and the address (badges_email_verify_address) as users prefs. - // The address will be used by edit_backpack_form for display during verification and to facilitate the resending - // of verification emails to said address. - $secret = random_string(15); - set_user_preference('badges_email_verify_secret', $secret); - set_user_preference('badges_email_verify_address', $email); - - // To, from. - $tempuser = $DB->get_record('user', array('id' => $USER->id), '*', MUST_EXIST); - $tempuser->email = $email; - $noreplyuser = core_user::get_noreply_user(); - - // Generate the verification email body. - $verificationurl = '/badges/backpackemailverify.php'; - $verificationurl = new moodle_url($verificationurl); - $verificationpath = $verificationurl->out(false); - - $site = get_site(); - $args = new stdClass(); - $args->link = $verificationpath . '?data='. $secret; - $args->sitename = $site->fullname; - $args->admin = generate_email_signoff(); - - $messagesubject = get_string('backpackemailverifyemailsubject', 'badges', $site->fullname); - $messagetext = get_string('backpackemailverifyemailbody', 'badges', $args); - $messagehtml = text_to_html($messagetext, false, false, true); - - return email_to_user($tempuser, $noreplyuser, $messagesubject, $messagetext, $messagehtml); -} diff --git a/badges/mybackpack.php b/badges/mybackpack.php index 5a403386e94..1a660239291 100644 --- a/badges/mybackpack.php +++ b/badges/mybackpack.php @@ -26,8 +26,6 @@ require_once(__DIR__ . '/../config.php'); require_once($CFG->libdir . '/badgeslib.php'); -require_once($CFG->dirroot . '/badges/backpack_form.php'); -require_once($CFG->dirroot . '/badges/lib/backpacklib.php'); require_login(); @@ -57,25 +55,34 @@ $badgescache = cache::make('core', 'externalbadges'); if ($disconnect && $backpack) { require_sesskey(); - $DB->delete_records('badge_external', array('backpackid' => $backpack->id)); - $DB->delete_records('badge_backpack', array('userid' => $USER->id)); - $badgescache->delete($USER->id); + $sitebackpack = badges_get_site_backpack(0, $backpack->backpackurl); + // If backpack is connected, need to select collections. + $bp = new \core_badges\backpack_api($sitebackpack, $backpack); + $bp->disconnect_backpack($USER->id, $backpack->id); redirect(new moodle_url('/badges/mybackpack.php')); } - if ($backpack) { + + $sitebackpack = badges_get_site_backpack(0, $backpack->backpackurl); + // If backpack is connected, need to select collections. - $bp = new OpenBadgesBackpackHandler($backpack); + $bp = new \core_badges\backpack_api($sitebackpack, $backpack); $request = $bp->get_collections(); - if (empty($request->groups)) { - $params['nogroups'] = get_string('error:nogroups', 'badges'); + $groups = $request; + if (isset($request->groups)) { + $groups = $request->groups; + } + if (empty($groups)) { + $err = get_string('error:nogroupssummary', 'badges'); + $err .= get_string('error:nogroupslink', 'badges', $sitebackpack->backpackweburl); + $params['nogroups'] = $err; } else { - $params['groups'] = $request->groups; + $params['groups'] = $groups; } $params['email'] = $backpack->email; - $params['selected'] = $DB->get_fieldset_select('badge_external', 'collectionid', 'backpackid = :bid', array('bid' => $backpack->id)); - $params['backpackid'] = $backpack->id; - $form = new edit_collections_form(new moodle_url('/badges/mybackpack.php'), $params); + $params['selected'] = $bp->get_collection_record($backpack->id); + $params['backpackweburl'] = $sitebackpack->backpackweburl; + $form = new \core_badges\form\collections(new moodle_url('/badges/mybackpack.php'), $params); if ($form->is_cancelled()) { redirect(new moodle_url('/badges/mybadges.php')); @@ -85,27 +92,7 @@ if ($backpack) { } else { $groups = array_filter($data->group); } - - // Remove all unselected collections if there are any. - $sqlparams = array('backpack' => $backpack->id); - $select = 'backpackid = :backpack '; - if (!empty($groups)) { - list($grouptest, $groupparams) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED, 'col', false); - $select .= ' AND collectionid ' . $grouptest; - $sqlparams = array_merge($sqlparams, $groupparams); - } - $DB->delete_records_select('badge_external', $select, $sqlparams); - - // Insert selected collections if they are not in database yet. - foreach ($groups as $group) { - $obj = new stdClass(); - $obj->backpackid = $data->backpackid; - $obj->collectionid = (int) $group; - if (!$DB->record_exists('badge_external', array('backpackid' => $obj->backpackid, 'collectionid' => $obj->collectionid))) { - $DB->insert_record('badge_external', $obj); - } - } - $badgescache->delete($USER->id); + $bp->set_backpack_collections($backpack->id, $groups); redirect(new moodle_url('/badges/mybadges.php')); } } else { @@ -121,8 +108,10 @@ if ($backpack) { // To pass through the current state of the verification attempt to the form. $params['email'] = get_user_preferences('badges_email_verify_address'); + $params['backpackpassword'] = get_user_preferences('badges_email_verify_password'); + $params['backpackid'] = get_user_preferences('badges_email_verify_backpackid'); - $form = new edit_backpack_form(new moodle_url('/badges/mybackpack.php'), $params); + $form = new \core_badges\form\backpack(new moodle_url('/badges/mybackpack.php'), $params); if ($form->is_cancelled()) { redirect(new moodle_url('/badges/mybadges.php')); } else if ($data = $form->get_data()) { @@ -131,11 +120,11 @@ if ($backpack) { // 2. After clicking 'Resend verification email'. We'll have $data->email. // 3. After clicking 'Connect using a different email' to cancel the verification process. We'll have $data->revertbutton. if (isset($data->revertbutton)) { - unset_user_preference('badges_email_verify_secret'); - unset_user_preference('badges_email_verify_address'); + badges_disconnect_user_backpack($USER->id); redirect(new moodle_url('/badges/mybackpack.php')); } else if (isset($data->email)) { - if (send_verification_email($data->email)) { + if (badges_send_verification_email($data->email, $data->backpackid, $data->backpackpassword)) { + $a = get_user_preferences('badges_email_verify_backpackid'); redirect(new moodle_url('/badges/mybackpack.php'), get_string('backpackemailverifypending', 'badges', $data->email), null, \core\output\notification::NOTIFY_INFO); diff --git a/badges/mybadges.php b/badges/mybadges.php index 97c4aeee0bf..26bffee1c62 100644 --- a/badges/mybadges.php +++ b/badges/mybadges.php @@ -99,10 +99,17 @@ $output = $PAGE->get_renderer('core', 'badges'); $badges = badges_get_user_badges($USER->id); echo $OUTPUT->header(); +$success = optional_param('success', '', PARAM_ALPHA); +$warning = optional_param('warning', '', PARAM_ALPHA); +if (!empty($success)) { + echo $OUTPUT->notification(get_string($success, 'core_badges'), 'notifysuccess'); +} else if (!empty($warning)) { + echo $OUTPUT->notification(get_string($warning, 'core_badges'), 'warning'); +} $totalcount = count($badges); $records = badges_get_user_badges($USER->id, null, $page, BADGE_PERPAGE, $search); -$userbadges = new badge_user_collection($records, $USER->id); +$userbadges = new \core_badges\output\badge_user_collection($records, $USER->id); $userbadges->sort = 'dateissued'; $userbadges->dir = 'DESC'; $userbadges->page = $page; diff --git a/badges/newbadge.php b/badges/newbadge.php index 45081bceb8f..9087b2d85b7 100644 --- a/badges/newbadge.php +++ b/badges/newbadge.php @@ -26,7 +26,6 @@ require_once(__DIR__ . '/../config.php'); require_once($CFG->libdir . '/badgeslib.php'); -require_once($CFG->dirroot . '/badges/edit_form.php'); $type = required_param('type', PARAM_INT); $courseid = optional_param('id', 0, PARAM_INT); @@ -62,13 +61,12 @@ if (($type == BADGE_TYPE_COURSE) && ($course = $DB->get_record('course', array(' require_capability('moodle/badges:createbadge', $PAGE->context); -$PAGE->requires->js('/badges/backpack.js'); -$PAGE->requires->js_init_call('check_site_access', null, false); +badges_local_backpack_js(true); $fordb = new stdClass(); $fordb->id = null; -$form = new edit_details_form($PAGE->url, array('action' => 'new')); +$form = new \core_badges\form\badge($PAGE->url, array('action' => 'new')); if ($form->is_cancelled()) { redirect(new moodle_url('/badges/index.php', array('type' => $type, 'id' => $courseid))); @@ -88,9 +86,18 @@ if ($form->is_cancelled()) { $fordb->timemodified = $now; $fordb->usercreated = $USER->id; $fordb->usermodified = $USER->id; - $fordb->issuername = $data->issuername; - $fordb->issuerurl = $data->issuerurl; - $fordb->issuercontact = $data->issuercontact; + + if (badges_open_badges_backpack_api() != OPEN_BADGES_V2) { + $fordb->issuername = $data->issuername; + $fordb->issuerurl = $data->issuerurl; + $fordb->issuercontact = $data->issuercontact; + } else { + $url = parse_url($CFG->wwwroot); + $fordb->issuerurl = $url['scheme'] . '://' . $url['host']; + $fordb->issuername = $CFG->badges_defaultissuername; + $fordb->issuercontact = $CFG->badges_defaultissuercontact; + } + $fordb->expiredate = ($data->expiry == 1) ? $data->expiredate : null; $fordb->expireperiod = ($data->expiry == 2) ? $data->expireperiod : null; $fordb->type = $type; @@ -123,4 +130,4 @@ echo $OUTPUT->box('', 'notifyproblem hide', 'check_connection'); $form->display(); -echo $OUTPUT->footer(); \ No newline at end of file +echo $OUTPUT->footer(); diff --git a/badges/recipients.php b/badges/recipients.php index 244fef3a3b1..0dce38df78c 100644 --- a/badges/recipients.php +++ b/badges/recipients.php @@ -98,7 +98,7 @@ $totalcount = $DB->count_records('badge_issued', array('badgeid' => $badge->id)) if ($badge->has_awards()) { $users = $DB->get_records_sql($sql, array('badgeid' => $badge->id), $page * BADGE_PERPAGE, BADGE_PERPAGE); - $recipients = new badge_recipients($users); + $recipients = new core_badges\output\badge_recipients($users); $recipients->sort = $sortby; $recipients->dir = $sorthow; $recipients->page = $page; @@ -110,4 +110,4 @@ if ($badge->has_awards()) { echo $output->notification(get_string('noawards', 'badges')); } -echo $output->footer(); \ No newline at end of file +echo $output->footer(); diff --git a/badges/related.php b/badges/related.php index d8af01b3596..066b5e95f12 100644 --- a/badges/related.php +++ b/badges/related.php @@ -95,7 +95,7 @@ if (is_null($action)) { } if ($badge->has_related()) { $badgerelated = $badge->get_related_badges(); - $renderrelated = new badge_related($badgerelated, $badgeid); + $renderrelated = new \core_badges\output\badge_related($badgerelated, $badgeid); echo $output->render($renderrelated); } else { echo $output->notification(get_string('norelated', 'badges')); diff --git a/badges/renderer.php b/badges/renderer.php index a401c950cc3..d863812b5b1 100644 --- a/badges/renderer.php +++ b/badges/renderer.php @@ -41,8 +41,20 @@ class core_badges_renderer extends plugin_renderer_base { $bname = $badge->name; $imageurl = moodle_url::make_pluginfile_url($context->id, 'badges', 'badgeimage', $badge->id, '/', 'f1', false); } else { - $bname = s($badge->assertion->badge->name); - $imageurl = $badge->imageUrl; + $bname = ''; + $imageurl = ''; + if (!empty($badge->name)) { + $bname = s($badge->name); + } + if (!empty($badge->image)) { + $imageurl = $badge->image; + } + if (isset($badge->assertion->badge->name)) { + $bname = s($badge->assertion->badge->name); + } + if (isset($badge->imageUrl)) { + $imageurl = $badge->imageUrl; + } } $name = html_writer::tag('span', $bname, array('class' => 'badge-name')); @@ -58,13 +70,27 @@ class core_badges_renderer extends plugin_renderer_base { $download = $status = $push = ''; if (($userid == $USER->id) && !$profile) { - $url = new moodle_url('mybadges.php', array('download' => $badge->id, 'hash' => $badge->uniquehash, 'sesskey' => sesskey())); + $params = array( + 'download' => $badge->id, + 'hash' => $badge->uniquehash, + 'sesskey' => sesskey() + ); + $url = new moodle_url( + 'mybadges.php', + $params + ); $notexpiredbadge = (empty($badge->dateexpire) || $badge->dateexpire > time()); $backpackexists = badges_user_has_backpack($USER->id); if (!empty($CFG->badges_allowexternalbackpack) && $notexpiredbadge && $backpackexists) { $assertion = new moodle_url('/badges/assertion.php', array('b' => $badge->uniquehash)); - $action = new component_action('click', 'addtobackpack', array('assertion' => $assertion->out(false))); - $push = $this->output->action_icon(new moodle_url('#'), new pix_icon('t/backpack', get_string('addtobackpack', 'badges')), $action); + $action = null; + if (badges_open_badges_backpack_api() == OPEN_BADGES_V1) { + $action = new component_action('click', 'addtobackpack', array('assertion' => $assertion->out(false))); + $url = new moodle_url('#'); + } else { + $url = new moodle_url('/badges/backpack-add.php', array('hash' => $badge->uniquehash)); + } + $push = $this->output->action_icon($url, new pix_icon('t/backpack', get_string('addtobackpack', 'badges')), $action); } $download = $this->output->action_icon($url, new pix_icon('t/download', get_string('download'))); @@ -260,7 +286,7 @@ class core_badges_renderer extends plugin_renderer_base { // Edit badge. if (has_capability('moodle/badges:configuredetails', $context)) { - $url = new moodle_url('/badges/edit.php', array('id' => $badge->id, 'action' => 'details')); + $url = new moodle_url('/badges/edit.php', array('id' => $badge->id, 'action' => 'badge')); $actions .= $this->output->action_icon($url, new pix_icon('t/edit', get_string('edit'))) . " "; } @@ -281,7 +307,7 @@ class core_badges_renderer extends plugin_renderer_base { } // Outputs issued badge with actions available. - protected function render_issued_badge(issued_badge $ibadge) { + protected function render_issued_badge(\core_badges\output\issued_badge $ibadge) { global $USER, $CFG, $DB, $SITE; $issued = $ibadge->issued; $userinfo = $ibadge->recipient; @@ -309,15 +335,24 @@ class core_badges_renderer extends plugin_renderer_base { get_string('download'), 'POST'); if (!empty($CFG->badges_allowexternalbackpack) && ($expiration > $now) && badges_user_has_backpack($USER->id)) { - $assertion = new moodle_url('/badges/assertion.php', array('b' => $issued['uid'])); - $action = new component_action('click', 'addtobackpack', array('assertion' => $assertion->out(false))); - $attributes = array( - 'type' => 'button', - 'id' => 'addbutton', - 'value' => get_string('addtobackpack', 'badges')); - $tobackpack = html_writer::tag('input', '', $attributes); - $this->output->add_action_handler($action, 'addbutton'); - $output .= $tobackpack; + + if (badges_open_badges_backpack_api() == OPEN_BADGES_V1) { + $assertion = new moodle_url('/badges/assertion.php', array('b' => $issued['uid'])); + $action = new component_action('click', 'addtobackpack', array('assertion' => $assertion->out(false))); + $attributes = array( + 'type' => 'button', + 'class' => 'btn btn-secondary m-1', + 'id' => 'addbutton', + 'value' => get_string('addtobackpack', 'badges')); + $tobackpack = html_writer::tag('input', '', $attributes); + $this->output->add_action_handler($action, 'addbutton'); + $output .= $tobackpack; + } else { + $assertion = new moodle_url('/badges/backpack-add.php', array('hash' => $issued['uid'])); + $attributes = ['class' => 'btn btn-secondary m-1', 'role' => 'button']; + $tobackpack = html_writer::link($assertion, get_string('addtobackpack', 'badges'), $attributes); + $output .= $tobackpack; + } } } $output .= html_writer::end_tag('div'); @@ -446,7 +481,7 @@ class core_badges_renderer extends plugin_renderer_base { } // Outputs external badge. - protected function render_external_badge(external_badge $ibadge) { + protected function render_external_badge(\core_badges\output\external_badge $ibadge) { $issued = $ibadge->issued; $assertion = $issued->assertion; $issuer = $assertion->badge->issuer; @@ -457,7 +492,10 @@ class core_badges_renderer extends plugin_renderer_base { $output = ''; $output .= html_writer::start_tag('div', array('id' => 'badge')); $output .= html_writer::start_tag('div', array('id' => 'badge-image')); - $output .= html_writer::empty_tag('img', array('src' => $issued->imageUrl)); + if (isset($issued->imageUrl)) { + $issued->image = $issued->imageUrl; + } + $output .= html_writer::empty_tag('img', array('src' => $issued->image)); if (isset($assertion->expires)) { $expiration = !strtotime($assertion->expires) ? s($assertion->expires) : strtotime($assertion->expires); if ($expiration < $today) { @@ -491,7 +529,9 @@ class core_badges_renderer extends plugin_renderer_base { $output .= $this->output->heading(get_string('issuerdetails', 'badges'), 3); $dl = array(); $dl[get_string('issuername', 'badges')] = s($issuer->name); - $dl[get_string('issuerurl', 'badges')] = html_writer::tag('a', $issuer->origin, array('href' => $issuer->origin)); + if (isset($issuer->origin)) { + $dl[get_string('issuerurl', 'badges')] = html_writer::tag('a', $issuer->origin, array('href' => $issuer->origin)); + } if (isset($issuer->contact)) { $dl[get_string('contact', 'badges')] = obfuscate_mailto($issuer->contact); @@ -502,10 +542,15 @@ class core_badges_renderer extends plugin_renderer_base { $dl = array(); $dl[get_string('name')] = s($assertion->badge->name); $dl[get_string('description', 'badges')] = s($assertion->badge->description); - $dl[get_string('bcriteria', 'badges')] = html_writer::tag('a', s($assertion->badge->criteria), array('href' => $assertion->badge->criteria)); + if (isset($assertion->badge->criteria)) { + $dl[get_string('bcriteria', 'badges')] = html_writer::tag( + 'a', + s($assertion->badge->criteria), + array('href' => $assertion->badge->criteria) + ); + } $output .= $this->definition_list($dl); - $output .= $this->output->heading(get_string('issuancedetails', 'badges'), 3); $dl = array(); if (isset($assertion->issued_on)) { $issuedate = !strtotime($assertion->issued_on) ? s($assertion->issued_on) : strtotime($assertion->issued_on); @@ -519,17 +564,24 @@ class core_badges_renderer extends plugin_renderer_base { } } if (isset($assertion->evidence)) { - $dl[get_string('evidence', 'badges')] = html_writer::tag('a', s($assertion->evidence), array('href' => $assertion->evidence)); + $dl[get_string('evidence', 'badges')] = html_writer::tag( + 'a', + s($assertion->evidence), + array('href' => $assertion->evidence) + ); + } + if (!empty($dl)) { + $output .= $this->output->heading(get_string('issuancedetails', 'badges'), 3); + $output .= $this->definition_list($dl); } - $output .= $this->definition_list($dl); $output .= html_writer::end_tag('div'); return $output; } // Displays the user badges. - protected function render_badge_user_collection(badge_user_collection $badges) { - global $CFG, $USER, $SITE; + protected function render_badge_user_collection(\core_badges\output\badge_user_collection $badges) { + global $CFG, $USER, $SITE, $OUTPUT; $backpack = $badges->backpack; $mybackpack = new moodle_url('/badges/mybackpack.php'); @@ -545,20 +597,23 @@ class core_badges_renderer extends plugin_renderer_base { $searchform = $this->output->box($this->helper_search_form($badges->search), 'boxwidthwide boxaligncenter'); // Download all button. - $downloadall = $this->output->single_button( + $actionhtml = $this->output->single_button( new moodle_url('/badges/mybadges.php', array('downloadall' => true, 'sesskey' => sesskey())), get_string('downloadall'), 'POST', array('class' => 'activatebadge')); + $downloadall = $this->output->box('', 'col-md-3'); + $downloadall .= $this->output->box($actionhtml, 'col-md-9'); + $downloadall = $this->output->box($downloadall, 'row m-l-2'); // Local badges. $localhtml = html_writer::start_tag('div', array('id' => 'issued-badge-table', 'class' => 'generalbox')); - $heading = get_string('localbadges', 'badges', format_string($SITE->fullname, true, array('context' => context_system::instance()))); + $sitename = format_string($SITE->fullname, true, array('context' => context_system::instance())); + $heading = get_string('localbadges', 'badges', $sitename); $localhtml .= $this->output->heading_with_help($heading, 'localbadgesh', 'badges'); if ($badges->badges) { - $downloadbutton = $this->output->heading(get_string('badgesearned', 'badges', $badges->totalcount), 4, 'activatebadge'); - $downloadbutton .= $downloadall; + $countmessage = $this->output->box(get_string('badgesearned', 'badges', $badges->totalcount)); $htmllist = $this->print_badges_list($badges->badges, $USER->id); - $localhtml .= $backpackconnect . $downloadbutton . $searchform . $htmlpagingbar . $htmllist . $htmlpagingbar; + $localhtml .= $backpackconnect . $countmessage . $searchform . $htmlpagingbar . $htmllist . $htmlpagingbar . $downloadall; } else { $localhtml .= $searchform . $this->output->notification(get_string('nobadges', 'badges')); } @@ -570,13 +625,17 @@ class core_badges_renderer extends plugin_renderer_base { $externalhtml .= html_writer::start_tag('div', array('class' => 'generalbox')); $externalhtml .= $this->output->heading_with_help(get_string('externalbadges', 'badges'), 'externalbadges', 'badges'); if (!is_null($backpack)) { + if ($backpack->backpackid != $CFG->badges_site_backpack) { + $externalhtml .= $OUTPUT->notification(get_string('backpackneedsupdate', 'badges'), 'warning'); + + } if ($backpack->totalcollections == 0) { - $externalhtml .= get_string('nobackpackcollections', 'badges', $backpack); + $externalhtml .= get_string('nobackpackcollectionssummary', 'badges', $backpack); } else { if ($backpack->totalbadges == 0) { - $externalhtml .= get_string('nobackpackbadges', 'badges', $backpack); + $externalhtml .= get_string('nobackpackbadgessummary', 'badges', $backpack); } else { - $externalhtml .= get_string('backpackbadges', 'badges', $backpack); + $externalhtml .= get_string('backpackbadgessummary', 'badges', $backpack); $externalhtml .= '

' . $this->print_badges_list($backpack->badges, $USER->id, true, true); } } @@ -585,13 +644,20 @@ class core_badges_renderer extends plugin_renderer_base { } $externalhtml .= html_writer::end_tag('div'); + $attr = ['class' => 'btn btn-secondary']; + $label = get_string('backpackbadgessettings', 'badges'); + $backpacksettings = html_writer::link(new moodle_url('/badges/mybackpack.php'), $label, $attr); + $actionshtml = $this->output->box('', 'col-md-3'); + $actionshtml .= $this->output->box($backpacksettings, 'col-md-9'); + $actionshtml = $this->output->box($actionshtml, 'row m-l-2'); + $externalhtml .= $actionshtml; } return $localhtml . $externalhtml; } // Displays the available badges. - protected function render_badge_collection(badge_collection $badges) { + protected function render_badge_collection(\core_badges\output\badge_collection $badges) { $paging = new paging_bar($badges->totalcount, $badges->page, $badges->perpage, $this->page->url, 'page'); $htmlpagingbar = $this->render($paging); $table = new html_table(); @@ -634,7 +700,7 @@ class core_badges_renderer extends plugin_renderer_base { } // Outputs table of badges with actions available. - protected function render_badge_management(badge_management $badges) { + protected function render_badge_management(\core_badges\output\badge_management $badges) { $paging = new paging_bar($badges->totalcount, $badges->page, $badges->perpage, $this->page->url, 'page'); // New badge button. @@ -642,7 +708,8 @@ class core_badges_renderer extends plugin_renderer_base { if (has_capability('moodle/badges:createbadge', $this->page->context)) { $n['type'] = $this->page->url->get_param('type'); $n['id'] = $this->page->url->get_param('id'); - $htmlnew = $this->output->single_button(new moodle_url('newbadge.php', $n), get_string('newbadge', 'badges')); + $btn = $this->output->single_button(new moodle_url('newbadge.php', $n), get_string('newbadge', 'badges')); + $htmlnew = $this->output->box($btn); } $htmlpagingbar = $this->render($paging); @@ -700,7 +767,7 @@ class core_badges_renderer extends plugin_renderer_base { if (has_capability('moodle/badges:configuredetails', $context)) { $row[] = new tabobject('details', - new moodle_url('/badges/edit.php', array('id' => $badgeid, 'action' => 'details')), + new moodle_url('/badges/edit.php', array('id' => $badgeid, 'action' => 'badge')), get_string('bdetails', 'badges') ); } @@ -912,7 +979,7 @@ class core_badges_renderer extends plugin_renderer_base { // Renders a table with users who have earned the badge. // Based on stamps collection plugin. - protected function render_badge_recipients(badge_recipients $recipients) { + protected function render_badge_recipients(\core_badges\output\badge_recipients $recipients) { $paging = new paging_bar($recipients->totalcount, $recipients->page, $recipients->perpage, $this->page->url, 'page'); $htmlpagingbar = $this->render($paging); $table = new html_table(); @@ -1128,10 +1195,10 @@ class core_badges_renderer extends plugin_renderer_base { /** * Renders a table for related badges. * - * @param badge_related $related list related badges. + * @param \core_badges\output\badge_related $related list related badges. * @return string list related badges to output. */ - protected function render_badge_related(badge_related $related) { + protected function render_badge_related(\core_badges\output\badge_related $related) { $currentbadge = new badge($related->currentbadgeid); $languages = get_string_manager()->get_list_of_languages(); $paging = new paging_bar($related->totalcount, $related->page, $related->perpage, $this->page->url, 'page'); @@ -1188,10 +1255,10 @@ class core_badges_renderer extends plugin_renderer_base { /** * Renders a table with alignment. * - * @param badge_alignments $alignments List alignments. + * @param core_badges\output\badge_alignments $alignments List alignments. * @return string List alignment to output. */ - protected function render_badge_alignments(badge_alignments $alignments) { + protected function render_badge_alignments(\core_badges\output\badge_alignments $alignments) { $currentbadge = new badge($alignments->currentbadgeid); $paging = new paging_bar($alignments->totalcount, $alignments->page, $alignments->perpage, $this->page->url, 'page'); $htmlpagingbar = $this->render($paging); @@ -1236,287 +1303,16 @@ class core_badges_renderer extends plugin_renderer_base { return $htmlpagingbar . $htmltable . $htmlpagingbar; } -} - -/** - * An issued badges for badge.php page - */ -class issued_badge implements renderable { - /** @var issued badge */ - public $issued; - - /** @var badge recipient */ - public $recipient; - - /** @var badge class */ - public $badgeclass; - - /** @var badge visibility to others */ - public $visible = 0; - - /** @var badge class */ - public $badgeid = 0; /** - * Initializes the badge to display + * Defer to template. * - * @param string $hash Issued badge hash + * @param \core_badges\output\external_backpacks_page $page + * @return bool|string */ - public function __construct($hash) { - global $DB; - - $assertion = new core_badges_assertion($hash); - $this->issued = $assertion->get_badge_assertion(); - $this->badgeclass = $assertion->get_badge_class(); - - $rec = $DB->get_record_sql('SELECT userid, visible, badgeid - FROM {badge_issued} - WHERE ' . $DB->sql_compare_text('uniquehash', 40) . ' = ' . $DB->sql_compare_text(':hash', 40), - array('hash' => $hash), IGNORE_MISSING); - if ($rec) { - // Get a recipient from database. - $namefields = get_all_user_name_fields(true, 'u'); - $user = $DB->get_record_sql("SELECT u.id, $namefields, u.deleted, u.email - FROM {user} u WHERE u.id = :userid", array('userid' => $rec->userid)); - $this->recipient = $user; - $this->visible = $rec->visible; - $this->badgeid = $rec->badgeid; - } + public function render_external_backpacks_page(\core_badges\output\external_backpacks_page $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('core_badges/external_backpacks_page', $data); } } -/** - * An external badges for external.php page - */ -class external_badge implements renderable { - /** @var issued badge */ - public $issued; - - /** @var User ID */ - public $recipient; - - /** @var validation of external badge */ - public $valid = true; - - /** - * Initializes the badge to display - * - * @param object $badge External badge information. - * @param int $recipient User id. - */ - public function __construct($badge, $recipient) { - global $DB; - // At this point a user has connected a backpack. So, we are going to get - // their backpack email rather than their account email. - $namefields = get_all_user_name_fields(true, 'u'); - $user = $DB->get_record_sql("SELECT {$namefields}, b.email - FROM {user} u INNER JOIN {badge_backpack} b ON u.id = b.userid - WHERE userid = :userid", array('userid' => $recipient), IGNORE_MISSING); - - $this->issued = $badge; - $this->recipient = $user; - - // Check if recipient is valid. - // There is no way to be 100% sure that a badge belongs to a user. - // Backpack does not return any recipient information. - // All we can do is compare that backpack email hashed using salt - // provided in the assertion matches a badge recipient from the assertion. - if ($user) { - if (validate_email($badge->assertion->recipient) && $badge->assertion->recipient == $user->email) { - // If we have email, compare emails. - $this->valid = true; - } else if ($badge->assertion->recipient == 'sha256$' . hash('sha256', $user->email)) { - // If recipient is hashed, but no salt, compare hashes without salt. - $this->valid = true; - } else if ($badge->assertion->recipient == 'sha256$' . hash('sha256', $user->email . $badge->assertion->salt)) { - // If recipient is hashed, compare hashes. - $this->valid = true; - } else { - // Otherwise, we cannot be sure that this user is a recipient. - $this->valid = false; - } - } else { - $this->valid = false; - } - } -} - -/** - * Badge recipients rendering class - */ -class badge_recipients implements renderable { - /** @var string how are the data sorted */ - public $sort = 'lastname'; - - /** @var string how are the data sorted */ - public $dir = 'ASC'; - - /** @var int page number to display */ - public $page = 0; - - /** @var int number of badge recipients to display per page */ - public $perpage = 30; - - /** @var int the total number or badge recipients to display */ - public $totalcount = null; - - /** @var array internal list of badge recipients ids */ - public $userids = array(); - /** - * Initializes the list of users to display - * - * @param array $holders List of badge holders - */ - public function __construct($holders) { - $this->userids = $holders; - } -} - -/** - * Collection of all badges for view.php page - */ -class badge_collection implements renderable { - - /** @var string how are the data sorted */ - public $sort = 'name'; - - /** @var string how are the data sorted */ - public $dir = 'ASC'; - - /** @var int page number to display */ - public $page = 0; - - /** @var int number of badges to display per page */ - public $perpage = BADGE_PERPAGE; - - /** @var int the total number of badges to display */ - public $totalcount = null; - - /** @var array list of badges */ - public $badges = array(); - - /** - * Initializes the list of badges to display - * - * @param array $badges Badges to render - */ - public function __construct($badges) { - $this->badges = $badges; - } -} - -/** - * Collection of badges used at the index.php page - */ -class badge_management extends badge_collection implements renderable { -} - -/** - * Collection of user badges used at the mybadges.php page - */ -class badge_user_collection extends badge_collection implements renderable { - /** @var array backpack settings */ - public $backpack = null; - - /** @var string search */ - public $search = ''; - - /** - * Initializes user badge collection. - * - * @param array $badges Badges to render - * @param int $userid Badges owner - */ - public function __construct($badges, $userid) { - global $CFG; - parent::__construct($badges); - - if (!empty($CFG->badges_allowexternalbackpack)) { - $this->backpack = get_backpack_settings($userid, true); - } - } -} - -/** - * Collection of all related badges. - * - * @copyright 2018 Tung Thai - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @author Tung Thai - */ -class badge_related implements renderable { - - /** @var string how are the data sorted. */ - public $sort = 'name'; - - /** @var string how are the data sorted. */ - public $dir = 'ASC'; - - /** @var int page number to display. */ - public $page = 0; - - /** @var int number of badges to display per page. */ - public $perpage = BADGE_PERPAGE; - - /** @var int the total number of badges to display. */ - public $totalcount = null; - - /** @var int the current badge. */ - public $currentbadgeid = 0; - - /** @var array list of badges. */ - public $badges = array(); - - /** - * Initializes the list of badges to display. - * - * @param array $badges related badges to render. - * @param int $currentbadgeid ID current badge. - */ - public function __construct($badges, $currentbadgeid) { - $this->badges = $badges; - $this->currentbadgeid = $currentbadgeid; - } -} - -/** - * Collection of all alignments. - * - * @copyright 2018 Tung Thai - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @author Tung Thai - */ -class badge_alignments implements renderable -{ - /** @var string how are the data sorted. */ - public $sort = 'name'; - - /** @var string how are the data sorted. */ - public $dir = 'ASC'; - - /** @var int page number to display. */ - public $page = 0; - - /** @var int number of badges to display per page. */ - public $perpage = BADGE_PERPAGE; - - /** @var int the total number of badges to display. */ - public $totalcount = null; - - /** @var array list of badges. */ - public $alignments = array(); - - /** @var array list of badges. */ - public $currentbadgeid = 0; - - /** - * Initializes the list of alignments to display. - * - * @param array $alignments List alignments to render. - * @param int $currentbadgeid ID current badge. - */ - public function __construct($alignments, $currentbadgeid) { - $this->alignments = $alignments; - $this->currentbadgeid = $currentbadgeid; - } -} diff --git a/badges/templates/external_backpacks_page.mustache b/badges/templates/external_backpacks_page.mustache new file mode 100644 index 00000000000..2c7dbba48f4 --- /dev/null +++ b/badges/templates/external_backpacks_page.mustache @@ -0,0 +1,48 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + Manage backpacks. + + Context variables required for this template: + * backpacks - array of objects containing id, backpackurl, enabled, apiversion and sortorder +}} + + + + + + + + + + + {{#backpacks}} + + + + + + + {{/backpacks}} + +
{{#str}}listbackpacks, core_badges{{/str}}
{{#str}}backpackweburl, core_badges{{/str}}{{#str}}sitebackpack, core_badges{{/str}}{{#str}}actions, core_badges{{/str}}
{{{backpackweburl}}} {{#sitebackpack}}Yes{{/sitebackpack}} + {{#canedit}} + + {{#str}}editsettings, core_badges{{/str}} + + {{/canedit}} +
diff --git a/badges/tests/badgeslib_test.php b/badges/tests/badgeslib_test.php index 4d9604e7036..b0df19fdef2 100644 --- a/badges/tests/badgeslib_test.php +++ b/badges/tests/badgeslib_test.php @@ -136,15 +136,16 @@ class core_badges_badgeslib_testcase extends advanced_testcase { // Format JSON-LD for Openbadge specification version 2.0. $this->assertion2 = new stdClass(); $this->assertion2->badge = '{"recipient":{"identity":"%s","type":"email","hashed":true,"salt":"%s"},' . - '"badge":{"name":"%s","description":"%s","image":{"id":"%s","author":"%s","caption":"%s"},' . + '"badge":{"name":"%s","description":"%s","image":"%s",' . '"criteria":{"id":"%s","narrative":"%s"},"issuer":{"name":"%s","url":"%s","email":"%s",' . '"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"Issuer"},' . '"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"BadgeClass","version":"%s",' . - '"@language":"%s","related":[{"id":"%s","version":"%s","@language":"%s"}],"endorsement":"%s",' . - '"alignment":[{"targetName":"%s","targetUrl":"%s","targetDescription":"%s","targetFramework":"%s",' . + '"@language":"en","related":[{"id":"%s","version":"%s","@language":"%s"}],"endorsement":"%s",' . + '"alignments":[{"targetName":"%s","targetUrl":"%s","targetDescription":"%s","targetFramework":"%s",' . '"targetCode":"%s"}]},"verify":{"type":"hosted","url":"%s"},"issuedOn":"%s","evidence":"%s",' . '"@context":"https:\/\/w3id.org\/openbadges\/v2","type":"Assertion","id":"%s"}'; - $this->assertion2->class = '{"name":"%s","description":"%s","image":{"id":"%s","author":"%s","caption":"%s"},' . + + $this->assertion2->class = '{"name":"%s","description":"%s","image":"%s",' . '"criteria":{"id":"%s","narrative":"%s"},"issuer":{"name":"%s","url":"%s","email":"%s",' . '"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"Issuer"},' . '"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"BadgeClass","version":"%s",' . diff --git a/badges/tests/external_test.php b/badges/tests/external_test.php index 7bbd982d485..45a1029e8d5 100644 --- a/badges/tests/external_test.php +++ b/badges/tests/external_test.php @@ -177,6 +177,8 @@ class core_badges_external_testcase extends externallib_advanced_testcase { if (!empty($alignments)) { foreach ($alignments as $alignment) { // Students cannot see some fields of the alignments. + unset($alignment->targetname); + unset($alignment->targeturl); unset($alignment->targetdescription); unset($alignment->targetframework); unset($alignment->targetcode); diff --git a/badges/view.php b/badges/view.php index f7d3fcb80ad..f943addf722 100644 --- a/badges/view.php +++ b/badges/view.php @@ -95,7 +95,7 @@ if ($totalcount) { echo $OUTPUT->box(get_string('error:notifycoursedate', 'badges'), 'generalbox notifyproblem'); } - $badges = new badge_collection($records); + $badges = new \core_badges\output\badge_collection($records); $badges->sort = $sortby; $badges->dir = $sorthow; $badges->page = $page; diff --git a/lang/en/badges.php b/lang/en/badges.php index 8e1c503620e..1d2e489a445 100644 --- a/lang/en/badges.php +++ b/lang/en/badges.php @@ -34,6 +34,7 @@ $string['addcompetency'] = 'Add competency'; $string['addcompetency_help'] = 'Select all competencies that should be added to this badge requirement. Hold CTRL key to select multiple items.'; $string['addbadgecriteria'] = 'Add badge criteria'; $string['addcriteria'] = 'Add criteria'; +$string['addbackpack'] = 'Add backpack'; $string['addcriteriatext'] = 'To start adding criteria, please select one of the options from the drop-down menu.'; $string['addcohort'] = 'Add cohort'; $string['addcohort_help'] = 'Select all cohorts that should be added to this badge requirement. Hold CTRL key to select multiple items.'; @@ -41,6 +42,7 @@ $string['addcourse'] = 'Add courses'; $string['addcourse_help'] = 'Select all courses that should be added to this badge requirement. Hold CTRL key to select multiple items.'; $string['addrelated'] = 'Add related badge'; $string['addtobackpack'] = 'Add to backpack'; +$string['addedtobackpack'] = 'Added badge to backpack'; $string['adminonly'] = 'This page is restricted to site administrators only.'; $string['after'] = 'after the date of issue.'; $string['aggregationmethod'] = 'Aggregation method'; @@ -69,6 +71,7 @@ $string['anymethodcompetencies'] = 'Any of the selected competencies have been c $string['anymethodcourseset'] = 'Any of the selected courses is complete'; $string['anymethodmanual'] = 'Any of the selected roles awards the badge'; $string['anymethodprofile'] = 'Any of the selected profile fields has been completed'; +$string['apiversion'] = 'API version supported'; $string['archivebadge'] = 'Would you like to delete badge \'{$a}\', but keep existing issued badges?'; $string['archiveconfirm'] = 'Delete and keep existing issued badges'; $string['archivehelp'] = '

This option means that the badge will be marked as "retired" and will no longer appear in the list of badges. Users will no longer be able to earn this badge, however existing badge recipients will still be able to display this badge on their profile page and push it to their external backpacks.

@@ -80,6 +83,7 @@ $string['awardedtoyou'] = 'Issued to me'; $string['awardoncron'] = 'Access to the badges was successfully enabled. Too many users can instantly earn this badge. To ensure site performance, this action will take some time to process.'; $string['awards'] = 'Recipients'; $string['backpackavailability'] = 'External badge verification'; +$string['backpackneedsupdate'] = 'The backpack connected to this profile does not match the backpack for the site. You need to disconnect and reconnect the backpack.'; $string['backpackavailability_help'] = 'For badge recipients to be able to prove they earned their badges from you, an external backpack service should be able to access your site and verify badges issued from it. Your site does not currently appear to be accessible, which means that badges you have already issued or will issue in the future cannot be verified. **Why am I seeing this message?** @@ -93,12 +97,11 @@ You should fix this issue on any production site where you are planning to issue **What if I can\'t make my whole site publicly accessible?** The only URL required for verification is [your-site-url]/badges/assertion.php so if you are able to modify your firewall to allow external access to that file, badge verification will still work.'; -$string['backpackbadges'] = 'You have {$a->totalbadges} badge(s) displayed from {$a->totalcollections} collection(s). Change backpack settings.'; +$string['backpackbadgessummary'] = 'You have {$a->totalbadges} badge(s) displayed from {$a->totalcollections} collection(s).'; +$string['backpackbadgessettings'] = 'Change backpack settings'; $string['backpackcannotsendverification'] = 'Cannot send verification email'; $string['backpackconnection'] = 'Backpack connection'; -$string['backpackconnection_help'] = 'This page allows you to set up connection to an external backpack provider. Connecting to a backpack lets you display external badges within this site and push badges earned here to your backpack. - -Currently, only Mozilla OpenBadges Backpack is supported. You need to sign up for a backpack service before trying to set up backpack connection on this page.'; +$string['backpackconnection_help'] = 'This page allows you to set up connection to an external backpack provider. Connecting to a backpack lets you display external badges within this site and push badges earned here to your backpack.'; $string['backpackconnectioncancelattempt'] = 'Connect using a different email address'; $string['backpackconnectionconnect'] = 'Connect to backpack'; $string['backpackconnectionresendemail'] = 'Resend verification email'; @@ -127,6 +130,9 @@ $string['backpackimport'] = 'Badge import settings'; $string['backpackimport_help'] = 'After the backpack connection is successfully established, badges from your backpack can be displayed on your badges page and your profile page. In this area, you can select collections of badges from your backpack that you would like to display in your profile.'; +$string['backpacksettings'] = 'Backpack settings'; +$string['backpackapiurl'] = 'Backpack API URL'; +$string['backpackweburl'] = 'Backpack Website URL'; $string['badges'] = 'Badges'; $string['badgedetails'] = 'Badge details'; $string['badgeimage'] = 'Image'; @@ -255,11 +261,14 @@ $string['dateearned'] = 'Date: {$a}'; $string['day'] = 'Day(s)'; $string['deactivate'] = 'Disable access'; $string['deactivatesuccess'] = 'Access to the badges was successfully disabled.'; -$string['defaultissuercontact'] = 'Default badge issuer contact details'; -$string['defaultissuercontact_desc'] = 'An email address associated with the badge issuer.'; -$string['defaultissuername'] = 'Default badge issuer name'; +$string['defaultissuercontact'] = 'Badge issuer email address'; +$string['defaultissuercontact_desc'] = 'An email address associated with the badge issuer. For an Open Badges v2.0 backpack, this is used for authentication when publishing badges to a backpack.'; +$string['defaultissuerpassword'] = 'Badge issuer password'; +$string['defaultissuerpassword_help'] = 'When publishing Badges to a Backpack using Open Badges v2.0, the "Badge issuer email address" and "Badge issuer password" must correspond to an account on the Backpack site that is used to issue Badges representing this site.'; +$string['defaultissuername'] = 'Badge issuer name'; $string['defaultissuername_desc'] = 'Name of the issuing agent or authority.'; $string['delbadge'] = 'Would you like to delete badge \'{$a}\' and remove all existing issued badges?'; +$string['delexternalbackpackconfirm'] = 'Delete site backpack \'{$a}\'?'; $string['delconfirm'] = 'Delete and remove existing issued badges'; $string['deletehelp'] = '

Fully deleting a badge means that all its information and criteria records will be permanently removed. Users who have earned this badge will no longer be able to access it and display it on their profile pages.

Note: Users who have earned this badge and have already pushed it to their external backpack, will still have this badge in their external backpack. However, they will not be able to access criteria and evidence pages linking back to this web site.

'; @@ -296,8 +305,8 @@ $string['error:invalidparamcohort'] = 'Cohort does not exist. '; $string['error:noactivities'] = 'There are no activities with completion criteria enabled in this course.'; $string['error:nocohorts'] = 'No cohorts'; $string['error:nocourses'] = 'Course completion is not enabled for any of the courses in this site, so none can be displayed. Course completion may be enabled in the course settings.'; -$string['error:nogroups'] = '

There are no public collections of badges available in your backpack.

-

Only public collections are shown, visit your backpack to create some public collections.

'; +$string['error:nogroupssummary'] = '

There are no public collections of badges available in your backpack.

'; +$string['error:nogroupslink'] = '

Only public collections are shown, visit your backpack to create some public collections.

'; $string['error:nopermissiontoview'] = 'You have no permissions to view badge recipients'; $string['error:nosuchbadge'] = 'Badge with id {$a} does not exist.'; $string['error:nosuchcohort'] = 'Warning: This cohort is no longer available.'; @@ -338,11 +347,13 @@ $string['expireperiodm'] = 'This badge expires {$a} minute(s) after being issued $string['expireperiods'] = 'This badge expires {$a} second(s) after being issued.'; $string['expirydate'] = 'Expiry date'; $string['expirydate_help'] = 'Optionally, badges can expire on a specific date, or the date can be calculated based on the date when the badge was issued to a user. '; +$string['existsinbackpack'] = 'Badge already exists in backpack'; $string['externalconnectto'] = 'To display external badges you need to connect to a backpack.'; $string['externalbadges'] = 'My badges from other web sites'; $string['externalbadgesp'] = 'Badges from other web sites:'; $string['externalbadges_help'] = 'This area displays badges from your external backpack.'; $string['fixed'] = 'Fixed date'; +$string['hidden'] = 'Hidden'; $string['hiddenbadge'] = 'Unfortunately, the badge owner has not made this information available.'; $string['imageauthoremail'] = 'Image author\'s email'; $string['imageauthoremail_help'] = 'If specified, the email address of the badge image author is displayed on the badge page.'; @@ -364,6 +375,7 @@ $string['issuerurl'] = 'Issuer URL'; $string['issuerurl_help'] = 'The website of the organisation issuing the endorsement. The URL should have a prefix http:// or https://.'; $string['language'] = 'Language'; $string['language_help'] = 'The language used on the badge page.'; +$string['listbackpacks'] = 'List of backpacks'; $string['localconnectto'] = 'To share these badges outside this web site you need to connect to a backpack.'; $string['localbadges'] = 'My badges from {$a} web site'; $string['localbadgesh'] = 'My badges from this web site'; @@ -376,6 +388,7 @@ $string['localbadgesp'] = 'Badges from {$a}:'; $string['makeprivate'] = 'Make private'; $string['makepublic'] = 'Make public'; $string['managebadges'] = 'Manage badges'; +$string['managebackpacks'] = 'Manage backpacks'; $string['message'] = 'Message body'; $string['messagebody'] = '

You have been awarded the badge "%badgename%"!

More information about this badge can be found on the %badgelink% badge information page.

@@ -392,8 +405,9 @@ $string['newimage'] = 'New image'; $string['noalignment'] = 'This badge does not have any external skills or standards specified.'; $string['noawards'] = 'This badge has not been earned yet.'; $string['nobackpack'] = 'There is no backpack service connected to this account.
'; -$string['nobackpackbadges'] = 'There are no badges in the collections you have selected. Add more collections.'; -$string['nobackpackcollections'] = 'No badge collections have been selected. Add collections.'; +$string['nobackpackbadgessummary'] = 'There are no badges in the collections you have selected.'; +$string['nobackpackcollectionssummary'] = 'No badge collections have been selected.'; +$string['nobackpacks'] = 'There are no backpacks available'; $string['nobadges'] = 'There are no badges available.'; $string['nocompetencies'] = 'No competencies selected.'; $string['nocriteria'] = 'Criteria for this badge have not been set up yet.'; @@ -429,6 +443,8 @@ $string['notifyweekly'] = 'Weekly'; $string['numawards'] = 'This badge has been issued to {$a->count} user(s).'; $string['numawardstat'] = 'This badge has been issued {$a} user(s).'; $string['overallcrit'] = 'of the selected criteria are complete.'; +$string['openbadgesv1'] = 'Open Badges v1.0'; +$string['openbadgesv2'] = 'Open Badges v2.0'; $string['potentialrecipients'] = 'Potential badge recipients'; $string['preferences'] = 'Badge preferences'; $string['privacy:metadata:backpack'] = 'A record of user\'s backpacks'; @@ -481,10 +497,12 @@ $string['reviewconfirm'] = '

This will make your badge visible to users and al $string['save'] = 'Save'; $string['searchname'] = 'Search by name'; $string['selectaward'] = 'Please select the role you would like to use to award this badge: '; -$string['selectgroup_end'] = 'Only public collections are shown, visit your backpack to create more public collections.'; +$string['selectgroup_end'] = 'Only public collections are shown, visit your backpack to create more public collections.'; $string['selectgroup_start'] = 'Select collections from your backpack to display on this site:'; $string['selecting'] = 'With selected badges...'; $string['setup'] = 'Set up connection'; +$string['sitebackpack'] = 'Site backpack'; +$string['sitebackpack_help'] = 'The external backpack to allow users to share their badges with. Changing this setting after users have connected their backpacks will require each user to update their backpack settings.'; $string['sitebadges'] = 'Site badges'; $string['sitebadges_help'] = 'Site badges can only be awarded to users for site-related activities. These include completing a set of courses or parts of user profiles. Site badges can also be issued manually by one user to another. @@ -549,3 +567,8 @@ $string['signinwithyouremail'] = 'Sign in with your email'; // Deprecated since Moodle 3.6. $string['error:invalidbadgeurl'] = 'Invalid issuer URL format. The URL should have a prefix http:// or https://.'; +$string['backpackbadges'] = 'You have {$a->totalbadges} badge(s) displayed from {$a->totalcollections} collection(s). Change backpack settings.'; +$string['error:nogroups'] = '

There are no public collections of badges available in your backpack.

+

Only public collections are shown, visit your backpack to create some public collections.

'; +$string['nobackpackbadges'] = 'There are no badges in the collections you have selected. Add more collections.'; +$string['nobackpackcollections'] = 'No badge collections have been selected. Add collections.'; diff --git a/lang/en/deprecated.txt b/lang/en/deprecated.txt index 428ca61b1d3..62f8f15734b 100644 --- a/lang/en/deprecated.txt +++ b/lang/en/deprecated.txt @@ -154,3 +154,7 @@ undockall,core_block undockblock,core_block undockitem,core_block canceledit,core_message +backpackbadges,core_badges +nobackpackbadges,core_badges +nobackpackcollections,core_badges +error:nogroups,core_badges diff --git a/lib/badgeslib.php b/lib/badgeslib.php index 4c1bb90ab8a..556dc1c6227 100644 --- a/lib/badgeslib.php +++ b/lib/badgeslib.php @@ -94,9 +94,13 @@ define('BADGE_MESSAGE_WEEKLY', 3); define('BADGE_MESSAGE_MONTHLY', 4); /* - * URL of backpack. Currently only the Open Badges backpack is supported. + * URL of backpack. Custom ones can be added. */ +define('BADGE_BACKPACKAPIURL', 'https://backpack.openbadges.org'); +define('BADGE_BACKPACKWEBURL', 'https://backpack.openbadges.org'); define('BADGE_BACKPACKURL', 'https://backpack.openbadges.org'); +define('BADGRIO_BACKPACKAPIURL', 'https://api.badgr.io/v2'); +define('BADGRIO_BACKPACKWEBURL', 'https://badgr.io'); /* * Open Badges specifications. @@ -114,881 +118,8 @@ define('OPEN_BADGES_V2_TYPE_ISSUER', 'Issuer'); define('OPEN_BADGES_V2_TYPE_ENDORSEMENT', 'Endorsement'); define('OPEN_BADGES_V2_TYPE_AUTHOR', 'Author'); -/** - * Class that represents badge. - * - */ -class badge { - /** @var int Badge id */ - public $id; - - /** Values from the table 'badge' */ - public $name; - public $description; - public $timecreated; - public $timemodified; - public $usercreated; - public $usermodified; - public $issuername; - public $issuerurl; - public $issuercontact; - public $expiredate; - public $expireperiod; - public $type; - public $courseid; - public $message; - public $messagesubject; - public $attachment; - public $notification; - public $status = 0; - public $nextcron; - - /** @var string control version of badge */ - public $version; - - /** @var string language code */ - public $language; - - /** @var string name image author */ - public $imageauthorname; - - /** @var string email image author */ - public $imageauthoremail; - - /** @var string url image author */ - public $imageauthorurl; - - /** @var string image caption */ - 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; - } - } - - $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 bool 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); - } - - // Copy endorsement. - $endorsement = $this->get_endorsement(); - if ($endorsement) { - unset($endorsement->id); - $endorsement->badgeid = $new; - $newbadge->save_endorsement($endorsement); - } - - // Copy alignments. - $alignments = $this->get_alignments(); - foreach ($alignments as $alignment) { - unset($alignment->id); - $alignment->badgeid = $new; - $newbadge->save_alignment($alignment); - } - - // Copy related badges. - $related = $this->get_related_badges(true); - if (!empty($related)) { - $newbadge->add_related_badges(array_keys($related)); - } - - // 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 bool 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. - * - */ - 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 bool 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 bool 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. - * - */ - 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 bool $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 bool 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 bool 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 bool 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 $archive bool 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, alignments 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 bool 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 bool 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 bool $activeonly Do not get the inactive badges when is true. - * @return array Related badges information. - */ - public function get_related_badges(bool $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 bool 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)); - } - - /** - * Get alignments of badge. - * - * @deprecated since Moodle 3.7 see MDL-63876 - * @return array List content alignments. - */ - public function get_alignment() { - debugging('Use of get_alignment is deprecated. Call get_alignments instead.', DEBUG_DEVELOPER); - - return $this->get_alignments(); - } - - /** - * 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'] = $issuerurl->out(false); - $issuer['type'] = OPEN_BADGES_V2_TYPE_ISSUER; - return $issuer; - } -} +// Global badge class has been moved to the component namespace. +class_alias('\core_badges\badge', 'badge'); /** * Sends notifications to users about awarded badges. @@ -1413,7 +544,6 @@ function badges_bake($hash, $badgeid, $userid = 0, $pathhash = false) { */ function get_backpack_settings($userid, $refresh = false) { global $DB; - require_once(__DIR__ . '/../badges/lib/backpacklib.php'); // Try to get badges from cache first. $badgescache = cache::make('core', 'externalbadges'); @@ -1424,19 +554,20 @@ function get_backpack_settings($userid, $refresh = false) { // Get badges through curl request to the backpack. $record = $DB->get_record('badge_backpack', array('userid' => $userid)); if ($record) { - $backpack = new OpenBadgesBackpackHandler($record); + $sitebackpack = badges_get_site_backpack(0, $record->backpackurl); + $backpack = new \core_badges\backpack_api($sitebackpack, $record); $out = new stdClass(); - $out->backpackurl = $backpack->get_url(); + $out->backpackid = $sitebackpack->id; if ($collections = $DB->get_records('badge_external', array('backpackid' => $record->id))) { $out->totalcollections = count($collections); $out->totalbadges = 0; $out->badges = array(); foreach ($collections as $collection) { - $badges = $backpack->get_badges($collection->collectionid); - if (isset($badges->badges)) { - $out->badges = array_merge($out->badges, $badges->badges); - $out->totalbadges += count($badges->badges); + $badges = $backpack->get_badges($collection, true); + if (!empty($badges)) { + $out->badges = array_merge($out->badges, $badges); + $out->totalbadges += count($badges); } else { $out->badges = array_merge($out->badges, array()); } @@ -1500,6 +631,10 @@ function badges_check_backpack_accessibility() { return 'available'; } + if (badges_open_badges_backpack_api() == OPEN_BADGES_V2) { + return 'available'; + } + global $CFG; include_once $CFG->libdir . '/filelib.php'; @@ -1581,12 +716,357 @@ function badges_handle_course_deletion($courseid) { function badges_setup_backpack_js() { global $CFG, $PAGE; if (!empty($CFG->badges_allowexternalbackpack)) { - $PAGE->requires->string_for_js('error:backpackproblem', 'badges'); - $PAGE->requires->js(new moodle_url(BADGE_BACKPACKURL . '/issuer.js'), true); - $PAGE->requires->js('/badges/backpack.js', true); + if (badges_open_badges_backpack_api() == OPEN_BADGES_V1) { + $PAGE->requires->string_for_js('error:backpackproblem', 'badges'); + $PAGE->requires->js(new moodle_url(BADGE_BACKPACKURL . '/issuer.js'), true); + $PAGE->requires->js('/badges/backpack.js', true); + } } } +/** + * No js files are required for backpack support. + * This only exists to directly support the custom V1 backpack api. + * + * @param boolean $checksite Call check site function. + * @return void + */ +function badges_local_backpack_js($checksite = false) { + global $CFG, $PAGE; + if (!empty($CFG->badges_allowexternalbackpack)) { + if (badges_open_badges_backpack_api() == OPEN_BADGES_V1) { + $PAGE->requires->js('/badges/backpack.js', true); + if ($checksite) { + $PAGE->requires->js_init_call('check_site_access', null, false); + } + } + } +} + +/** + * Create the backpack with this data. + * + * @param stdClass $data The new backpack data. + * @return boolean + */ +function badges_create_site_backpack($data) { + global $DB; + $context = context_system::instance(); + require_capability('moodle/badges:manageglobalsettings', $context); + + $count = $DB->count_records('badge_external_backpack'); + + $backpack = new stdClass(); + $backpack->apiversion = $data->apiversion; + $backpack->backpackapiurl = $data->backpackapiurl; + $backpack->backpackweburl = $data->backpackweburl; + $backpack->sortorder = $count; + $DB->insert_record('badge_external_backpack', $backpack); + return true; +} + +/** + * Update the backpack with this id. + * + * @param integer $id The backpack to edit + * @param stdClass $data The new backpack data. + * @return boolean + */ +function badges_update_site_backpack($id, $data) { + global $DB; + $context = context_system::instance(); + require_capability('moodle/badges:manageglobalsettings', $context); + + if ($backpack = badges_get_site_backpack($id)) { + $backpack = new stdClass(); + $backpack->id = $id; + $backpack->apiversion = $data->apiversion; + $backpack->backpackweburl = $data->backpackweburl; + $backpack->backpackapiurl = $data->backpackapiurl; + $backpack->password = $data->password; + $DB->update_record('badge_external_backpack', $backpack); + return true; + } + return false; +} + +/** + * Is any backpack enabled that supports open badges V1? + * @return boolean + */ +function badges_open_badges_backpack_api() { + global $CFG; + + $backpack = badges_get_site_backpack($CFG->badges_site_backpack); + + if (empty($backpack->apiversion)) { + return OPEN_BADGES_V2; + } + return $backpack->apiversion; +} + +/** + * Get a site backpacks by id or url. + * + * @param int $id The backpack id. + * @param string $backpackurl The backpack url. + * @param string $apiversion Filter by api version - one of OPEN_BADGES_V1 or OPEN_BADGES_V2 + * @return array(stdClass) + */ +function badges_get_site_backpack($id, $backpackapiurl = '', $apiversion = '') { + global $DB; + + $filters = array(); + if (!empty($id)) { + $filters['id'] = $id; + } + if (!empty($backpackapiurl)) { + $filters['backpackapiurl'] = $backpackapiurl; + } + if (!empty($apiversion)) { + $filters['apiversion'] = $apiversion; + } + + return $DB->get_record('badge_external_backpack', $filters); +} + +/** + * List the backpacks at site level. + * + * @return array(stdClass) + */ +function badges_get_site_backpacks() { + global $DB, $CFG; + + $all = $DB->get_records('badge_external_backpack'); + + foreach ($all as $key => $bp) { + if ($bp->id == $CFG->badges_site_backpack) { + $all[$key]->sitebackpack = true; + } else { + $all[$key]->sitebackpack = false; + } + } + return $all; +} + +/** + * List the supported badges api versions. + * + * @return array(version) + */ +function badges_get_badge_api_versions() { + return [ + OPEN_BADGES_V1 => get_string('openbadgesv1', 'badges'), + OPEN_BADGES_V2 => get_string('openbadgesv2', 'badges') + ]; +} + +/** + * Called on install or upgrade to create default list of backpacks a user can connect to. + * + * @return void + */ +function badges_install_default_backpacks() { + global $DB; + + $record = new stdClass(); + $record->backpackweburl = BADGE_BACKPACKWEBURL; + $record->backpackapiurl = BADGE_BACKPACKAPIURL; + $record->apiversion = OPEN_BADGES_V1; + $record->sortorder = 0; + $record->password = ''; + + $bpid = 0; + if (!($bp = $DB->get_record('badge_external_backpack', array('backpackapiurl' => $record->backpackapiurl)))) { + $bpid = $DB->insert_record('badge_external_backpack', $record); + } else { + $bpid = $bp->id; + } + set_config('badges_site_backpack', $bpid); + + $record = new stdClass(); + $record->backpackapiurl = BADGRIO_BACKPACKAPIURL; + $record->backpackweburl = BADGRIO_BACKPACKWEBURL; + $record->apiversion = OPEN_BADGES_V2; + $record->sortorder = 1; + $record->password = ''; + + if (!$DB->record_exists('badge_external_backpack', array('backpackapiurl' => $record->backpackapiurl))) { + $DB->insert_record('badge_external_backpack', $record); + } + +} + +/** + * Get the default issuer for a badge from this site. + * + * @return array + */ +function badges_get_default_issuer() { + global $CFG; + + $issuer = array(); + $issuerurl = new moodle_url('/badges/issuer.php'); + $issuer['name'] = $CFG->badges_defaultissuername; + $issuer['url'] = $issuerurl; + $issuer['email'] = $CFG->badges_defaultissuercontact; + $issuer['@context'] = OPEN_BADGES_V2_CONTEXT; + $issuer['id'] = $issuerurl->out(false); + $issuer['type'] = OPEN_BADGES_V2_TYPE_ISSUER; + return $issuer; +} + +/** + * Disconnect from the user backpack by deleting the user preferences. + * + * @param integer $userid The user to diconnect. + * @return boolean + */ +function badges_disconnect_user_backpack($userid) { + global $USER; + + // We can only change backpack settings for our own real backpack. + if ($USER->id != $userid || + \core\session\manager::is_loggedinas()) { + + return false; + } + + unset_user_preference('badges_email_verify_secret'); + unset_user_preference('badges_email_verify_address'); + unset_user_preference('badges_email_verify_backpackid'); + unset_user_preference('badges_email_verify_password'); + + return true; +} + +/** + * Used to remember which objects we connected with a backpack before. + * + * @param integer $sitebackpackid The site backpack to connect to. + * @param string $type The type of this remote object. + * @param string $internalid The id for this object on the Moodle site. + * @return mixed The id or false if it doesn't exist. + */ +function badges_external_get_mapping($sitebackpackid, $type, $internalid) { + global $DB; + // Return externalid if it exists. + $params = [ + 'sitebackpackid' => $sitebackpackid, + 'type' => $type, + 'internalid' => $internalid + ]; + + $record = $DB->get_record('badge_external_identifier', $params, 'externalid', IGNORE_MISSING); + if ($record) { + return $record->externalid; + } + return false; +} + +/** + * Save the info about which objects we connected with a backpack before. + * + * @param integer $sitebackpackid The site backpack to connect to. + * @param string $type The type of this remote object. + * @param string $internalid The id for this object on the Moodle site. + * @param string $externalid The id of this object on the remote site. + * @return boolean + */ +function badges_external_create_mapping($sitebackpackid, $type, $internalid, $externalid) { + global $DB; + + $params = [ + 'sitebackpackid' => $sitebackpackid, + 'type' => $type, + 'internalid' => $internalid, + 'externalid' => $externalid + ]; + + return $DB->insert_record('badge_external_identifier', $params); +} + +/** + * Delete all external mapping information for a backpack. + * + * @param integer $sitebackpackid The site backpack to connect to. + * @return boolean + */ +function badges_external_delete_mappings($sitebackpackid) { + global $DB; + + $params = ['sitebackpackid' => $sitebackpackid]; + + return $DB->delete_records('badge_external_identifier', $params); +} + +/** + * Delete a specific external mapping information for a backpack. + * + * @param integer $sitebackpackid The site backpack to connect to. + * @param string $type The type of this remote object. + * @param string $internalid The id for this object on the Moodle site. + * @return boolean + */ +function badges_external_delete_mapping($sitebackpackid, $type, $internalid) { + global $DB; + + $params = [ + 'sitebackpackid' => $sitebackpackid, + 'type' => $type, + 'internalid' => $internalid + ]; + + $DB->delete_record('badge_external_identifier', $params); +} + +/** + * Create and send a verification email to the email address supplied. + * + * Since we're not sending this email to a user, email_to_user can't be used + * but this function borrows largely the code from that process. + * + * @param string $email the email address to send the verification email to. + * @param int $backpackid the id of the backpack to connect to + * @param string $password the user entered password to connect to this backpack + * @return true if the email was sent successfully, false otherwise. + */ +function badges_send_verification_email($email, $backpackid, $backpackpassword) { + global $DB, $USER; + + // Store a user secret (badges_email_verify_secret) and the address (badges_email_verify_address) as users prefs. + // The address will be used by edit_backpack_form for display during verification and to facilitate the resending + // of verification emails to said address. + $secret = random_string(15); + set_user_preference('badges_email_verify_secret', $secret); + set_user_preference('badges_email_verify_address', $email); + set_user_preference('badges_email_verify_backpackid', $backpackid); + set_user_preference('badges_email_verify_password', $backpackpassword); + + // To, from. + $tempuser = $DB->get_record('user', array('id' => $USER->id), '*', MUST_EXIST); + $tempuser->email = $email; + $noreplyuser = core_user::get_noreply_user(); + + // Generate the verification email body. + $verificationurl = '/badges/backpackemailverify.php'; + $verificationurl = new moodle_url($verificationurl); + $verificationpath = $verificationurl->out(false); + + $site = get_site(); + $args = new stdClass(); + $args->link = $verificationpath . '?data='. $secret; + $args->sitename = $site->fullname; + $args->admin = generate_email_signoff(); + + $messagesubject = get_string('backpackemailverifyemailsubject', 'badges', $site->fullname); + $messagetext = get_string('backpackemailverifyemailbody', 'badges', $args); + $messagehtml = text_to_html($messagetext, false, false, true); + + return email_to_user($tempuser, $noreplyuser, $messagesubject, $messagetext, $messagehtml); +} + /** * Return all the enabled criteria types for this site. * diff --git a/lib/db/install.php b/lib/db/install.php index 8719b1337d2..59cebd4b3ed 100644 --- a/lib/db/install.php +++ b/lib/db/install.php @@ -319,4 +319,7 @@ function xmldb_main_install() { require_once($CFG->libdir . '/db/upgradelib.php'); make_default_scale(); make_competence_scale(); + + require_once($CFG->libdir . '/badgeslib.php'); + badges_install_default_backpacks(); } diff --git a/lib/db/install.xml b/lib/db/install.xml old mode 100644 new mode 100755 index e901acb9eae..078dcd4ecaa --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -1,5 +1,5 @@ - @@ -3171,6 +3171,7 @@ + @@ -3182,12 +3183,27 @@ + + + + + + + + + + + + + + +
@@ -3216,6 +3232,21 @@
+ + + + + + + + + + + + + + +
diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php old mode 100644 new mode 100755 index 744561c125d..78cc4c3b632 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -3301,5 +3301,73 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2019042700.01); } + if ($oldversion < 2019050100.01) { + + // Define field apiversion to be added to badge_backpack. + $table = new xmldb_table('badge_backpack'); + $field = new xmldb_field('apiversion', XMLDB_TYPE_CHAR, '12', null, XMLDB_NOTNULL, null, '1.0', 'password'); + + // Conditionally launch add field apiversion. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Define table badge_external_backpack to be created. + $table = new xmldb_table('badge_external_backpack'); + + // Adding fields to table badge_external_backpack. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('backpackapiurl', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + $table->add_field('backpackweburl', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + $table->add_field('apiversion', XMLDB_TYPE_CHAR, '12', null, XMLDB_NOTNULL, null, '1.0'); + $table->add_field('sortorder', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('password', XMLDB_TYPE_CHAR, '255', null, null, null, null); + + // Adding keys to table badge_external_backpack. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + $table->add_key('backpackapiurlkey', XMLDB_KEY_UNIQUE, ['backpackapiurl']); + $table->add_key('backpackweburlkey', XMLDB_KEY_UNIQUE, ['backpackweburl']); + + // Conditionally launch create table for badge_external_backpack. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Define field entityid to be added to badge_external. + $table = new xmldb_table('badge_external'); + $field = new xmldb_field('entityid', XMLDB_TYPE_CHAR, '255', null, null, null, null, 'collectionid'); + + // Conditionally launch add field entityid. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Add default backpacks. + require_once($CFG->libdir.'/badgeslib.php'); // Core Upgrade-related functions for badges only. + badges_install_default_backpacks(); + + // Define table badge_external_identifier to be created. + $table = new xmldb_table('badge_external_identifier'); + + // Adding fields to table badge_external_identifier. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('sitebackpackid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('internalid', XMLDB_TYPE_CHAR, '128', null, XMLDB_NOTNULL, null, null); + $table->add_field('externalid', XMLDB_TYPE_CHAR, '128', null, XMLDB_NOTNULL, null, null); + $table->add_field('type', XMLDB_TYPE_CHAR, '16', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table badge_external_identifier. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + $table->add_key('fk_backpackid', XMLDB_KEY_FOREIGN, ['sitebackpackid'], 'badge_backpack', ['id']); + $table->add_key('backpack-internal-external', XMLDB_KEY_UNIQUE, ['sitebackpackid', 'internalid', 'externalid', 'type']); + + // Conditionally launch create table for badge_external_identifier. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + // Main savepoint reached. + upgrade_main_savepoint(true, 2019050100.01); + } + return true; } diff --git a/lib/setuplib.php b/lib/setuplib.php index dbfa05e5e59..fa14b7de350 100644 --- a/lib/setuplib.php +++ b/lib/setuplib.php @@ -1407,7 +1407,7 @@ function disable_output_buffering() { */ function is_major_upgrade_required() { global $CFG; - $lastmajordbchanges = 2019041000.03; + $lastmajordbchanges = 2019050100.01; $required = empty($CFG->version); $required = $required || (float)$CFG->version < $lastmajordbchanges; diff --git a/version.php b/version.php index e838d63b97e..67f62b46f3c 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2019050100.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2019050100.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes.