Merge branch 'MDL-65959-310' of git://github.com/peterRd/moodle into MOODLE_310_STABLE
authorSara Arjona <sara@moodle.com>
Mon, 26 Oct 2020 21:44:05 +0000 (22:44 +0100)
committerSara Arjona <sara@moodle.com>
Mon, 26 Oct 2020 21:44:05 +0000 (22:44 +0100)
20 files changed:
badges/backpack-add.php
badges/backpack-connect.php
badges/backpack-export.php
badges/backpackemailverify.php
badges/backpacks.php
badges/classes/backpack_api.php
badges/classes/backpack_api_mapping.php
badges/classes/form/backpack.php
badges/classes/form/external_backpack.php
badges/mybackpack.php
badges/renderer.php
badges/tests/badgeslib_test.php
badges/tests/behat/backpack.feature
badges/upgrade.txt
lang/en/badges.php
lang/en/deprecated.txt
lib/badgeslib.php
lib/db/install.xml
lib/db/upgrade.php
version.php

index 9c57520..2e2478b 100644 (file)
 require_once(__DIR__ . '/../config.php');
 require_once($CFG->libdir . '/badgeslib.php');
 
-if (badges_open_badges_backpack_api() != OPEN_BADGES_V2) {
+require_login();
+
+$userbackpack = badges_get_user_backpack();
+if (badges_open_badges_backpack_api($userbackpack->id) != 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));
@@ -44,53 +45,111 @@ if (!empty($issuedbadge->recipient->id)) {
     // * Create badge
     // * Create assertion (Award the badge!)
 
+    // With the introduction OBv2.1 and MDL-65959 to allow cross region Badgr imports the above (old) procedure will
+    // only be completely performed if both the site and user backpacks conform to the same apiversion.
+    // Else we will attempt at pushing the assertion to the user's backpack. In this case, the id set against the assertion
+    // has to be a publicly accessible resource.
+
     // 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($backpack->externalbackpackid);
-    $assertion = new core_badges_assertion($id, $sitebackpack->apiversion);
-    $api = new \core_badges\backpack_api($sitebackpack);
-    $api->authenticate();
+    $userbackpack = badges_get_site_backpack($backpack->externalbackpackid, $USER->id);
+    $assertion = new core_badges_assertion($id, OPEN_BADGES_V2);
+    $assertiondata = $assertion->get_badge_assertion(false, false);
+    $assertionid = $assertion->get_assertion_hash();
+    $assertionentityid = $assertiondata['id'];
+    $badgeadded = false;
+    if (badges_open_badges_backpack_api() == OPEN_BADGES_V2) {
+        $sitebackpack = badges_get_site_primary_backpack();
+        $api = new \core_badges\backpack_api($sitebackpack);
+        $response = $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);
-    }
+        // A numeric response indicates a valid successful authentication. Else an error object will be returned.
+        if (is_numeric($response)) {
+            // 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);
+            // Create assertion (Award the badge!).
+            $assertionentityid = badges_external_get_mapping(
+                $sitebackpack->id,
+                OPEN_BADGES_V2_TYPE_ASSERTION,
+                $assertionid
+            );
 
-    $assertionid = $assertion->get_assertion_hash();
+            if (!$assertionentityid && strpos($sitebackpack->backpackapiurl, 'badgr')) {
+                $assertionentityid = badges_generate_badgr_open_url(
+                    $sitebackpack,
+                    OPEN_BADGES_V2_TYPE_ASSERTION,
+                    $assertionentityid
+                );
+            }
 
-    if (!($assertionentityid = badges_external_get_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_ASSERTION, $assertionid))) {
-        $response = $api->put_badgeclass_assertion($badgeentityid, $assertiondata);
+            // Create an assertion for the recipient in the issuer's account.
+            if (!$assertionentityid) {
+                $response = $api->put_badgeclass_assertion($badgeentityid, $assertiondata);
+                if (!$response) {
+                    throw new moodle_exception('invalidrequest', 'error');
+                }
+                $assertionentityid = badges_generate_badgr_open_url($sitebackpack, OPEN_BADGES_V2_TYPE_ASSERTION, $response->id);
+                $badgeadded = true;
+                badges_external_create_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_ASSERTION, $assertionid,
+                    $response->id);
+            } else {
+                // An assertion already exists. Make sure it's up to date.
+                $internalid = badges_external_get_mapping(
+                    $sitebackpack->id,
+                    OPEN_BADGES_V2_TYPE_ASSERTION,
+                    $assertionid,
+                    'externalid'
+                );
+                $response = $api->update_assertion($internalid, $assertiondata);
+                if (!$response) {
+                    throw new moodle_exception('invalidrequest', 'error');
+                }
+            }
+        }
+    }
+
+    // Now award/upload the badge to the user's account.
+    // - If a user and site backpack have the same provider we can skip this as Badgr automatically maps recipients
+    // based on email address.
+    // - This is only needed when the backpacks are from different regions.
+    if ($assertionentityid && !badges_external_get_mapping($userbackpack->id, OPEN_BADGES_V2_TYPE_ASSERTION, $assertionid)) {
+        $userapi = new \core_badges\backpack_api($userbackpack, $backpack);
+        $userapi->authenticate();
+        $response = $userapi->import_badge_assertion($assertionentityid);
         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'];
+        $badgeadded = true;
+        badges_external_create_mapping($userbackpack->id, OPEN_BADGES_V2_TYPE_ASSERTION, $assertionid,
+            $assertionentityid);
     }
+
+    $response = $badgeadded ? ['success' => 'addedtobackpack'] : ['warning' => 'existsinbackpack'];
     redirect(new moodle_url('/badges/mybadges.php', $response));
 } else {
     redirect(new moodle_url('/badges/mybadges.php'));
index cece808..ff77f49 100644 (file)
 require_once(__DIR__ . '/../config.php');
 require_once($CFG->libdir . '/badgeslib.php');
 
+$backpackid = required_param('backpackid', PARAM_INT);
 $scope = optional_param('scope', '', PARAM_RAW);
 $action = optional_param('action', null, PARAM_RAW);
 
-if (badges_open_badges_backpack_api() != OPEN_BADGES_V2P1) {
+if (badges_open_badges_backpack_api($backpackid) != OPEN_BADGES_V2P1) {
     throw new coding_exception('backpacks only support Open Badges V2.1');
 }
 
 require_login();
 
-$externalbackpack = badges_get_site_backpack($CFG->badges_site_backpack);
+$externalbackpack = badges_get_site_backpack($backpackid);
 $persistedissuer = \core\oauth2\issuer::get_record(['id' => $externalbackpack->oauth2_issuerid]);
 if ($persistedissuer) {
     $issuer = new \core\oauth2\issuer($externalbackpack->oauth2_issuerid);
     $returnurl = new moodle_url('/badges/backpack-connect.php',
-        ['action' => 'authorization', 'sesskey' => sesskey()]);
+        ['action' => 'authorization', 'sesskey' => sesskey(), 'backpackid' => $backpackid]);
 
     $client = new core_badges\oauth2\client($issuer, $returnurl, $scope, $externalbackpack);
     if ($client) {
index e1015b7..60a0ace 100644 (file)
@@ -25,9 +25,6 @@
 require_once(__DIR__ . '/../config.php');
 require_once($CFG->libdir . '/badgeslib.php');
 
-if (badges_open_badges_backpack_api() != OPEN_BADGES_V2P1) {
-    throw new coding_exception('backpacks only support Open Badges V2.1');
-}
 $hash = optional_param('hash', null, PARAM_RAW);
 
 $PAGE->set_pagelayout('admin');
@@ -37,7 +34,12 @@ require_login();
 if (empty($CFG->badges_allowexternalbackpack) || empty($CFG->enablebadges)) {
     redirect($CFG->wwwroot);
 }
-$backpack = badges_get_site_backpack($CFG->badges_site_backpack);
+
+$backpack = badges_get_user_backpack();
+if (badges_open_badges_backpack_api($backpack->id) != OPEN_BADGES_V2P1) {
+    throw new coding_exception('backpacks only support Open Badges V2.1');
+}
+
 $userbadges = badges_get_user_badges($USER->id);
 $context = context_user::instance($USER->id);
 
@@ -50,7 +52,6 @@ $PAGE->set_pagelayout('standard');
 
 $redirecturl = new moodle_url('/badges/mybadges.php');
 if ($hash) {
-    $backpack = badges_get_site_backpack($CFG->badges_site_backpack);
     $api = new core_badges\backpack_api2p1($backpack);
     $notify = $api->put_assertions($hash);
     if (!empty($notify['status']) && $notify['status'] == \core\output\notification::NOTIFY_SUCCESS) {
index 9b563ff..73edaa4 100644 (file)
@@ -54,19 +54,21 @@ if (!is_null($storedsecret)) {
                 null, \core\output\notification::NOTIFY_ERROR);
         }
 
-        $obj = new stdClass();
-        $obj->userid = $USER->id;
-        $obj->email = $data->email;
-        $obj->externalbackpackid = $backpackid;
-        $obj->backpackuid = $backpackuid;
-        $obj->autosync = 0;
-        $obj->password = $password;
-
-        $DB->insert_record('badge_backpack', $obj);
+        $values = [
+            'userid' => $USER->id,
+            'backpackemail' => $data->email,
+            'externalbackpackid' => $backpackid,
+            'backpackuid' => $backpackuid,
+            'autosync' => 0,
+            'password' => $password
+        ];
+        badges_save_backpack_credentials((object) $values);
 
         // Remove the verification vars and redirect to the mypackpack page.
         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');
         redirect(new moodle_url($redirect), get_string('backpackemailverifysuccess', 'badges'),
             null, \core\output\notification::NOTIFY_SUCCESS);
     } else {
index 74beadd..635a164 100644 (file)
@@ -69,7 +69,11 @@ if ($action == 'edit') {
     } else if ($data = $form->get_data()) {
         require_sesskey();
         if (!empty($data->id)) {
-            badges_update_site_backpack($data->id, $data);
+            $id = $data->id;
+            badges_update_site_backpack($id, $data);
+            // Apart from the password, any change here would result in an error in other parts of the badge systems.
+            // In order to negate this, we restart any further mapping from scratch.
+            badges_external_delete_mappings($id);
         } else {
             badges_create_site_backpack($data);
         }
index 0916dea..e0db003 100644 (file)
@@ -87,17 +87,13 @@ class backpack_api {
         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 : '';
+        $this->email = $sitebackpack->backpackemail;
         $this->isuserbackpack = false;
         $this->backpackid = $sitebackpack->id;
         if (!empty($userbackpack)) {
-            if ($userbackpack->externalbackpackid != $sitebackpack->id) {
-                throw new coding_exception('Incorrect backpack');
-            }
             $this->isuserbackpack = true;
             $this->password = $userbackpack->password;
             $this->email = $userbackpack->email;
@@ -152,6 +148,21 @@ class backpack_api {
                     true,                                       // JSON Encoded.
                     true                                        // Auth required.
                 ];
+                $mapping[] = [
+                    'importbadge',                                // Action.
+                    // Badgr.io does not return the public information about a badge
+                    // if the issuer is associated with another user. We need to pass
+                    // the expand parameters which are not in any specification to get
+                    // additional information about the assertion in a single request.
+                    '[URL]/backpack/import',
+                    ['url' => '[PARAM]'],  // Post params.
+                    '',                                             // Request exporter.
+                    'core_badges\external\assertion_exporter',      // Response exporter.
+                    false,                                          // Multiple.
+                    'post',                                         // Method.
+                    true,                                           // JSON Encoded.
+                    true                                            // Auth required.
+                ];
                 $mapping[] = [
                     'badges',                                   // Action.
                     '[URL]/backpack/collections/[PARAM1]',      // URL
@@ -214,6 +225,17 @@ class backpack_api {
                     true,                                       // JSON Encoded.
                     true                                        // Auth required.
                 ];
+                $mapping[] = [
+                    'updateassertion',                                // Action.
+                    '[URL]/assertions/[PARAM2]?expand=badgeclass&expand=issuer',
+                    '[PARAM]',                                  // Post params.
+                    'core_badges\external\assertion_exporter', // Request exporter.
+                    'core_badges\external\assertion_exporter', // Response exporter.
+                    false,                                      // Multiple.
+                    'put',                                     // Method.
+                    true,                                       // JSON Encoded.
+                    true                                        // Auth required.
+                ];
                 foreach ($mapping as $map) {
                     $map[] = false; // Site api function.
                     $map[] = OPEN_BADGES_V2; // V2 function.
@@ -408,6 +430,38 @@ class backpack_api {
         return $this->curl_request('assertions', null, $entityid, $data);
     }
 
+    /**
+     * Update a badgeclass assertion.
+     *
+     * @param string $entityid The id of the badge class.
+     * @param array $data The structure of the badge class assertion.
+     * @return mixed
+     */
+    public function update_assertion(string $entityid, array $data) {
+        // V2 Only.
+        if ($this->backpackapiversion == OPEN_BADGES_V1) {
+            throw new coding_exception('Not supported in this backpack API');
+        }
+
+        return $this->curl_request('updateassertion', null, $entityid, $data);
+    }
+
+    /**
+     * Import a badge assertion into a backpack. This is used to handle cross domain backpacks.
+     *
+     * @param string $data The structure of the badge class assertion.
+     * @return mixed
+     * @throws coding_exception
+     */
+    public function import_badge_assertion(string $data) {
+        // V2 Only.
+        if ($this->backpackapiversion == OPEN_BADGES_V1) {
+            throw new coding_exception('Not supported in this backpack API');
+        }
+
+        return $this->curl_request('importbadge', null, null, $data);
+    }
+
     /**
      * Select collections from a backpack.
      *
index efd57c4..aa9155c 100644 (file)
@@ -209,6 +209,8 @@ class backpack_api_mapping {
                 } else if ($value == '[PASSWORD]') {
                     $value = $password;
                     $request[$key] = $value;
+                } else if ($value == '[PARAM]') {
+                    $request[$key] = is_array($param) ? $param[0] : $param;
                 }
             }
         }
@@ -312,6 +314,7 @@ class backpack_api_mapping {
         return array(
             'FRESH_CONNECT'     => true,
             'RETURNTRANSFER'    => true,
+            'FOLLOWLOCATION'    => true,
             'FORBID_REUSE'      => true,
             'HEADER'            => 0,
             'CONNECTTIMEOUT'    => 3,
@@ -359,6 +362,8 @@ class backpack_api_mapping {
             $response = $curl->get($url, $post, $options);
         } else if ($this->method == 'post') {
             $response = $curl->post($url, $post, $options);
+        } else if ($this->method == 'put') {
+            $response = $curl->put($url, $post, $options);
         }
         $response = json_decode($response);
         if (isset($response->result)) {
index 6c7370c..6310b4c 100644 (file)
@@ -39,7 +39,7 @@ use stdClass;
  * Form to edit backpack initial details.
  *
  */
-class backpack extends moodleform {
+class backpack extends external_backpack {
 
     /**
      * Defines the form
@@ -47,30 +47,66 @@ class backpack extends moodleform {
     public function definition() {
         global $USER, $PAGE, $OUTPUT, $CFG;
         $mform = $this->_form;
+        $this->_customdata['userbackpack'] = 1;
 
         $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('hidden', 'userid', $USER->id);
         $mform->setType('userid', PARAM_INT);
-        $sitebackpack = badges_get_site_backpack($CFG->badges_site_backpack);
-
+        $freeze = [];
         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);
+            $freeze = ['backpackemail'];
+            $mform->addElement('hidden', 'password', $this->_customdata['backpackpassword']);
+            $mform->setType('password', PARAM_RAW);
+            $mform->addElement('hidden', 'externalbackpackid', $this->_customdata['backpackid']);
+            $mform->setType('externalbackpackid', 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);
-            $mform->addElement('hidden', 'email', $this->_customdata['email']);
-            $mform->setType('email', PARAM_EMAIL);
-            $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);
+        } else {
+            $sitebackpacks = badges_get_site_backpacks();
+            $choices = [];
+            $restrictedoptions = [];
+            foreach ($sitebackpacks as $backpack) {
+                $choices[$backpack->id] = $backpack->backpackweburl;
+                if ($backpack->apiversion == OPEN_BADGES_V2P1) {
+                    $restrictedoptions[] = $backpack->id;
+                }
+            }
+            $mform->addElement('select', 'externalbackpackid', get_string('backpackprovider', 'badges'), $choices);
+            $mform->setType('externalbackpackid', PARAM_INT);
+            $mform->setDefault('externalbackpackid', $CFG->badges_site_backpack);
+            $mform->hideIf('password', 'externalbackpackid', 'in', $restrictedoptions);
+            $mform->hideIf('backpackemail', 'externalbackpackid', 'in', $restrictedoptions);
+
+            $status = html_writer::tag('span', get_string('notconnected', 'badges'),
+                array('class' => 'notconnected', 'id' => 'connection-status'));
+        }
+        $mform->addElement('static', 'status', get_string('status'), $status);
+
+        $this->add_auth_fields($this->_customdata['email'] ?? $USER->email, !isset($this->_customdata['email']));
+
+        $mform->setDisableShortforms(false);
+
+        // Freeze any elemnts after definition.
+        if ($freeze) {
+            $mform->freeze($freeze);
+        }
+        $this->add_action_buttons();
+    }
+
+    /**
+     * Override add_action_buttons
+     *
+     * @param bool $cancel
+     * @param null|text $submitlabel
+     */
+    public function add_action_buttons($cancel = true, $submitlabel = null) {
+        $mform = $this->_form;
+        if (isset($this->_customdata['email'])) {
             $buttonarray = [];
             $buttonarray[] = &$mform->createElement('submit', 'submitbutton',
                                                     get_string('backpackconnectionresendemail', 'badges'));
@@ -80,28 +116,7 @@ class backpack 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', get_string('backpackweburl', 'badges'), $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);
-            if (badges_open_badges_backpack_api() != OPEN_BADGES_V2P1) {
-                $mform->addElement('text', 'email', get_string('email'), 'maxlength="100" size="30"');
-                $mform->addHelpButton('email', 'backpackemail', 'badges');
-                $mform->addRule('email', get_string('required'), 'required', null, 'client');
-                $mform->setType('email', PARAM_EMAIL);
-                if (badges_open_badges_backpack_api() == OPEN_BADGES_V2) {
-                    $mform->addElement('passwordunmask', 'backpackpassword', get_string('password'));
-                    $mform->setType('backpackpassword', PARAM_RAW);
-                } else {
-                    $mform->addElement('hidden', 'backpackpassword', '');
-                    $mform->setType('backpackpassword', PARAM_RAW);
-                }
-            }
-            $this->add_action_buttons(false, get_string('backpackconnectionconnect', 'badges'));
+            parent::add_action_buttons(false, get_string('backpackconnectionconnect', 'badges'));
         }
     }
 
@@ -118,19 +133,18 @@ class backpack extends moodleform {
         // 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();
-            $backpack = badges_get_site_backpack($data['backpackid']);
-            $check->email = $data['email'];
-            $check->password = $data['backpackpassword'];
-            $check->externalbackpackid = $backpack->id;
+            $check->email = $data['backpackemail'];
+            $check->password = $data['password'];
+            $sitebackpack = badges_get_site_backpack($data['externalbackpackid']);
+            $bp = new \core_badges\backpack_api($sitebackpack, $check);
 
-            $bp = new \core_badges\backpack_api($backpack, $check);
             $result = $bp->authenticate();
             if ($result === false || !empty($result->error)) {
-                $errors['email'] = get_string('backpackconnectionunexpectedresult', 'badges');
+                $errors['backpackemail'] = get_string('backpackconnectionunexpectedresult', 'badges');
                 $msg = $bp->get_authentication_error();
                 if (!empty($msg)) {
-                    $errors['email'] .= '<br/><br/>';
-                    $errors['email'] .= get_string('backpackconnectionunexpectedmessage', 'badges', $msg);
+                    $errors['backpackemail'] .= '<br/><br/>';
+                    $errors['backpackemail'] .= get_string('backpackconnectionunexpectedmessage', 'badges', $msg);
                 }
             }
         }
index e5c495c..b5d10e0 100644 (file)
@@ -53,11 +53,6 @@ class external_backpack extends \moodleform {
         $mform->addElement('hidden', 'action', 'edit');
         $mform->setType('action', PARAM_ALPHA);
 
-        if ($backpack) {
-            $mform->addElement('hidden', 'id', $backpack->id);
-            $mform->setType('id', PARAM_INTEGER);
-        }
-
         $mform->addElement('text', 'backpackapiurl',  get_string('backpackapiurl', 'core_badges'));
         $mform->setType('backpackapiurl', PARAM_URL);
         $mform->addRule('backpackapiurl', null, 'required', null, 'client');
@@ -74,16 +69,22 @@ class external_backpack extends \moodleform {
         $mform->setDefault('apiversion', OPEN_BADGES_V2P1);
         $mform->addRule('apiversion', null, 'required', null, 'client');
 
-        $issuername = $CFG->badges_defaultissuername;
-        $mform->addElement('static', 'issuerinfo', get_string('defaultissuername', 'core_badges'), $issuername);
+        $mform->addElement('hidden', 'id', ($backpack->id ?? null));
+        $mform->setType('id', PARAM_INT);
+        $mform->addElement('hidden', 'badgebackpack', 0);
+        $mform->setType('badgebackpack', PARAM_INT);
+        $mform->addElement('hidden', 'userid', 0);
+        $mform->setType('userid', PARAM_INT);
+        $mform->addElement('hidden', 'backpackuid', 0);
+        $mform->setType('backpackuid', PARAM_INT);
+
+        $mform->addElement('advcheckbox', 'includeauthdetails', null, get_string('includeauthdetails', 'core_badges'));
+        if (!empty($backpack->backpackemail) || !empty($backpack->password)) {
+            $mform->setDefault('includeauthdetails', 1);
+        }
 
         $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', 'neq', 2);
+        $this->add_auth_fields($issuercontact);
 
         $oauth2options = badges_get_oauth2_service_options();
         $mform->addElement('select', 'oauth2_issuerid', get_string('oauth2issuer', 'core_badges'), $oauth2options);
@@ -94,6 +95,12 @@ class external_backpack extends \moodleform {
             $this->set_data($backpack);
         }
 
+        $mform->hideIf('includeauthdetails', 'apiversion', 'in', [OPEN_BADGES_V2P1]);
+        $mform->hideIf('backpackemail', 'includeauthdetails');
+        $mform->hideIf('backpackemail', 'apiversion', 'in', [OPEN_BADGES_V2P1]);
+        $mform->hideIf('password', 'includeauthdetails');
+        $mform->hideIf('password', 'apiversion', 'in', [1, OPEN_BADGES_V2P1]);
+
         // Disable short forms.
         $mform->setDisableShortforms();
 
@@ -120,4 +127,54 @@ class external_backpack extends \moodleform {
 
         return $errors;
     }
+
+    /**
+     * Return submitted data if properly submitted or returns NULL if validation fails or
+     * if there is no submitted data.
+     *
+     * @return object|void
+     */
+    public function get_data() {
+        $data = parent::get_data();
+        if ($data ) {
+            if ((isset($data->includeauthdetails) && !$data->includeauthdetails)
+                || (isset($data->apiversion) && $data->apiversion == 2.1)) {
+                $data->backpackemail = "";
+                $data->password = "";
+            }
+
+            if ((isset($data->apiversion) && $data->apiversion == 1)) {
+                $data->password = "";
+            }
+        }
+
+        return $data;
+    }
+
+    /**
+     * Add backpack specific auth details.
+     *
+     * @param string|null $email The email addressed provided or null if it's new.
+     * @param bool $includepassword Include the password field. Defaults to true
+     * @throws \coding_exception
+     */
+    protected function add_auth_fields(?string $email, bool $includepassword = true) {
+        $mform = $this->_form;
+        $emailstring = get_string('email');
+        $passwordstring = get_string('password');
+        if (!isset($this->_customdata['userbackpack'])) {
+            $emailstring = get_string('defaultissuercontact', 'core_badges');
+            $passwordstring = get_string('defaultissuerpassword', 'core_badges');
+        }
+
+        $mform->addElement('text', 'backpackemail', $emailstring);
+        $mform->setType('backpackemail', PARAM_EMAIL);
+        $mform->setDefault('backpackemail', $email);
+
+        if ($includepassword) {
+            $mform->addElement('passwordunmask', 'password', $passwordstring);
+            $mform->setType('password', PARAM_RAW);
+            $mform->addHelpButton('password', 'defaultissuerpassword', 'badges');
+        }
+    }
 }
index 61dd374..89c3ffd 100644 (file)
@@ -55,7 +55,7 @@ $badgescache = cache::make('core', 'externalbadges');
 
 if ($disconnect && $backpack) {
     require_sesskey();
-    $sitebackpack = badges_get_site_backpack($backpack->externalbackpackid);
+    $sitebackpack = badges_get_user_backpack();
     if ($sitebackpack->apiversion == OPEN_BADGES_V2P1) {
         $bp = new \core_badges\backpack_api2p1($sitebackpack);
         $bp->disconnect_backpack($backpack);
@@ -71,11 +71,7 @@ if ($disconnect && $backpack) {
 $warning = '';
 if ($backpack) {
 
-    $sitebackpack = badges_get_site_backpack($backpack->externalbackpackid);
-
-    if ($sitebackpack->id != $CFG->badges_site_backpack) {
-        $warning = $OUTPUT->notification(get_string('backpackneedsupdate', 'badges'), 'warning');
-    }
+    $sitebackpack = badges_get_user_backpack();
 
     // If backpack is connected, need to select collections.
     $bp = new \core_badges\backpack_api($sitebackpack, $backpack);
@@ -107,16 +103,6 @@ if ($backpack) {
         $bp->set_backpack_collections($backpack->id, $groups);
         redirect(new moodle_url('/badges/mybadges.php'));
     }
-} else if (badges_open_badges_backpack_api() == OPEN_BADGES_V2P1) {
-    // If backpack is version 2.1 to redirect on the backpack site to login.
-    // User input username/email/password on the backpack site
-    // After confirm the scopes.
-    $form = new \core_badges\form\backpack(new moodle_url('/badges/mybackpack.php'));
-    if ($form->is_cancelled()) {
-        redirect(new moodle_url('/badges/mybadges.php'));
-    } else if ($data = $form->get_submitted_data()) {
-        redirect(new moodle_url('/badges/backpack-connect.php'));
-    }
 } else {
     // If backpack is not connected, need to connect first.
     // To create a new connection to the backpack, first we need to verify the user's email address:
@@ -134,25 +120,33 @@ if ($backpack) {
     $params['backpackid'] = get_user_preferences('badges_email_verify_backpackid');
 
     $form = new \core_badges\form\backpack(new moodle_url('/badges/mybackpack.php'), $params);
+    $data = $form->get_submitted_data();
     if ($form->is_cancelled()) {
         redirect(new moodle_url('/badges/mybadges.php'));
-    } else if ($data = $form->get_data()) {
-        // The form may have been submitted under one of the following circumstances:
-        // 1. After clicking 'Connect to backpack'. We'll have $data->email.
-        // 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)) {
-            badges_disconnect_user_backpack($USER->id);
-            redirect(new moodle_url('/badges/mybackpack.php'));
-        } else if (isset($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);
-            } else {
-                print_error ('backpackcannotsendverification', 'badges');
+    } else if ($form->is_submitted()) {
+        if (badges_open_badges_backpack_api($data->externalbackpackid) == OPEN_BADGES_V2P1) {
+            // If backpack is version 2.1 to redirect on the backpack site to login.
+            // User input username/email/password on the backpack site
+            // After confirm the scopes.
+            redirect(new moodle_url('/badges/backpack-connect.php', ['backpackid' => $data->externalbackpackid]));
+        } else if ($data = $form->get_data()) {
+            // The form may have been submitted under one of the following circumstances:
+            // 1. After clicking 'Connect to backpack'. We'll have $data->email.
+            // 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)) {
+                badges_disconnect_user_backpack($USER->id);
+                redirect(new moodle_url('/badges/mybackpack.php'));
+            } else if (isset($data->backpackemail)) {
+                if (badges_send_verification_email($data->backpackemail, $data->externalbackpackid, $data->password)) {
+                    $a = get_user_preferences('badges_email_verify_backpackid');
+                    redirect(new moodle_url('/badges/mybackpack.php'),
+                        get_string('backpackemailverifypending', 'badges', $data->backpackemail),
+                        null, \core\output\notification::NOTIFY_INFO);
+                } else {
+                    print_error ('backpackcannotsendverification', 'badges');
+                }
             }
         }
     }
index 28a182b..b561bfe 100644 (file)
@@ -80,14 +80,14 @@ class core_badges_renderer extends plugin_renderer_base {
                     $params
                 );
                 $notexpiredbadge = (empty($badge->dateexpire) || $badge->dateexpire > time());
-                $backpackexists = badges_user_has_backpack($USER->id);
-                if (!empty($CFG->badges_allowexternalbackpack) && $notexpiredbadge && $backpackexists) {
+                $userbackpack = badges_get_user_backpack();
+                if (!empty($CFG->badges_allowexternalbackpack) && $notexpiredbadge && $userbackpack) {
                     $assertion = new moodle_url('/badges/assertion.php', array('b' => $badge->uniquehash));
                     $action = null;
-                    if (badges_open_badges_backpack_api() == OPEN_BADGES_V1) {
+                    if (badges_open_badges_backpack_api($userbackpack->id) == OPEN_BADGES_V1) {
                         $action = new component_action('click', 'addtobackpack', array('assertion' => $assertion->out(false)));
                         $addurl = new moodle_url('#');
-                    } else if (badges_open_badges_backpack_api() == OPEN_BADGES_V2P1) {
+                    } else if (badges_open_badges_backpack_api($userbackpack->id) == OPEN_BADGES_V2P1) {
                         $addurl = new moodle_url('/badges/backpack-export.php', array('hash' => $badge->uniquehash));
                     } else {
                         $addurl = new moodle_url('/badges/backpack-add.php', array('hash' => $badge->uniquehash));
@@ -342,9 +342,10 @@ class core_badges_renderer extends plugin_renderer_base {
                         new moodle_url('/badges/badge.php', array('hash' => $ibadge->hash, 'bake' => true)),
                         get_string('download'),
                         'POST');
-            if (!empty($CFG->badges_allowexternalbackpack) && ($expiration > $now) && badges_user_has_backpack($USER->id)) {
+            if (!empty($CFG->badges_allowexternalbackpack) && ($expiration > $now)
+                && $userbackpack = badges_get_user_backpack($USER->id)) {
 
-                if (badges_open_badges_backpack_api() == OPEN_BADGES_V1) {
+                if (badges_open_badges_backpack_api($userbackpack->id) == OPEN_BADGES_V1) {
                     $assertion = new moodle_url('/badges/assertion.php', array('b' => $ibadge->hash));
                     $action = new component_action('click', 'addtobackpack', array('assertion' => $assertion->out(false)));
                     $attributes = array(
@@ -356,7 +357,7 @@ class core_badges_renderer extends plugin_renderer_base {
                     $this->output->add_action_handler($action, 'addbutton');
                     $output .= $tobackpack;
                 } else {
-                    if (badges_open_badges_backpack_api() == OPEN_BADGES_V2P1) {
+                    if (badges_open_badges_backpack_api($userbackpack->id) == OPEN_BADGES_V2P1) {
                         $assertion = new moodle_url('/badges/backpack-export.php', array('hash' => $ibadge->hash));
                     } else {
                         $assertion = new moodle_url('/badges/backpack-add.php', array('hash' => $ibadge->hash));
@@ -650,10 +651,6 @@ 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 .= $this->output->notification(get_string('backpackneedsupdate', 'badges'), 'warning');
-
-                }
                 if ($backpack->totalcollections == 0) {
                     $externalhtml .= get_string('nobackpackcollectionssummary', 'badges', $backpack);
                 } else {
index fc80b9b..e8a6956 100644 (file)
@@ -915,4 +915,475 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
         $result = badges_delete_site_backpack($backpack->id);
         $this->assertFalse($result);
     }
+
+    /**
+     * Test to validate badges_save_backpack_credentials.
+     *
+     * @dataProvider save_backpack_credentials_provider
+     * @param  bool $addbackpack True if backpack data has to be created; false otherwise (empty data will be used then).
+     * @param  string|null  $mail  Backpack mail address.
+     * @param  string|null  $password  Backpack password.
+     */
+    public function test_save_backpack_credentials(bool $addbackpack = true, ?string $mail = null, ?string $password = null) {
+        global $DB;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $data = [];
+        if ($addbackpack) {
+            $data = new \stdClass();
+            $data->apiversion = OPEN_BADGES_V2P1;
+            $data->backpackapiurl = 'https://dc.imsglobal.org/obchost/ims/ob/v2p1';
+            $data->backpackweburl = 'https://dc.imsglobal.org';
+            badges_create_site_backpack($data);
+            $backpack = $DB->get_record('badge_external_backpack', ['backpackweburl' => $data->backpackweburl]);
+            $user = $this->getDataGenerator()->create_user();
+
+            $data = [
+                'externalbackpackid' => $backpack->id,
+                'userid' => $user->id,
+            ];
+
+            if (!empty($mail)) {
+                $data['backpackemail'] = $mail;
+            }
+            if (!empty($password)) {
+                $data['password'] = $password;
+            }
+        }
+
+        $return = badges_save_backpack_credentials((object) $data);
+        if (array_key_exists('userid', $data)) {
+            $record = $DB->get_record('badge_backpack', ['userid' => $user->id]);
+        } else {
+            $record = $DB->get_records('badge_backpack');
+        }
+
+        if (!empty($mail) && !empty($password)) {
+            // The backpack credentials are created if the given information is right.
+            $this->assertNotEmpty($record);
+            $this->assertEquals($data['externalbackpackid'], $return);
+        } else if ($addbackpack) {
+            // If no email and password are given, no backpack is created/modified.
+            $this->assertEmpty($record);
+            $this->assertEquals($data['externalbackpackid'], $return);
+        } else {
+            // There weren't fields to add to the backpack so no DB change is expected.
+            $this->assertEmpty($record);
+            $this->assertEquals(0, $return);
+        }
+
+        // Confirm the existing backpack credential can be updated (if it has been created).
+        if (!empty($record)) {
+            $data['backpackemail'] = 'modified_' . $mail;
+            $data['id'] = $record->id;
+            $return = badges_save_backpack_credentials((object) $data);
+            $record = $DB->get_record('badge_backpack', ['userid' => $user->id]);
+
+            $this->assertNotEmpty($record);
+            $this->assertEquals($data['backpackemail'], $record->email);
+            $this->assertEquals($data['externalbackpackid'], $return);
+        }
+    }
+
+    /**
+     * Data provider for test_create_backpack_credentials().
+     *
+     * @return array
+     */
+    public function save_backpack_credentials_provider(): array {
+        return [
+            'Empty fields' => [
+                false,
+            ],
+            'No backpack mail or password are defined' => [
+                true,
+            ],
+            'Both backpack mail and password are defined' => [
+                true, 'test@test.com', '1234',
+            ],
+            'Only backpack mail is defined (no password is given)' => [
+                true, 'test@test.com', null,
+            ],
+            'Only backpack password is defined (no mail is given)' => [
+                true, null, '1234'
+            ],
+        ];
+    }
+
+
+    /**
+     * Test badges_save_external_backpack without any auth details and also tests duplicate entries.
+     *
+     * @param boolean $withauth Test with authentication details provided
+     * @param boolean $duplicates Test for duplicates
+     * @dataProvider test_badges_save_external_backpack_provider
+     * @throws dml_exception
+     */
+    public function test_badges_save_external_backpack($withauth, $duplicates) {
+        global $DB;
+        $this->resetAfterTest();
+        $user = $this->getDataGenerator()->create_user();
+
+        $data = [
+            'userid' => $user->id,
+            'apiversion' => 2,
+            'backpackapiurl' => 'https://api.ca.badgr.io/v2',
+            'backpackweburl' => 'https://ca.badgr.io',
+        ];
+
+        if ($withauth) {
+            $data['backpackemail'] = 'test@test.com';
+            $data['password'] = 'test';
+        }
+
+        $result = badges_save_external_backpack((object) $data);
+        $record = $DB->get_record('badge_external_backpack', ['id' => $result]);
+        $this->assertEquals($record->backpackweburl, $data['backpackweburl']);
+        $this->assertEquals($record->backpackapiurl, $data['backpackapiurl']);
+        $record = $DB->get_record('badge_backpack', ['userid' => $user->id]);
+        if (!$withauth) {
+            $this->assertEmpty($record);
+        } else {
+            $this->assertNotEmpty($record);
+        }
+
+        if ($duplicates) {
+            // We shouldn't be able to insert multiple external_backpacks with the same values.
+            $this->expectException('dml_write_exception');
+            $result = badges_save_external_backpack((object)$data);
+        }
+    }
+
+    /**
+     * Provider for test_badges_save_external_backpack
+     *
+     * @return array
+     */
+    public function test_badges_save_external_backpack_provider() {
+        return [
+            "Test without any auth details and duplicates" => [
+                false, true
+            ],
+            "Test without any auth details and without duplicates" => [
+                false, false
+            ],
+            "Test with auth details and duplicates" => [
+                true, true
+            ],
+            "Test with any auth details and duplicates" => [
+                true, false
+            ],
+        ];
+    }
+
+    /**
+     * Test backpack creation/update with auth details provided
+     *
+     * @param boolean $isadmin
+     * @param boolean $updatetest
+     * @dataProvider test_badges_create_site_backpack_provider
+     */
+    public function test_badges_create_site_backpack($isadmin, $updatetest) {
+        global $DB;
+        $this->resetAfterTest();
+
+        $data = [
+            'apiversion' => 2,
+            'backpackapiurl' => 'https://api.ca.badgr.io/v2',
+            'backpackweburl' => 'https://ca.badgr.io',
+        ];
+
+        $data['backpackemail'] = 'test@test.com';
+        $data['password'] = 'test';
+        if ($isadmin || $updatetest) {
+            $this->setAdminUser();
+            $backpack = badges_create_site_backpack((object) $data);
+        }
+
+        if ($isadmin) {
+            if ($updatetest) {
+                $record = $DB->get_record('badge_backpack', ['userid' => 0]);
+                $data['badgebackpack'] = $record->id;
+                $data['backpackapiurl'] = 'https://api.ca.badgr.io/v3';
+                badges_update_site_backpack($backpack, (object)$data);
+            }
+            $record = $DB->get_record('badge_external_backpack', ['id' => $backpack]);
+            $this->assertEquals($record->backpackweburl, $data['backpackweburl']);
+            $this->assertEquals($record->backpackapiurl, $data['backpackapiurl']);
+            $record = $DB->get_record('badge_backpack', ['userid' => 0]);
+            $this->assertNotEmpty($record);
+        } else {
+            $user = $this->getDataGenerator()->create_user();
+            $this->setUser($user);
+            $this->expectException('required_capability_exception');
+            if ($updatetest) {
+                $result = badges_update_site_backpack($backpack, (object) $data);
+            } else {
+                $result = badges_create_site_backpack((object)$data);
+            }
+        }
+    }
+
+    /**
+     * Provider for test_badges_(create/update)_site_backpack
+     */
+    public function test_badges_create_site_backpack_provider() {
+        return [
+            "Test as admin user - creation test" => [true, true],
+            "Test as admin user - update test" => [true, false],
+            "Test as normal user - creation test" => [false, true],
+            "Test as normal user - update test" => [false, false],
+        ];
+    }
+
+    /**
+     * Test the badges_open_badges_backpack_api with different backpacks
+     */
+    public function test_badges_open_badges_backpack_api() {
+        $this->resetAfterTest();
+
+        $data = [
+            'apiversion' => 2,
+            'backpackapiurl' => 'https://api.ca.badgr.io/v2',
+            'backpackweburl' => 'https://ca.badgr.io',
+        ];
+
+        // Given a complete set of unique data, a new backpack and auth records should exist in the tables.
+        $data['backpackemail'] = 'test@test.com';
+        $data['password'] = 'test';
+        $backpack1 = badges_save_external_backpack((object) $data);
+        $data['backpackweburl'] = 'https://eu.badgr.io';
+        $data['backpackapiurl'] = 'https://api.eu.badgr.io/v2';
+        $data['apiversion'] = 2.1;
+        $backpack2 = badges_save_external_backpack((object) $data);
+
+        set_config('badges_site_backpack', $backpack2);
+        // The default response should check the default site backpack api version.
+        $this->assertEquals(2.1, badges_open_badges_backpack_api());
+        // Check the api version for the other backpack created.
+        $this->assertEquals(2, badges_open_badges_backpack_api($backpack1));
+        $this->assertEquals(2.1, badges_open_badges_backpack_api($backpack2));
+    }
+
+    /**
+     * Test the badges_get_site_backpack function
+     */
+    public function test_badges_get_site_backpack() {
+        $this->resetAfterTest();
+        $user = $this->getDataGenerator()->create_user();
+        $data = [
+            'apiversion' => '2',
+            'backpackapiurl' => 'https://api.ca.badgr.io/v2',
+            'backpackweburl' => 'https://ca.badgr.io',
+        ];
+        $backpack1 = badges_save_external_backpack((object) $data);
+        $data2 = array_merge($data, [
+            'backpackapiurl' => 'https://api.eu.badgr.io/v2',
+            'backpackweburl' => 'https://eu.badgr.io',
+            'backpackemail' => 'test@test.com',
+            'password' => 'test',
+        ]);
+        $backpack2 = badges_save_external_backpack((object) $data2);
+        $data3 = array_merge($data2, [
+            'userid' => $user->id,
+            'externalbackpackid' => $backpack2,
+            'backpackemail' => 'test2@test.com'
+        ]);
+        // In the following case, the id returned below equals backpack2. So we aren't storing it.
+        badges_save_backpack_credentials((object) $data3);
+        unset($data3['userid']);
+
+        // Get a site back based on the id returned from creation and no user id provided.
+        $this->assertEquals($data, array_intersect($data, (array) badges_get_site_backpack($backpack1)));
+        $this->assertEquals($data2, array_intersect($data2, (array) badges_get_site_backpack($backpack2)));
+        $this->assertEquals($data2, array_intersect($data2, (array) badges_get_site_backpack($backpack2, 0)));
+        $this->assertEquals($data3, array_intersect($data3, (array) badges_get_site_backpack($backpack2, $user->id)));
+
+        // Non-existent user backpack should return only configuration details and not auth details.
+        $userbackpack = badges_get_site_backpack($backpack1, $user->id);
+        $this->assertNull($userbackpack->badgebackpack);
+        $this->assertNull($userbackpack->password);
+        $this->assertNull($userbackpack->backpackemail);
+    }
+
+    /**
+     * Test the badges_get_user_backpack function
+     */
+    public function test_badges_get_user_backpack() {
+        $this->resetAfterTest();
+        $user = $this->getDataGenerator()->create_user();
+        $data = [
+            'apiversion' => '2',
+            'backpackapiurl' => 'https://api.ca.badgr.io/v2',
+            'backpackweburl' => 'https://ca.badgr.io',
+        ];
+        $backpack1 = badges_save_external_backpack((object) $data);
+        $data2 = array_merge($data, [
+            'backpackapiurl' => 'https://api.eu.badgr.io/v2',
+            'backpackweburl' => 'https://eu.badgr.io',
+            'backpackemail' => 'test@test.com',
+            'password' => 'test',
+        ]);
+        $backpack2 = badges_save_external_backpack((object) $data2);
+        $data3 = array_merge($data2, [
+            'userid' => $user->id,
+            'externalbackpackid' => $backpack2,
+            'backpackemail' => 'test2@test.com'
+        ]);
+        // In the following case, the id returned below equals backpack2. So we aren't storing it.
+        badges_save_backpack_credentials((object) $data3);
+        unset($data3['userid']);
+
+        // Currently logged in as admin.
+        $this->assertEquals($data2, array_intersect($data2, (array) badges_get_user_backpack()));
+        $this->assertEquals($data2, array_intersect($data2, (array) badges_get_user_backpack(0)));
+        $this->assertEquals($data3, array_intersect($data3, (array) badges_get_user_backpack($user->id)));
+
+        // Non-existent user backpack should return nothing.
+        $this->assertFalse(badges_get_user_backpack($backpack1, $user->id));
+
+        // Login as user.
+        $this->setUser($user);
+        $this->assertEquals($data3, array_intersect($data3, (array) badges_get_user_backpack()));
+    }
+
+    /**
+     * Test the badges_get_site_primary_backpack function
+     *
+     * @param boolean $withauth Testing with authentication or not.
+     * @dataProvider test_badges_get_site_primary_backpack_provider
+     */
+    public function test_badges_get_site_primary_backpack($withauth) {
+        $data = [
+            'apiversion' => '2',
+            'backpackapiurl' => 'https://api.ca.badgr.io/v2',
+            'backpackweburl' => 'https://ca.badgr.io',
+        ];
+        if ($withauth) {
+            $data = array_merge($data, [
+                'backpackemail' => 'test@test.com',
+                'password' => 'test',
+            ]);
+        }
+        $backpack = badges_save_external_backpack((object) $data);
+
+        set_config('badges_site_backpack', $backpack);
+        $sitebackpack = badges_get_site_primary_backpack();
+        $this->assertEquals($backpack, $sitebackpack->id);
+
+        if ($withauth) {
+            $this->assertEquals($data, array_intersect($data, (array) $sitebackpack));
+            $this->assertEquals($data['password'], $sitebackpack->password);
+            $this->assertEquals($data['backpackemail'], $sitebackpack->backpackemail);
+        } else {
+            $this->assertNull($sitebackpack->badgebackpack);
+            $this->assertNull($sitebackpack->password);
+            $this->assertNull($sitebackpack->backpackemail);
+        }
+    }
+
+    /**
+     * Test the test_badges_get_site_primary_backpack function.
+     *
+     * @return array
+     */
+    public function test_badges_get_site_primary_backpack_provider() {
+        return [
+            "Test with auth details" => [true],
+            "Test without auth details" => [false],
+        ];
+    }
+
+    /**
+     * Test the Badgr URL generator function
+     *
+     * @param mixed $type Type corresponding to the badge entites
+     * @param string $expected Expected string result
+     * @dataProvider badgr_open_url_generator
+     */
+    public function test_badges_generate_badgr_open_url($type, $expected) {
+        $data = [
+            'apiversion' => '2',
+            'backpackapiurl' => 'https://api.ca.badgr.io/v2',
+            'backpackweburl' => 'https://ca.badgr.io',
+            'backpackemail' => 'test@test.com',
+            'password' => 'test',
+        ];
+        $backpack2 = badges_save_external_backpack((object) $data);
+        $backpack = badges_get_site_backpack($backpack2);
+        $this->assertEquals($expected, badges_generate_badgr_open_url($backpack, $type, 123455));
+    }
+
+    /**
+     * Data provider for test_badges_generate_badgr_open_url
+     * @return array
+     */
+    public function badgr_open_url_generator() {
+        return [
+            'Badgr Assertion URL test' => [
+                OPEN_BADGES_V2_TYPE_ASSERTION, "https://api.ca.badgr.io/public/assertions/123455"
+            ],
+            'Badgr Issuer URL test' => [
+                OPEN_BADGES_V2_TYPE_ISSUER, "https://api.ca.badgr.io/public/issuers/123455"
+            ],
+            'Badgr Badge URL test' => [
+                OPEN_BADGES_V2_TYPE_BADGE, "https://api.ca.badgr.io/public/badges/123455"
+            ]
+        ];
+    }
+
+    /**
+     * Test badges_external_get_mapping function
+     *
+     * @param int $internalid The internal id of the mapping
+     * @param int $externalid The external / remote ref to the mapping
+     * @param mixed $expected The expected result from the function
+     * @param string|null $field The field we are passing to the function. Null if we don't want to pass anything.ss
+     *
+     * @dataProvider badges_external_get_mapping_provider
+     */
+    public function test_badges_external_get_mapping($internalid, $externalid, $expected, $field = null) {
+        $data = [
+            'apiversion' => '2',
+            'backpackapiurl' => 'https://api.ca.badgr.io/v2',
+            'backpackweburl' => 'https://ca.badgr.io',
+            'backpackemail' => 'test@test.com',
+            'password' => 'test',
+        ];
+        $backpack2 = badges_save_external_backpack((object) $data);
+        badges_external_create_mapping($backpack2, OPEN_BADGES_V2_TYPE_BADGE, $internalid, $externalid);
+        $expected = $expected == "id" ? $backpack2 : $expected;
+        if ($field) {
+            $this->assertEquals($expected, badges_external_get_mapping($backpack2, OPEN_BADGES_V2_TYPE_BADGE, $internalid, $field));
+        } else {
+            $this->assertEquals($expected, badges_external_get_mapping($backpack2, OPEN_BADGES_V2_TYPE_BADGE, $internalid));
+        }
+    }
+
+    /**
+     * Data provider for badges_external_get_mapping_provider
+     *
+     * @return array
+     */
+    public function badges_external_get_mapping_provider() {
+        return [
+            "Get the site backpack value" => [
+                1234, 4321, 'id', 'sitebackpackid'
+            ],
+            "Get the type of the mapping" => [
+                1234, 4321, OPEN_BADGES_V2_TYPE_BADGE, 'type'
+            ],
+            "Get the externalid of the mapping" => [
+                1234, 4321, 4321, 'externalid'
+            ],
+            "Get the externalid of the mapping without providing a param" => [
+                1234, 4321, 4321, null
+            ],
+            "Get the internalid of the mapping" => [
+                1234, 4321, 1234, 'internalid'
+            ]
+        ];
+    }
 }
index ea02aa7..3a979f6 100644 (file)
@@ -1,13 +1,13 @@
 @core @core_badges @_file_upload
 Feature: Backpack badges
-  The settings to connect to backpack with OAuth2 service
-  As an learner
+  Test the settings to add/update a backpack for a site and user.
   I need to verify display backpack in the my profile
 
   Background:
-    Given the following "badge external backpack" exist:
+    Given the following "badge external backpacks" exist:
       | backpackapiurl                               | backpackweburl           | apiversion |
-      | https://dc.imsglobal.org/obchost/ims/ob/v2p1 | https://dc.imsglobal.org | 2.1          |
+      | https://dc.imsglobal.org/obchost/ims/ob/v2p1 | https://dc.imsglobal.org | 2.1        |
+      | https://test.com/                            | https://test.com/        | 2          |
     And the following "users" exist:
       | username | firstname | lastname | email                |
       | student1 | Student   | 1        | student1@example.com |
@@ -121,3 +121,49 @@ Feature: Backpack badges
     Then I should see "The site backpack has been deleted."
     And I should not see "https://dc.imsglobal.org"
     And "Delete" "button" should not exist
+
+  @javascript
+  Scenario: Add a new site backpack with authentication details checkbox
+    Given I am on homepage
+    And I log in as "admin"
+    And I navigate to "Badges > Manage backpacks" in site administration
+    When I press "Add a new backpack"
+    And I set the field "backpackapiurl" to "http://backpackapiurl.cat"
+    And I set the field "backpackweburl" to "http://backpackweburl.cat"
+    And I set the field "apiversion" to "2.1"
+    Then "Include authentication details with the backpack" "checkbox" should not exist
+    And I should not see "Badge issuer email address"
+    And I should not see "Badge issuer password"
+    And I set the field "apiversion" to "1"
+    And "Include authentication details with the backpack" "checkbox" should exist
+    And I click on "includeauthdetails" "checkbox"
+    And I should see "Badge issuer email address"
+    And I should not see "Badge issuer password"
+    And I set the field "apiversion" to "2"
+    And "Include authentication details with the backpack" "checkbox" should exist
+    And I should see "Badge issuer email address"
+    And I should see "Badge issuer password"
+    And I set the field "backpackemail" to "test@test.com"
+    And I set the field "password" to "123456"
+    And I press "Save changes"
+    And I click on "Edit" "link" in the "http://backpackweburl.cat" "table_row"
+    And "input[name=includeauthdetails][type=checkbox][value=1]" "css_element" should exist
+    And I click on "includeauthdetails" "checkbox"
+    And I press "Save changes"
+    And I click on "Edit" "link" in the "http://backpackweburl.cat" "table_row"
+    And "input[name=includeauthdetails][type=checkbox][value=1]" "css_element" should not exist
+    And I click on "includeauthdetails" "checkbox"
+    And I should not see "test@test.com"
+    And I log out
+
+  @javascript
+  Scenario: View backpack form as a student
+    Given I log in as "student1"
+    And I follow "Preferences" in the user menu
+    And I follow "Backpack settings"
+    When I set the field "externalbackpackid" to "https://dc.imsglobal.org"
+    Then I should not see "Email address"
+    And I should not see "Password"
+    And I set the field "externalbackpackid" to "https://test.com/"
+    And I should see "Email address"
+    And I should see "Password"
index faca603..2ca56f3 100644 (file)
@@ -1,6 +1,22 @@
 This files describes API changes in /badges/*,
 information provided here is intended especially for developers.
 
+=== 3.10 ===
+* Users can now specify a backpack that differs from the site backpack. In order to do this, connection details need to
+be set in 'Manage backpacks' with OR without auth details.
+* Introduced new functions in backpack_api
+** 'import_badge_assertion' to facilitate cross domain badge imports.
+** 'update_assertion' updates a previously defined/created assertion.
+* New badge lib functions introduced
+** badges_save_external_backpack() - This method handles inserts/updates to the site wide backpacks' configuration details.
+** badges_save_backpack_credentials() - This method handles inserts/updates any authentication details to connect to the backpacks created. This can either be site OR user backpack authentication details
+** badges_get_user_backpack() - Gets a specific user's backpack. Defaults to current user's backpack if none provided.
+** badges_get_site_primary_backpack() - Get the primary backpack set for the site as defined in $CFG->badges_site_backpack
+* badges_open_badges_backpack_api() - Now accepts a backpackid(badge_external_backpack id) to check whether the version of the provided backpack.
+  This was introduced because now there is a difference between a site and user backpack. If null, defaults to site_backpack.
+* badges_get_site_backpack() - Accepts an additional $userid param if we want to get a specific user's backpack. Defaults to 0 if we are trying to get the site/admin level backpack
+* badges_external_get_mapping() - Accepts an additional argument to indicate which value it wants returned. Defaults to 'externalid' which contains the OBv2 badge URL
+
 === 3.9 ===
 * BADGE_BACKPACKAPIURL and BADGE_BACKPACKWEBURL are deprecated and should not be used.
 * OBv2 has been set to the default value when the obversion is not defined.
index a98db31..51b61a6 100644 (file)
@@ -83,7 +83,6 @@ $string['awards'] = 'Recipients';
 $string['backpackavailability'] = 'External badge verification';
 $string['backpackconnectionok'] = 'Backpack connection successfully established';
 $string['backpackconnectionnottested'] = 'The connection cannot be tested for this backpack because only Open Badges v2.0 backpacks support it.';
-$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?**
@@ -137,6 +136,7 @@ In this area, you can select collections of badges from your backpack that you w
 $string['backpacksettings'] = 'Backpack settings';
 $string['backpackapiurl'] = 'Backpack API URL';
 $string['backpackweburl'] = 'Backpack URL';
+$string['backpackprovider'] = 'Backpack Provider';
 $string['badges'] = 'Badges';
 $string['badgedetails'] = 'Badge details';
 $string['badgeimage'] = 'Image';
@@ -571,7 +571,11 @@ $string['version'] = 'Version';
 $string['version_help'] = 'The version field may be used to keep track of the badge\'s development. If specified, the version is displayed on the badge page.';
 $string['warnexpired'] = ' (This badge has expired!)';
 $string['year'] = 'Year(s)';
+$string['includeauthdetails'] = "Include authentication details with the backpack";
 
 // Deprecated since Moodle 3.9.
 $string['editsettings'] = 'Edit settings';
 $string['sitebackpackverify'] = 'Backpack connection';
+
+// Deprecated since Moodle 3.10.
+$string['backpackneedsupdate'] = 'The backpack connected to this profile does not match the backpack for the site. You need to disconnect and reconnect the backpack.';
index 4ba2ea5..1028bfe 100644 (file)
@@ -124,3 +124,4 @@ userfilterplaceholder,core
 sitebackpackverify,core_badges
 filetypesnotwhitelisted,core_form
 modeloutputdirinfo,core_analytics
+backpackneedsupdate,core_badges
index 78228c0..81bb7ef 100644 (file)
@@ -762,7 +762,7 @@ function badges_local_backpack_js($checksite = false) {
 }
 
 /**
- * Create the backpack with this data.
+ * Create the site backpack with this data.
  *
  * @param stdClass $data The new backpack data.
  * @return boolean
@@ -773,14 +773,8 @@ function badges_create_site_backpack($data) {
     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;
+    $data->sortorder = $count;
+    return badges_save_external_backpack($data);
 }
 
 /**
@@ -796,15 +790,8 @@ function badges_update_site_backpack($id, $data) {
     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 = !empty($data->password) ? $data->password : '';
-        $backpack->oauth2_issuerid = !empty($data->oauth2_issuerid) ? $data->oauth2_issuerid : '';
-        $DB->update_record('badge_external_backpack', $backpack);
-        return true;
+        $data->id = $id;
+        return badges_save_external_backpack($data);
     }
     return false;
 }
@@ -850,14 +837,88 @@ function badges_delete_site_backpack($id) {
     return false;
 }
 
+/**
+ * Perform the actual create/update of external bakpacks. Any checks on the validity of the id will need to be
+ * performed before it reaches this function.
+ *
+ * @param stdClass $data The backpack data we are updating/inserting
+ * @return int Returns the id of the new/updated record
+ */
+function badges_save_external_backpack(stdClass $data) {
+    global $DB;
+    $backpack = new stdClass();
+
+    $backpack->apiversion = $data->apiversion;
+    $backpack->backpackweburl = $data->backpackweburl;
+    $backpack->backpackapiurl = $data->backpackapiurl;
+    $backpack->oauth2_issuerid = $data->oauth2_issuerid ?? '';
+    if (isset($data->sortorder)) {
+        $backpack->sortorder = $data->sortorder;
+    }
+
+    $method = 'insert_record';
+    if (isset($data->id) && $data->id) {
+        $backpack->id = $data->id;
+        $method = 'update_record';
+    }
+    $record = $DB->$method('badge_external_backpack', $backpack, true);
+    $data->externalbackpackid = $data->id ?? $record;
+
+    unset($data->id);
+    badges_save_backpack_credentials($data);
+    return $data->externalbackpackid;
+}
+
+/**
+ * Create a backpack with the provided details. Stores the auth details of the backpack
+ *
+ * @param stdClass $data Backpack specific data.
+ * @return int The id of the external backpack that the credentials correspond to
+ */
+function badges_save_backpack_credentials(stdClass $data) {
+    global $DB;
+
+    if (isset($data->backpackemail) && isset($data->password)) {
+        $backpack = new stdClass();
+
+        $backpack->email = $data->backpackemail;
+        $backpack->password = !empty($data->password) ? $data->password : '';
+        $backpack->externalbackpackid = $data->externalbackpackid;
+        $backpack->userid = $data->userid ?? 0;
+        $backpack->backpackuid = $data->backpackuid ?? 0;
+        $backpack->autosync = $data->autosync ?? 0;
+
+        $id = null;
+        if (isset($data->badgebackpack) && $data->badgebackpack) {
+            $id = $data->badgebackpack;
+        } else if (isset($data->id) && $data->id) {
+            $id = $data->id;
+        }
+
+        $method = $id ? 'update_record' : 'insert_record';
+        if ($id) {
+            $backpack->id = $id;
+        }
+
+        $DB->$method('badge_backpack', $backpack);
+        return $backpack->externalbackpackid;
+    }
+
+    return $data->externalbackpackid ?? 0;
+}
+
 /**
  * Is any backpack enabled that supports open badges V1?
+ * @param int|null $backpackid Check the version of the given id OR if null the sitewide backpack
  * @return boolean
  */
-function badges_open_badges_backpack_api() {
-    global $CFG;
+function badges_open_badges_backpack_api(?int $backpackid = null) {
+    if (!$backpackid) {
+        global $CFG;
+        $backpackid = $CFG->badges_site_backpack;
+    }
 
-    $backpack = badges_get_site_backpack($CFG->badges_site_backpack);
+    $backpack = badges_get_site_backpack($backpackid);
     if (empty($backpack->apiversion)) {
         return OPEN_BADGES_V2;
     }
@@ -865,15 +926,54 @@ function badges_open_badges_backpack_api() {
 }
 
 /**
- * Get a site backpacks by id or url.
+ * Get a site backpacks by id for a particular user or site (if userid is 0)
  *
  * @param int $id The backpack id.
+ * @param int $userid The owner of the backpack, 0 if it's a sitewide backpack else a user's site backpack
  * @return array(stdClass)
  */
-function badges_get_site_backpack($id) {
+function badges_get_site_backpack($id, int $userid = 0) {
     global $DB;
 
-    return $DB->get_record('badge_external_backpack', ['id' => $id]);
+    $sql = "SELECT beb.*, bb.id AS badgebackpack, bb.password, bb.email AS backpackemail
+              FROM {badge_external_backpack} beb
+         LEFT JOIN {badge_backpack} bb ON bb.externalbackpackid = beb.id AND bb.userid=:userid
+             WHERE beb.id=:id";
+
+    return $DB->get_record_sql($sql, ['id' => $id, 'userid' => $userid]);
+}
+
+/**
+ * Get the user backpack for the currently logged in user OR the provided user
+ *
+ * @param int|null $userid The user whose backpack you're requesting for. If null, get the logged in user's backpack
+ * @return mixed The user's backpack or none.
+ * @throws dml_exception
+ */
+function badges_get_user_backpack(?int $userid = 0) {
+    global $DB;
+
+    if (!$userid) {
+        global $USER;
+        $userid = $USER->id;
+    }
+
+    $sql = "SELECT beb.*, bb.id AS badgebackpack, bb.password, bb.email AS backpackemail
+              FROM {badge_external_backpack} beb
+              JOIN {badge_backpack} bb ON bb.externalbackpackid = beb.id AND bb.userid=:userid";
+
+    return $DB->get_record_sql($sql, ['userid' => $userid]);
+}
+
+/**
+ * Get the primary backpack for the site
+ *
+ * @return array(stdClass)
+ */
+function badges_get_site_primary_backpack() {
+    global $CFG;
+
+    return badges_get_site_backpack($CFG->badges_site_backpack);
 }
 
 /**
@@ -917,6 +1017,7 @@ function badges_get_badge_api_versions() {
 function badges_get_default_issuer() {
     global $CFG, $SITE;
 
+    $sitebackpack = badges_get_site_primary_backpack();
     $issuer = array();
     $issuerurl = new moodle_url('/');
     $issuer['name'] = $CFG->badges_defaultissuername;
@@ -924,7 +1025,7 @@ function badges_get_default_issuer() {
         $issuer['name'] = $SITE->fullname ? $SITE->fullname : $SITE->shortname;
     }
     $issuer['url'] = $issuerurl->out(false);
-    $issuer['email'] = $CFG->badges_defaultissuercontact;
+    $issuer['email'] = $sitebackpack->backpackemail ?? $CFG->badges_defaultissuercontact;
     $issuer['@context'] = OPEN_BADGES_V2_CONTEXT;
     $issuerid = new moodle_url('/badges/issuer_json.php');
     $issuer['id'] = $issuerid->out(false);
@@ -962,9 +1063,10 @@ function badges_disconnect_user_backpack($userid) {
  * @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 $param The param we need to return. Defaults to the externalid.
  * @return mixed The id or false if it doesn't exist.
  */
-function badges_external_get_mapping($sitebackpackid, $type, $internalid) {
+function badges_external_get_mapping($sitebackpackid, $type, $internalid, $param = 'externalid') {
     global $DB;
     // Return externalid if it exists.
     $params = [
@@ -973,9 +1075,9 @@ function badges_external_get_mapping($sitebackpackid, $type, $internalid) {
         'internalid' => $internalid
     ];
 
-    $record = $DB->get_record('badge_external_identifier', $params, 'externalid', IGNORE_MISSING);
+    $record = $DB->get_record('badge_external_identifier', $params, $param, IGNORE_MISSING);
     if ($record) {
-        return $record->externalid;
+        return $record->$param;
     }
     return false;
 }
@@ -1256,3 +1358,26 @@ function badges_get_oauth2_service_options() {
 
     return $options;
 }
+
+/**
+ * Generate a public badgr URL that conforms to OBv2. This is done because badgr responses do not currently conform to
+ * the spec.
+ *
+ * WARNING: This is an extremely hacky way of implementing this and should be removed once the standards are conformed to.
+ *
+ * @param stdClass $backpack The Badgr backpack we are pushing to
+ * @param string $type The type of object we are dealing with either Issuer, Assertion OR Badge.
+ * @param string $externalid The externalid as provided by the backpack
+ * @return string The public URL to access Badgr objects
+ */
+function badges_generate_badgr_open_url($backpack, $type, $externalid) {
+    if (badges_open_badges_backpack_api($backpack->id) == OPEN_BADGES_V2) {
+        $entity = strtolower($type);
+        if ($type == OPEN_BADGES_V2_TYPE_BADGE) {
+            $entity = "badge";
+        }
+        $url = new moodle_url($backpack->backpackapiurl);
+        return "{$url->get_scheme()}://{$url->get_host()}/public/{$entity}s/$externalid";
+
+    }
+}
\ No newline at end of file
index 391c799..00697cb 100644 (file)
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
         <KEY NAME="fk_userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
+        <KEY NAME="backpackcredentials" TYPE="unique" FIELDS="userid, externalbackpackid"/>
         <KEY NAME="externalbackpack" TYPE="foreign" FIELDS="externalbackpackid" REFTABLE="badge_external_backpack" REFFIELDS="id"/>
       </KEYS>
     </TABLE>
         <FIELD NAME="backpackweburl" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="apiversion" TYPE="char" LENGTH="12" NOTNULL="true" DEFAULT="1.0" SEQUENCE="false"/>
         <FIELD NAME="sortorder" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
-        <FIELD NAME="password" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="Password to login into external backpack and issue badges."/>
         <FIELD NAME="oauth2_issuerid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="OAuth 2 Issuer"/>
       </FIELDS>
       <KEYS>
       </INDEXES>
     </TABLE>
   </TABLES>
-</XMLDB>
\ No newline at end of file
+</XMLDB>
index 249d23b..5237245 100644 (file)
@@ -2876,5 +2876,44 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2020102300.01);
     }
 
+    if ($oldversion < 2020102300.02) {
+        $table = new xmldb_table('badge_backpack');
+        $uniquekey = new xmldb_key('backpackcredentials', XMLDB_KEY_UNIQUE, ['userid', 'externalbackpackid']);
+
+        // All external backpack providers/hosts are now exclusively stored in badge_external_backpack.
+        // All credentials are stored in badge_backpack and are unique per user, backpack.
+        if (!$dbman->find_key_name($table, $uniquekey)) {
+            $dbman->add_key($table, $uniquekey);
+        }
+
+        // If there is a current backpack set then copy it across to the new structure.
+        if ($CFG->badges_defaultissuercontact) {
+            // Get the currently used site backpacks.
+            $records = $DB->get_records_select('badge_external_backpack', "password IS NOT NULL AND password != ''");
+            $backpack = [
+                'userid' => '0',
+                'email' => $CFG->badges_defaultissuercontact,
+                'backpackuid' => -1
+            ];
+
+            // Create records corresponding to the site backpacks.
+            foreach ($records as $record) {
+                $backpack['password'] = $record->password;
+                $backpack['externalbackpackid'] = $record->id;
+                $DB->insert_record('badge_backpack', (object) $backpack);
+            }
+        }
+
+        // Drop the password field as this is moved to badge_backpack.
+        $table = new xmldb_table('badge_external_backpack');
+        $field = new xmldb_field('password', XMLDB_TYPE_CHAR, '50');
+        if ($dbman->field_exists($table, $field)) {
+            $dbman->drop_field($table, $field);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2020102300.02);
+    }
+
     return true;
 }
index 3b91464..2f08fc2 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2020102300.01;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2020102300.02;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 $release  = '3.10dev+ (Build: 20201023)';// Human-friendly version name