Merge branch 'MDL-65495-master-courseid' of git://github.com/mudrd8mz/moodle
authorAdrian Greeve <abgreeve@gmail.com>
Tue, 7 May 2019 01:53:17 +0000 (09:53 +0800)
committerAdrian Greeve <abgreeve@gmail.com>
Tue, 7 May 2019 08:26:46 +0000 (16:26 +0800)
149 files changed:
admin/settings/badges.php
admin/templates/setting_configduration.mustache
admin/tool/lp/amd/build/competencies.min.js
admin/tool/lp/amd/build/course_competency_settings.min.js
admin/tool/lp/amd/build/module_navigation.min.js [new file with mode: 0644]
admin/tool/lp/amd/src/competencies.js
admin/tool/lp/amd/src/course_competency_settings.js
admin/tool/lp/amd/src/module_navigation.js [new file with mode: 0644]
admin/tool/lp/classes/external.php
admin/tool/lp/classes/output/course_competencies_page.php
admin/tool/lp/classes/output/module_navigation.php [new file with mode: 0644]
admin/tool/lp/classes/output/renderer.php
admin/tool/lp/coursecompetencies.php
admin/tool/lp/lang/en/tool_lp.php
admin/tool/lp/templates/course_competencies_page.mustache
admin/tool/lp/templates/module_navigation.mustache [new file with mode: 0644]
admin/tool/lp/tests/behat/course_competencies.feature [new file with mode: 0644]
admin/tool/lp/tests/externallib_test.php
badges/action.php
badges/alignment.php
badges/assertion.php
badges/backpack-add.php [new file with mode: 0644]
badges/backpack.js
badges/backpackemailverify.php
badges/backpacks.php [new file with mode: 0644]
badges/badge.php
badges/badge_json.php
badges/classes/assertion.php
badges/classes/backpack_api.php [new file with mode: 0644]
badges/classes/backpack_api_mapping.php [new file with mode: 0644]
badges/classes/badge.php [new file with mode: 0644]
badges/classes/external.php
badges/classes/external/alignment_exporter.php
badges/classes/external/assertion_exporter.php [new file with mode: 0644]
badges/classes/external/backpack_exporter.php [new file with mode: 0644]
badges/classes/external/badgeclass_exporter.php [new file with mode: 0644]
badges/classes/external/collection_exporter.php [new file with mode: 0644]
badges/classes/external/issuer_exporter.php [new file with mode: 0644]
badges/classes/external/recipient_exporter.php [new file with mode: 0644]
badges/classes/external/user_badge_exporter.php
badges/classes/external/verification_exporter.php [new file with mode: 0644]
badges/classes/form/backpack.php [moved from badges/backpack_form.php with 60% similarity]
badges/classes/form/badge.php [moved from badges/edit_form.php with 71% similarity]
badges/classes/form/collections.php [new file with mode: 0644]
badges/classes/form/external_backpack.php [new file with mode: 0644]
badges/classes/form/message.php [new file with mode: 0644]
badges/classes/observer.php
badges/classes/output/badge_alignments.php [new file with mode: 0644]
badges/classes/output/badge_collection.php [new file with mode: 0644]
badges/classes/output/badge_management.php [new file with mode: 0644]
badges/classes/output/badge_recipients.php [new file with mode: 0644]
badges/classes/output/badge_related.php [new file with mode: 0644]
badges/classes/output/badge_user_collection.php [new file with mode: 0644]
badges/classes/output/external_backpacks_page.php [new file with mode: 0644]
badges/classes/output/external_backpacks_table.php [new file with mode: 0644]
badges/classes/output/external_badge.php [new file with mode: 0644]
badges/classes/output/issued_badge.php [new file with mode: 0644]
badges/classes/privacy/provider.php
badges/edit.php
badges/external.php
badges/index.php
badges/lib/backpacklib.php [deleted file]
badges/mybackpack.php
badges/mybadges.php
badges/newbadge.php
badges/recipients.php
badges/related.php
badges/renderer.php
badges/templates/external_backpacks_page.mustache [new file with mode: 0644]
badges/tests/badgeslib_test.php
badges/tests/behat/add_badge.feature
badges/tests/external_test.php
badges/upgrade.txt
badges/view.php
course/classes/external/course_summary_exporter.php
course/upgrade.txt
favourites/classes/local/service/component_favourite_service.php [new file with mode: 0644]
favourites/classes/service_factory.php
favourites/tests/component_favourite_service_test.php [new file with mode: 0644]
favourites/tests/repository_test.php
favourites/tests/user_favourite_service_test.php [moved from favourites/tests/service_test.php with 100% similarity]
grade/templates/edit_tree.mustache
group/tests/privacy_provider_test.php
lang/en/badges.php
lang/en/deprecated.txt
lang/en/message.php
lib/badgeslib.php
lib/behat/classes/partial_named_selector.php
lib/classes/hub/registration.php
lib/db/install.php
lib/db/install.xml [changed mode: 0644->0755]
lib/db/services.php
lib/db/upgrade.php [changed mode: 0644->0755]
lib/form/cancel.php
lib/form/submit.php
lib/formslib.php
lib/outputrenderers.php
lib/setuplib.php
lib/testing/generator/data_generator.php
message/amd/build/message_drawer_view_conversation.min.js
message/amd/build/message_drawer_view_conversation_constants.min.js
message/amd/build/message_drawer_view_conversation_patcher.min.js
message/amd/build/message_drawer_view_conversation_renderer.min.js
message/amd/build/message_drawer_view_conversation_state_manager.min.js
message/amd/build/message_repository.min.js
message/amd/src/message_drawer_view_conversation.js
message/amd/src/message_drawer_view_conversation_constants.js
message/amd/src/message_drawer_view_conversation_patcher.js
message/amd/src/message_drawer_view_conversation_renderer.js
message/amd/src/message_drawer_view_conversation_state_manager.js
message/amd/src/message_repository.js
message/classes/api.php
message/externallib.php
message/templates/message_drawer_view_conversation_body_confirm_dialogue.mustache
message/tests/api_test.php
message/tests/behat/favourite_conversations.feature
message/tests/behat/group_conversation.feature
message/tests/behat/message_delete_conversation.feature
message/tests/behat/message_send_messages.feature
message/tests/behat/self_conversation.feature [new file with mode: 0644]
message/tests/behat/unread_messages.feature
message/tests/externallib_test.php
message/tests/privacy_provider_test.php
mod/forum/amd/build/inpage_reply.min.js
mod/forum/amd/build/selectors.min.js
mod/forum/amd/src/inpage_reply.js
mod/forum/amd/src/selectors.js
mod/forum/classes/local/exporters/author.php
mod/forum/classes/local/exporters/discussion_summary.php
mod/forum/templates/forum_discussion_post.mustache
mod/forum/templates/inpage_reply.mustache
mod/lesson/continue.php
mod/quiz/amd/build/modal_quiz_question_bank.min.js
mod/quiz/amd/src/modal_quiz_question_bank.js
mod/quiz/classes/output/edit_renderer.php
mod/quiz/classes/structure.php
mod/quiz/lang/en/quiz.php
mod/quiz/styles.css
mod/quiz/tests/behat/editing_add_from_question_bank.feature
mod/quiz/tests/behat/editing_remove_multiple_questions.feature
mod/quiz/tests/structure_test.php
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-debug.js
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-min.js
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes.js
mod/quiz/yui/src/toolboxes/js/resource.js
mod/quiz/yui/src/toolboxes/js/toolbox.js
question/type/questionbase.php
report/competency/templates/report.mustache
version.php

index 276582c..24333ba 100644 (file)
@@ -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)
+        )
+    );
 }
index d2faa4a..d20dabc 100644 (file)
@@ -40,7 +40,7 @@
     <div class="form-inline">
         <input type="text" size="5" id="{{id}}v" name="{{name}}[v]" value="{{value}}" class="form-control text-ltr">
         <label class="sr-only" for="{{id}}u">{{#str}}durationunits, admin{{/str}}</label>
-        <select id="{{id}}u" name="{{name}}[u]" class="form-control">
+        <select id="{{id}}u" name="{{name}}[u]" class="form-control custom-select">
             {{#options}}
                 <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
             {{/options}}
index 1f5ddbe..6d24fd8 100644 (file)
Binary files a/admin/tool/lp/amd/build/competencies.min.js and b/admin/tool/lp/amd/build/competencies.min.js differ
index 5fe246e..fe53dc6 100644 (file)
Binary files a/admin/tool/lp/amd/build/course_competency_settings.min.js and b/admin/tool/lp/amd/build/course_competency_settings.min.js differ
diff --git a/admin/tool/lp/amd/build/module_navigation.min.js b/admin/tool/lp/amd/build/module_navigation.min.js
new file mode 100644 (file)
index 0000000..db9d241
Binary files /dev/null and b/admin/tool/lp/amd/build/module_navigation.min.js differ
index 1b82116..e3a5176 100644 (file)
@@ -144,7 +144,7 @@ define(['jquery',
                     });
                     requests.push({
                         methodname: 'tool_lp_data_for_course_competencies_page',
-                        args: {courseid: self.itemid}
+                        args: {courseid: self.itemid, moduleid: 0}
                     });
 
                     pagerender = 'tool_lp/course_competencies_page';
@@ -212,7 +212,7 @@ define(['jquery',
                 {methodname: 'core_competency_remove_competency_from_course',
                     args: {courseid: localthis.itemid, competencyid: deleteid}},
                 {methodname: 'tool_lp_data_for_course_competencies_page',
-                    args: {courseid: localthis.itemid}}
+                    args: {courseid: localthis.itemid, moduleid: 0}}
             ]);
             pagerender = 'tool_lp/course_competencies_page';
             pageregion = 'coursecompetenciespage';
@@ -311,7 +311,7 @@ define(['jquery',
                     {methodname: 'core_competency_set_course_competency_ruleoutcome',
                       args: {coursecompetencyid: coursecompetencyid, ruleoutcome: ruleoutcome}},
                     {methodname: 'tool_lp_data_for_course_competencies_page',
-                      args: {courseid: localthis.itemid}}
+                      args: {courseid: localthis.itemid, moduleid: 0}}
                 ]);
 
                 requests[1].done(function(context) {
index 06af055..ff21ade 100644 (file)
@@ -134,7 +134,7 @@ define(['jquery',
 
         ajax.call([
             {methodname: 'tool_lp_data_for_course_competencies_page',
-              args: {courseid: courseId}}
+              args: {courseid: courseId, moduleid: 0}}
         ])[0].done(function(context) {
             templates.render('tool_lp/course_competencies_page', context).done(function(html, js) {
                 $('[data-region="coursecompetenciespage"]').replaceWith(html);
diff --git a/admin/tool/lp/amd/src/module_navigation.js b/admin/tool/lp/amd/src/module_navigation.js
new file mode 100644 (file)
index 0000000..d08a550
--- /dev/null
@@ -0,0 +1,62 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Module to navigation between users in a course.
+ *
+ * @package    tool_lp
+ * @copyright  2019 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(['jquery'], function($) {
+
+    /**
+     * ModuleNavigation
+     *
+     * @param {String} moduleSelector The selector of the module element.
+     * @param {String} baseUrl The base url for the page (no params).
+     * @param {Number} courseId The course id
+     * @param {Number} moduleId The activity module (filter)
+     */
+    var ModuleNavigation = function(moduleSelector, baseUrl, courseId, moduleId) {
+        this._baseUrl = baseUrl;
+        this._moduleId = moduleId;
+        this._courseId = courseId;
+
+        $(moduleSelector).on('change', this._moduleChanged.bind(this));
+    };
+
+    /**
+     * The module was changed in the select list.
+     *
+     * @method _moduleChanged
+     * @param {Event} e the event
+     */
+    ModuleNavigation.prototype._moduleChanged = function(e) {
+        var newModuleId = $(e.target).val();
+        var queryStr = '?mod=' + newModuleId + '&courseid=' + this._courseId;
+        document.location = this._baseUrl + queryStr;
+    };
+
+    /** @type {Number} The id of the course. */
+    ModuleNavigation.prototype._courseId = null;
+    /** @type {Number} The id of the module. */
+    ModuleNavigation.prototype._moduleId = null;
+    /** @type {String} Plugin base url. */
+    ModuleNavigation.prototype._baseUrl = null;
+
+    return /** @alias module:tool_lp/module_navigation */ ModuleNavigation;
+});
index d79317c..ada3e88 100644 (file)
@@ -363,7 +363,13 @@ class external extends external_api {
             'The course id',
             VALUE_REQUIRED
         );
-        $params = array('courseid' => $courseid);
+        $moduleid = new external_value(
+            PARAM_INT,
+            'The module id',
+            VALUE_DEFAULT,
+            0
+        );
+        $params = array('courseid' => $courseid, 'moduleid' => $moduleid);
         return new external_function_parameters($params);
     }
 
@@ -371,16 +377,18 @@ class external extends external_api {
      * Loads the data required to render the course_competencies_page template.
      *
      * @param int $courseid The course id to check.
+     * @param int $moduleid The module id to check (0 for no filter).
      * @return boolean
      */
-    public static function data_for_course_competencies_page($courseid) {
+    public static function data_for_course_competencies_page($courseid, $moduleid) {
         global $PAGE;
         $params = self::validate_parameters(self::data_for_course_competencies_page_parameters(), array(
             'courseid' => $courseid,
+            'moduleid' => $moduleid,
         ));
         self::validate_context(context_course::instance($params['courseid']));
 
-        $renderable = new output\course_competencies_page($params['courseid']);
+        $renderable = new output\course_competencies_page($params['courseid'], $params['moduleid']);
         $renderer = $PAGE->get_renderer('tool_lp');
 
         $data = $renderable->export_for_template($renderer);
@@ -425,6 +433,7 @@ class external extends external_api {
                 ),
             ))),
             'manageurl' => new external_value(PARAM_LOCALURL, 'Url to the manage competencies page.'),
+            'pluginbaseurl' => new external_value(PARAM_LOCALURL, 'Url to the course competencies page.'),
         ));
 
     }
index 218d8a5..830cf8a 100644 (file)
@@ -57,6 +57,9 @@ class course_competencies_page implements renderable, templatable {
     /** @var int $courseid Course id for this page. */
     protected $courseid = null;
 
+    /** @var int $moduleid Module id for this page. */
+    protected $moduleid = null;
+
     /** @var context $context The context for this page. */
     protected $context = null;
 
@@ -76,10 +79,31 @@ class course_competencies_page implements renderable, templatable {
      * Construct this renderable.
      * @param int $courseid The course record for this page.
      */
-    public function __construct($courseid) {
+    public function __construct($courseid, $moduleid) {
         $this->context = context_course::instance($courseid);
         $this->courseid = $courseid;
+        $this->moduleid = $moduleid;
         $this->coursecompetencylist = api::list_course_competencies($courseid);
+
+        if ($this->moduleid > 0) {
+            $modulecompetencies = api::list_course_module_competencies_in_course_module($this->moduleid);
+            foreach ($this->coursecompetencylist as $ccid => $coursecompetency) {
+                $coursecompetency = $coursecompetency['coursecompetency'];
+                $found = false;
+                foreach ($modulecompetencies as $mcid => $modulecompetency) {
+                    if ($modulecompetency->get('competencyid') == $coursecompetency->get('competencyid')) {
+                        $found = true;
+                        break;
+                    }
+                }
+
+                if (!$found) {
+                    // We need to filter out this competency.
+                    unset($this->coursecompetencylist[$ccid]);
+                }
+            }
+        }
+
         $this->canmanagecoursecompetencies = has_capability('moodle/competency:coursecompetencymanage', $this->context);
         $this->canconfigurecoursecompetencies = has_capability('moodle/competency:coursecompetencyconfigure', $this->context);
         $this->cangradecompetencies = has_capability('moodle/competency:competencygrade', $this->context);
@@ -112,6 +136,7 @@ class course_competencies_page implements renderable, templatable {
 
         $data = new stdClass();
         $data->courseid = $this->courseid;
+        $data->moduleid = $this->moduleid;
         $data->pagecontextid = $this->context->id;
         $data->competencies = array();
         $data->pluginbaseurl = (new moodle_url('/admin/tool/lp'))->out(true);
@@ -120,6 +145,24 @@ class course_competencies_page implements renderable, templatable {
         if ($gradable) {
             $usercompetencycourses = api::list_user_competencies_in_course($this->courseid, $USER->id);
             $data->gradableuserid = $USER->id;
+
+            if ($this->moduleid > 0) {
+                $modulecompetencies = api::list_course_module_competencies_in_course_module($this->moduleid);
+                foreach ($usercompetencycourses as $ucid => $usercoursecompetency) {
+                    $found = false;
+                    foreach ($modulecompetencies as $mcid => $modulecompetency) {
+                        if ($modulecompetency->get('competencyid') == $usercoursecompetency->get('competencyid')) {
+                            $found = true;
+                            break;
+                        }
+                    }
+
+                    if (!$found) {
+                        // We need to filter out this competency.
+                        unset($usercompetencycourses[$ucid]);
+                    }
+                }
+            }
         }
 
         $ruleoutcomelist = course_competency::get_ruleoutcome_list();
diff --git a/admin/tool/lp/classes/output/module_navigation.php b/admin/tool/lp/classes/output/module_navigation.php
new file mode 100644 (file)
index 0000000..68ff9b0
--- /dev/null
@@ -0,0 +1,103 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * User navigation class.
+ *
+ * @package    tool_lp
+ * @copyright  2019 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_lp\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+use renderable;
+use renderer_base;
+use templatable;
+use context_course;
+use core_course\external\course_module_summary_exporter;
+use stdClass;
+
+/**
+ * User course navigation class.
+ *
+ * @package    tool_lp
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class module_navigation implements renderable, templatable {
+
+    /** @var courseid */
+    protected $courseid;
+
+    /** @var moduleid */
+    protected $moduleid;
+
+    /** @var baseurl */
+    protected $baseurl;
+
+    /**
+     * Construct.
+     *
+     * @param int $courseid
+     * @param int $moduleid
+     * @param string $baseurl
+     */
+    public function __construct($courseid, $moduleid, $baseurl) {
+        $this->courseid = $courseid;
+        $this->moduleid = $moduleid;
+        $this->baseurl = $baseurl;
+    }
+
+    /**
+     * Export the data.
+     *
+     * @param renderer_base $output
+     * @return stdClass
+     */
+    public function export_for_template(renderer_base $output) {
+
+        $context = context_course::instance($this->courseid);
+
+        $data = new stdClass();
+        $data->courseid = $this->courseid;
+        $data->moduleid = $this->moduleid;
+        $data->baseurl = $this->baseurl;
+        $data->hasmodules = false;
+        $data->modules = array();
+
+        $data->hasmodules = true;
+        $data->modules = array();
+        $empty = (object)['id' => 0, 'name' => get_string('nofiltersapplied')];
+        $data->modules[] = $empty;
+
+        $modinfo = get_fast_modinfo($this->courseid);
+        foreach ($modinfo->get_cms() as $cm) {
+            if ($cm->uservisible) {
+                $exporter = new course_module_summary_exporter(null, ['cm' => $cm]);
+                $module = $exporter->export($output);
+                if ($module->id == $this->moduleid) {
+                    $module->selected = true;
+                }
+                $data->modules[] = $module;
+                $data->hasmodules = true;
+            }
+        }
+
+        return $data;
+    }
+}
index ca45e29..3907215 100644 (file)
@@ -263,4 +263,16 @@ class renderer extends plugin_renderer_base {
         $n = new \core\output\notification($message, \core\output\notification::NOTIFY_SUCCESS);
         return $this->render($n);
     }
+
+    /**
+     * Defer to template.
+     *
+     * @param module_navigation $nav
+     * @return string
+     */
+    public function render_module_navigation(module_navigation $nav) {
+        $data = $nav->export_for_template($this);
+        return parent::render_from_template('tool_lp/module_navigation', $data);
+    }
+
 }
index e700a17..f3e65a7 100644 (file)
 require_once(__DIR__ . '/../../../config.php');
 
 $id = required_param('courseid', PARAM_INT);
+$currentmodule = optional_param('mod', null, PARAM_INT);
+if ($currentmodule > 0) {
+    $cm = get_coursemodule_from_id('', $currentmodule, 0, false, MUST_EXIST);
+}
 
 $params = array('id' => $id);
 $course = $DB->get_record('course', $params, '*', MUST_EXIST);
@@ -33,16 +37,22 @@ require_login($course);
 \core_competency\api::require_enabled();
 
 $context = context_course::instance($course->id);
-$urlparams = array('courseid' => $id);
+$urlparams = array('courseid' => $id, 'mod' => $currentmodule);
 
 $url = new moodle_url('/admin/tool/lp/coursecompetencies.php', $urlparams);
 
 list($title, $subtitle) = \tool_lp\page_helper::setup_for_course($url, $course);
+if ($currentmodule > 0) {
+    $title = get_string('filtermodule', 'report_competency', format_string($cm->name));
+}
 
 $output = $PAGE->get_renderer('tool_lp');
-$page = new \tool_lp\output\course_competencies_page($course->id);
+$page = new \tool_lp\output\course_competencies_page($course->id, $currentmodule);
 
 echo $output->header();
+$baseurl = new moodle_url('/admin/tool/lp/coursecompetencies.php');
+$nav = new \tool_lp\output\module_navigation($course->id, $currentmodule, $baseurl);
+echo $output->render($nav);
 echo $output->heading($title);
 
 echo $output->render($page);
index a197960..c4db7ba 100644 (file)
@@ -146,6 +146,7 @@ $string['nfiles'] = '{$a} file(s)';
 $string['noactivities'] = 'No activities';
 $string['nocompetencies'] = 'No competencies have been created in this framework.';
 $string['nocompetenciesincourse'] = 'No competencies have been linked to this course.';
+$string['nocompetenciesinactivity'] = 'No competencies have been linked to this activity or resource.';
 $string['nocompetenciesinevidence'] = 'No competencies have been linked to this evidence.';
 $string['nocompetenciesinlearningplan'] = 'No competencies have been linked to this learning plan.';
 $string['nocompetenciesintemplate'] = 'No competencies have been linked to this learning plan template.';
index b41af9b..8de78a0 100644 (file)
 </table>
 {{^competencies}}
 <p class="alert alert-info">
-    {{#str}}nocompetenciesincourse, tool_lp{{/str}}
+    {{#moduleid}}
+        {{#str}}nocompetenciesinactivity, tool_lp{{/str}}
+    {{/moduleid}}
+    {{^moduleid}}
+        {{#str}}nocompetenciesincourse, tool_lp{{/str}}
+    {{/moduleid}}
 </p>
 {{/competencies}}
 </div>
diff --git a/admin/tool/lp/templates/module_navigation.mustache b/admin/tool/lp/templates/module_navigation.mustache
new file mode 100644 (file)
index 0000000..aa10239
--- /dev/null
@@ -0,0 +1,52 @@
+{{!
+    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 <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template tool_lp/module_navigation
+
+    Show an auto-complete for filtering by competencies linked to a module.
+
+    Context variables required for this template:
+    * hasmodules
+    * modules - array
+      * id
+      * selected
+      * name
+
+    // No example context because the JS is connected to webservices
+}}
+<div class="float-right card p-2">
+<form class="user-competency-course-navigation">
+{{#hasmodules}}
+<span>
+<label for="module-nav-{{uniqid}}" class="accesshide">{{#str}}filterbyactivity, tool_lp{{/str}}</label>
+<select id="module-nav-{{uniqid}}">
+{{#modules}}
+<option value="{{id}}" {{#selected}}selected="selected"{{/selected}}>{{name}}</option>
+{{/modules}}
+</select>
+</span>
+{{/hasmodules}}
+</form>
+</div>
+{{#js}}
+require(['core/form-autocomplete', 'tool_lp/module_navigation'], function(autocomplete, nav) {
+    (new nav('#module-nav-{{uniqid}}', '{{baseurl}}', {{courseid}}, {{moduleid}}));
+{{#hasmodules}}
+    autocomplete.enhance('#module-nav-{{uniqid}}', false, false, {{# quote }}{{# str }}filterbyactivity, tool_lp{{/ str }}{{/ quote }});
+{{/hasmodules}}
+});
+{{/js}}
diff --git a/admin/tool/lp/tests/behat/course_competencies.feature b/admin/tool/lp/tests/behat/course_competencies.feature
new file mode 100644 (file)
index 0000000..f765213
--- /dev/null
@@ -0,0 +1,63 @@
+@report @javascript @tool_lp
+Feature: See the competencies for an activity on the course competencies page.
+  As a student
+  In order to see only the competencies for an activity in the course competencies page.
+
+  Background:
+    Given the following lp "frameworks" exist:
+      | shortname | idnumber |
+      | Test-Framework | ID-FW1 |
+    And the following lp "competencies" exist:
+      | shortname | framework |
+      | Test-Comp1 | ID-FW1 |
+      | Test-Comp2 | ID-FW1 |
+    Given the following "courses" exist:
+      | shortname | fullname   |
+      | C1        | Course 1 |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | student1 | Student | 1 | student1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+    And the following "activities" exist:
+      | activity | name       | intro      | course | idnumber |
+      | page     | PageName1  | PageDesc1  | C1     | PAGE1    |
+      | page     | PageName2  | PageDesc2  | C1     | PAGE2    |
+    And I log in as "admin"
+    And I am on site homepage
+    And I follow "Course 1"
+    And I follow "Competencies"
+    And I press "Add competencies to course"
+    And "Competency picker" "dialogue" should be visible
+    And I select "Test-Comp1" of the competency tree
+    And I click on "Add" "button" in the "Competency picker" "dialogue"
+    And I press "Add competencies to course"
+    And "Competency picker" "dialogue" should be visible
+    And I select "Test-Comp2" of the competency tree
+    And I click on "Add" "button" in the "Competency picker" "dialogue"
+    And I am on "Course 1" course homepage
+    And I follow "PageName1"
+    And I navigate to "Edit settings" in current page administration
+    And I follow "Expand all"
+    And I set the field "Course competencies" to "Test-Comp1"
+    And I press "Save and return to course"
+    And I log out
+
+  @javascript
+  Scenario: Go to the competency course competencies page.
+    When I log in as "student1"
+    And I am on site homepage
+    And I follow "Course 1"
+    And I follow "Competencies"
+    Then I should see "Test-Comp1"
+    And I should see "Test-Comp2"
+    And I set the field "Filter competencies by resource or activity" to "PageName1"
+    And I press key "13" in the field "Filter competencies by resource or activity"
+    And I should see "Test-Comp1"
+    And I should not see "Test-Comp2"
+    And I set the field "Filter competencies by resource or activity" to "PageName2"
+    And I press key "13" in the field "Filter competencies by resource or activity"
+    And I should not see "Test-Comp1"
+    And I should not see "Test-Comp2"
+    And I should see "No competencies have been linked to this activity or resource."
index 44c9521..450570a 100644 (file)
@@ -461,4 +461,33 @@ class tool_lp_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals('A', $summary->evidence[1]->gradename);
     }
 
+    public function test_data_for_course_competency_page() {
+        $this->setAdminUser();
+
+        $dg = $this->getDataGenerator();
+        $lpg = $dg->get_plugin_generator('core_competency');
+        $f1 = $lpg->create_framework();
+        $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get('id')));
+        $course1 = $dg->create_course(array('category' => $this->category->id));
+        $cc = api::add_competency_to_course($course1->id, $c1->get('id'));
+
+        $evidence = \core_competency\external::grade_competency($this->user->id, $c1->get('id'), 1, true);
+        $evidence = \core_competency\external::grade_competency($this->user->id, $c1->get('id'), 2, true);
+
+        $pagegenerator = $this->getDataGenerator()->get_plugin_generator('mod_page');
+        $page = $pagegenerator->create_instance(array('course' => $course1->id));
+        $page2 = $pagegenerator->create_instance(array('course' => $course1->id));
+
+        $cm = get_coursemodule_from_instance('page', $page->id);
+        $cm2 = get_coursemodule_from_instance('page', $page2->id);
+        // Add the competency to the course module.
+        $ccm = api::add_competency_to_course_module($cm, $c1->get('id'));
+        $summary = external::data_for_course_competencies_page($course1->id, 0);
+        $summary2 = external::data_for_course_competencies_page($course1->id, $cm->id);
+        $summary3 = external::data_for_course_competencies_page($course1->id, $cm2->id);
+
+        $this->assertEquals(count($summary->competencies), 1);
+        $this->assertEquals(count($summary->competencies), count($summary2->competencies));
+        $this->assertEquals(count($summary3->competencies), 0);
+    }
 }
index 05dafdd..a3bdb56 100644 (file)
@@ -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)));
 }
index f67a304..cf5b433 100644 (file)
@@ -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'));
index 67b4144..ec08d0f 100644 (file)
@@ -28,6 +28,7 @@ define('AJAX_SCRIPT', true);
 define('NO_MOODLE_COOKIES', true); // No need for a session here.
 
 require_once(__DIR__ . '/../config.php');
+require_once($CFG->libdir . '/badgeslib.php');
 
 if (empty($CFG->enablebadges)) {
     print_error('badgesdisabled', 'badges');
diff --git a/badges/backpack-add.php b/badges/backpack-add.php
new file mode 100644 (file)
index 0000000..9c57520
--- /dev/null
@@ -0,0 +1,97 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Optionally award a badge and redirect to the my badges page.
+ *
+ * @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($backpack->externalbackpackid);
+    $assertion = new core_badges_assertion($id, $sitebackpack->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'));
+}
index 2282907..3ecdf77 100644 (file)
@@ -1,5 +1,6 @@
 /**
  * Push badges to backpack.
+ * @deprecated since 3.7
  */
 function addtobackpack(event, args) {
     var badgetable = Y.one('#issued-badge-table');
@@ -20,6 +21,7 @@ function addtobackpack(event, args) {
 
 /**
  * Check if website is externally accessible from the backpack.
+ * @deprecated since 3.7
  */
 function check_site_access() {
     var add = Y.one('#check_connection');
index d9dfda2..9b563ff 100644 (file)
  */
 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,20 @@ $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->email = $storedemail;
-        $bp = new OpenBadgesBackpackHandler($data);
+        $data->password = $password;
+        $data->externalbackpackid = $backpackid;
+        $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 +57,11 @@ if (!is_null($storedsecret)) {
         $obj = new stdClass();
         $obj->userid = $USER->id;
         $obj->email = $data->email;
-        $obj->backpackurl = $data->backpackurl;
+        $obj->externalbackpackid = $backpackid;
         $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 (file)
index 0000000..341d511
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * 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();
index 560bb88..23a59a6 100644 (file)
@@ -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';
index 2362bad..0f09064 100644 (file)
@@ -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) {
index e1e0451..9525019 100644 (file)
@@ -87,34 +87,72 @@ 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) {
             $hash = $this->_data->uniquehash;
             $email = empty($this->_data->backpackemail) ? $this->_data->email : $this->_data->backpackemail;
-            $assertionurl = new moodle_url('/badges/assertion.php', array('b' => $hash));
+            $assertionurl = new moodle_url('/badges/assertion.php', array('b' => $hash, 'obversion' => $this->_obversion));
             $classurl = new moodle_url('/badges/assertion.php', array('b' => $hash, 'action' => 1));
 
             // 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, '/', 'f3.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,14 +210,21 @@ class core_badges_assertion {
      * @return array Issuer information.
      */
     public function get_issuer() {
+        global $CFG;
         $issuer = array();
         if ($this->_data) {
             // Required.
-            $issuer['name'] = $this->_data->issuername;
-            $issuer['url'] = $this->_data->issuerurl;
-            // Optional.
-            if (!empty($this->_data->issuercontact)) {
-                $issuer['email'] = $this->_data->issuercontact;
+            if (badges_open_badges_backpack_api() == OPEN_BADGES_V1) {
+                $issuer['name'] = $this->_data->issuername;
+                $issuer['url'] = $this->_data->issuerurl;
+                // Optional.
+                if (!empty($this->_data->issuercontact)) {
+                    $issuer['email'] = $this->_data->issuercontact;
+                } else {
+                    $issuer['email'] = $CFG->badges_defaultissuercontact;
+                }
+            } else {
+                $issuer = badges_get_default_issuer();
             }
         }
         $this->embed_data_badge_version2($issuer, OPEN_BADGES_V2_TYPE_ISSUER);
@@ -284,7 +346,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 +363,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 (file)
index 0000000..b525837
--- /dev/null
@@ -0,0 +1,658 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 <yuliya.bozhko@totaralms.com>
+ */
+
+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;
+use context_system;
+
+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.
+ *
+ * @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
+ */
+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)) {
+            if ($userbackpack->externalbackpackid != $sitebackpack->id) {
+                throw new coding_exception('Incorrect backpack');
+            }
+            $this->isuserbackpack = true;
+            $this->password = $userbackpack->password;
+            $this->email = $userbackpack->email;
+        }
+
+        $this->define_mappings();
+        // Clear the last authentication error.
+        backpack_api_mapping::set_authentication_error('');
+    }
+
+    /**
+     * 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[] = [
+                    'assertion',                                // 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/assertions/[PARAM2]?expand=badgeclass&expand=issuer',
+                    [],                                         // 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);
+    }
+
+    /**
+     * 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);
+    }
+
+    /**
+     * 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 last error message returned during an authentication request.
+     *
+     * @return string
+     */
+    public function get_authentication_error() {
+        return backpack_api_mapping::get_authentication_error();
+    }
+
+    /**
+     * 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) {
+        global $PAGE;
+
+        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 = [];
+                    $context = context_system::instance();
+                    $output = $PAGE->get_renderer('core', 'badges');
+                    foreach ($badges->assertions as $assertion) {
+                        $remoteassertion = $this->get_assertion($assertion);
+                        // Remote badge was fetched nested in the assertion.
+                        $remotebadge = $remoteassertion->badgeclass;
+                        if (!$remotebadge) {
+                            continue;
+                        }
+                        $apidata = badgeclass_exporter::map_external_data($remotebadge, $this->backpackapiversion);
+                        $exporterinstance = new badgeclass_exporter($apidata, ['context' => $context]);
+                        $remotebadge = $exporterinstance->export($output);
+
+                        $remoteissuer = $remotebadge->issuer;
+                        $apidata = issuer_exporter::map_external_data($remoteissuer, $this->backpackapiversion);
+                        $exporterinstance = new issuer_exporter($apidata, ['context' => $context]);
+                        $remoteissuer = $exporterinstance->export($output);
+
+                        $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 (file)
index 0000000..efd57c4
--- /dev/null
@@ -0,0 +1,399 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 <yuliya.bozhko@totaralms.com>
+ */
+
+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.
+ *
+ * @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
+ */
+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;
+
+    /** @var string Error string from authentication request. */
+    private static $authenticationerror = '';
+
+    /**
+     * 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;
+    }
+
+    /**
+     * Remember the error message in a static variable.
+     *
+     * @param string $msg The message.
+     */
+    public static function set_authentication_error($msg) {
+        self::$authenticationerror = $msg;
+    }
+
+    /**
+     * Get the last authentication error in this request.
+     *
+     * @return string
+     */
+    public static function get_authentication_error() {
+        return self::$authenticationerror;
+    }
+
+    /**
+     * 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 mixed
+     */
+    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;
+        }
+        if (!empty($response->error)) {
+            self::set_authentication_error($response->error);
+        }
+        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;
+        } else if (isset($response->error_description)) {
+            self::set_authentication_error($response->error_description);
+        }
+        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 (file)
index 0000000..7334681
--- /dev/null
@@ -0,0 +1,941 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 <yuliya.bozhko@totaralms.com>
+ */
+
+namespace core_badges;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot.'/lib/badgeslib.php');
+
+use context_system;
+use context_course;
+use context_user;
+use moodle_exception;
+use moodle_url;
+use core_text;
+use award_criteria;
+use core_php_time_limit;
+use html_writer;
+use stdClass;
+
+/**
+ * Class that represents badge.
+ *
+ * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class badge {
+    /** @var int Badge id */
+    public $id;
+
+    /** @var string Badge name */
+    public $name;
+
+    /** @var string Badge description */
+    public $description;
+
+    /** @var integer Timestamp this badge was created */
+    public $timecreated;
+
+    /** @var integer Timestamp this badge was modified */
+    public $timemodified;
+
+    /** @var int The user who created this badge */
+    public $usercreated;
+
+    /** @var int The user who modified this badge */
+    public $usermodified;
+
+    /** @var string The name of the issuer of this badge */
+    public $issuername;
+
+    /** @var string The url of the issuer of this badge */
+    public $issuerurl;
+
+    /** @var string The email of the issuer of this badge */
+    public $issuercontact;
+
+    /** @var integer Timestamp this badge will expire */
+    public $expiredate;
+
+    /** @var integer Duration this badge is valid for */
+    public $expireperiod;
+
+    /** @var integer Site or course badge */
+    public $type;
+
+    /** @var integer The course this badge belongs to */
+    public $courseid;
+
+    /** @var string The message this badge includes. */
+    public $message;
+
+    /** @var string The subject of the message for this badge */
+    public $messagesubject;
+
+    /** @var int Is this badge image baked. */
+    public $attachment;
+
+    /** @var int Send a message when this badge is awarded. */
+    public $notification;
+
+    /** @var int Lifecycle status for this badge. */
+    public $status = 0;
+
+    /** @var int Timestamp to next run cron for this badge. */
+    public $nextcron;
+
+    /** @var int What backpack api version to use for this badge. */
+    public $version;
+
+    /** @var string What language is this badge written in. */
+    public $language;
+
+    /** @var string The author of the image for this badge. */
+    public $imageauthorname;
+
+    /** @var string The email of the author of the image for this badge. */
+    public $imageauthoremail;
+
+    /** @var string The url of the author of the image for this badge. */
+    public $imageauthorurl;
+
+    /** @var string The caption of the image for this badge. */
+    public $imagecaption;
+
+    /** @var array Badge criteria */
+    public $criteria = array();
+
+    /**
+     * Constructs with badge details.
+     *
+     * @param int $badgeid badge ID.
+     */
+    public function __construct($badgeid) {
+        global $DB;
+        $this->id = $badgeid;
+
+        $data = $DB->get_record('badge', array('id' => $badgeid));
+
+        if (empty($data)) {
+            print_error('error:nosuchbadge', 'badges', $badgeid);
+        }
+
+        foreach ((array)$data as $field => $value) {
+            if (property_exists($this, $field)) {
+                $this->{$field} = $value;
+            }
+        }
+
+        if (badges_open_badges_backpack_api() != OPEN_BADGES_V1) {
+            // For Open Badges 2 we need to use a single site issuer with no exceptions.
+            $issuer = badges_get_default_issuer();
+            $this->issuername = $issuer['name'];
+            $this->issuercontact = $issuer['email'];
+            $this->issuerurl = $issuer['url'];
+        }
+
+        $this->criteria = self::get_criteria();
+    }
+
+    /**
+     * Use to get context instance of a badge.
+     *
+     * @return context instance.
+     */
+    public function get_context() {
+        if ($this->type == BADGE_TYPE_SITE) {
+            return context_system::instance();
+        } else if ($this->type == BADGE_TYPE_COURSE) {
+            return context_course::instance($this->courseid);
+        } else {
+            debugging('Something is wrong...');
+        }
+    }
+
+    /**
+     * Return array of aggregation methods
+     *
+     * @return array
+     */
+    public static function get_aggregation_methods() {
+        return array(
+                BADGE_CRITERIA_AGGREGATION_ALL => get_string('all', 'badges'),
+                BADGE_CRITERIA_AGGREGATION_ANY => get_string('any', 'badges'),
+        );
+    }
+
+    /**
+     * Return array of accepted criteria types for this badge
+     *
+     * @return array
+     */
+    public function get_accepted_criteria() {
+        global $CFG;
+        $criteriatypes = array();
+
+        if ($this->type == BADGE_TYPE_COURSE) {
+            $criteriatypes = array(
+                    BADGE_CRITERIA_TYPE_OVERALL,
+                    BADGE_CRITERIA_TYPE_MANUAL,
+                    BADGE_CRITERIA_TYPE_COURSE,
+                    BADGE_CRITERIA_TYPE_BADGE,
+                    BADGE_CRITERIA_TYPE_ACTIVITY,
+                    BADGE_CRITERIA_TYPE_COMPETENCY
+            );
+        } else if ($this->type == BADGE_TYPE_SITE) {
+            $criteriatypes = array(
+                    BADGE_CRITERIA_TYPE_OVERALL,
+                    BADGE_CRITERIA_TYPE_MANUAL,
+                    BADGE_CRITERIA_TYPE_COURSESET,
+                    BADGE_CRITERIA_TYPE_BADGE,
+                    BADGE_CRITERIA_TYPE_PROFILE,
+                    BADGE_CRITERIA_TYPE_COHORT,
+                    BADGE_CRITERIA_TYPE_COMPETENCY
+            );
+        }
+        $alltypes = badges_list_criteria();
+        foreach ($criteriatypes as $index => $type) {
+            if (!isset($alltypes[$type])) {
+                unset($criteriatypes[$index]);
+            }
+        }
+
+        return $criteriatypes;
+    }
+
+    /**
+     * Save/update badge information in 'badge' table only.
+     * Cannot be used for updating awards and criteria settings.
+     *
+     * @return boolean Returns true on success.
+     */
+    public function save() {
+        global $DB;
+
+        $fordb = new stdClass();
+        foreach (get_object_vars($this) as $k => $v) {
+            $fordb->{$k} = $v;
+        }
+        unset($fordb->criteria);
+
+        $fordb->timemodified = time();
+        if ($DB->update_record_raw('badge', $fordb)) {
+            // Trigger event, badge updated.
+            $eventparams = array('objectid' => $this->id, 'context' => $this->get_context());
+            $event = \core\event\badge_updated::create($eventparams);
+            $event->trigger();
+            return true;
+        } else {
+            throw new moodle_exception('error:save', 'badges');
+            return false;
+        }
+    }
+
+    /**
+     * Creates and saves a clone of badge with all its properties.
+     * Clone is not active by default and has 'Copy of' attached to its name.
+     *
+     * @return int ID of new badge.
+     */
+    public function make_clone() {
+        global $DB, $USER, $PAGE;
+
+        $fordb = new stdClass();
+        foreach (get_object_vars($this) as $k => $v) {
+            $fordb->{$k} = $v;
+        }
+
+        $fordb->name = get_string('copyof', 'badges', $this->name);
+        $fordb->status = BADGE_STATUS_INACTIVE;
+        $fordb->usercreated = $USER->id;
+        $fordb->usermodified = $USER->id;
+        $fordb->timecreated = time();
+        $fordb->timemodified = time();
+        unset($fordb->id);
+
+        if ($fordb->notification > 1) {
+            $fordb->nextcron = badges_calculate_message_schedule($fordb->notification);
+        }
+
+        $criteria = $fordb->criteria;
+        unset($fordb->criteria);
+
+        if ($new = $DB->insert_record('badge', $fordb, true)) {
+            $newbadge = new badge($new);
+
+            // Copy badge image.
+            $fs = get_file_storage();
+            if ($file = $fs->get_file($this->get_context()->id, 'badges', 'badgeimage', $this->id, '/', 'f1.png')) {
+                if ($imagefile = $file->copy_content_to_temp()) {
+                    badges_process_badge_image($newbadge, $imagefile);
+                }
+            }
+
+            // Copy badge criteria.
+            foreach ($this->criteria as $crit) {
+                $crit->make_clone($new);
+            }
+
+            // Trigger event, badge duplicated.
+            $eventparams = array('objectid' => $new, 'context' => $PAGE->context);
+            $event = \core\event\badge_duplicated::create($eventparams);
+            $event->trigger();
+
+            return $new;
+        } else {
+            throw new moodle_exception('error:clone', 'badges');
+            return false;
+        }
+    }
+
+    /**
+     * Checks if badges is active.
+     * Used in badge award.
+     *
+     * @return boolean A status indicating badge is active
+     */
+    public function is_active() {
+        if (($this->status == BADGE_STATUS_ACTIVE) ||
+            ($this->status == BADGE_STATUS_ACTIVE_LOCKED)) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Use to get the name of badge status.
+     *
+     * @return string
+     */
+    public function get_status_name() {
+        return get_string('badgestatus_' . $this->status, 'badges');
+    }
+
+    /**
+     * Use to set badge status.
+     * Only active badges can be earned/awarded/issued.
+     *
+     * @param int $status Status from BADGE_STATUS constants
+     */
+    public function set_status($status = 0) {
+        $this->status = $status;
+        $this->save();
+        if ($status == BADGE_STATUS_ACTIVE) {
+            // Trigger event, badge enabled.
+            $eventparams = array('objectid' => $this->id, 'context' => $this->get_context());
+            $event = \core\event\badge_enabled::create($eventparams);
+            $event->trigger();
+        } else if ($status == BADGE_STATUS_INACTIVE) {
+            // Trigger event, badge disabled.
+            $eventparams = array('objectid' => $this->id, 'context' => $this->get_context());
+            $event = \core\event\badge_disabled::create($eventparams);
+            $event->trigger();
+        }
+    }
+
+    /**
+     * Checks if badges is locked.
+     * Used in badge award and editing.
+     *
+     * @return boolean A status indicating badge is locked
+     */
+    public function is_locked() {
+        if (($this->status == BADGE_STATUS_ACTIVE_LOCKED) ||
+                ($this->status == BADGE_STATUS_INACTIVE_LOCKED)) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Checks if badge has been awarded to users.
+     * Used in badge editing.
+     *
+     * @return boolean A status indicating badge has been awarded at least once
+     */
+    public function has_awards() {
+        global $DB;
+        $awarded = $DB->record_exists_sql('SELECT b.uniquehash
+                    FROM {badge_issued} b INNER JOIN {user} u ON b.userid = u.id
+                    WHERE b.badgeid = :badgeid AND u.deleted = 0', array('badgeid' => $this->id));
+
+        return $awarded;
+    }
+
+    /**
+     * Gets list of users who have earned an instance of this badge.
+     *
+     * @return array An array of objects with information about badge awards.
+     */
+    public function get_awards() {
+        global $DB;
+
+        $awards = $DB->get_records_sql(
+                'SELECT b.userid, b.dateissued, b.uniquehash, u.firstname, u.lastname
+                    FROM {badge_issued} b INNER JOIN {user} u
+                        ON b.userid = u.id
+                    WHERE b.badgeid = :badgeid AND u.deleted = 0', array('badgeid' => $this->id));
+
+        return $awards;
+    }
+
+    /**
+     * Indicates whether badge has already been issued to a user.
+     *
+     * @param int $userid User to check
+     * @return boolean
+     */
+    public function is_issued($userid) {
+        global $DB;
+        return $DB->record_exists('badge_issued', array('badgeid' => $this->id, 'userid' => $userid));
+    }
+
+    /**
+     * Issue a badge to user.
+     *
+     * @param int $userid User who earned the badge
+     * @param boolean $nobake Not baking actual badges (for testing purposes)
+     */
+    public function issue($userid, $nobake = false) {
+        global $DB, $CFG;
+
+        $now = time();
+        $issued = new stdClass();
+        $issued->badgeid = $this->id;
+        $issued->userid = $userid;
+        $issued->uniquehash = sha1(rand() . $userid . $this->id . $now);
+        $issued->dateissued = $now;
+
+        if ($this->can_expire()) {
+            $issued->dateexpire = $this->calculate_expiry($now);
+        } else {
+            $issued->dateexpire = null;
+        }
+
+        // Take into account user badges privacy settings.
+        // If none set, badges default visibility is set to public.
+        $issued->visible = get_user_preferences('badgeprivacysetting', 1, $userid);
+
+        $result = $DB->insert_record('badge_issued', $issued, true);
+
+        if ($result) {
+            // Trigger badge awarded event.
+            $eventdata = array (
+                'context' => $this->get_context(),
+                'objectid' => $this->id,
+                'relateduserid' => $userid,
+                'other' => array('dateexpire' => $issued->dateexpire, 'badgeissuedid' => $result)
+            );
+            \core\event\badge_awarded::create($eventdata)->trigger();
+
+            // Lock the badge, so that its criteria could not be changed any more.
+            if ($this->status == BADGE_STATUS_ACTIVE) {
+                $this->set_status(BADGE_STATUS_ACTIVE_LOCKED);
+            }
+
+            // Update details in criteria_met table.
+            $compl = $this->get_criteria_completions($userid);
+            foreach ($compl as $c) {
+                $obj = new stdClass();
+                $obj->id = $c->id;
+                $obj->issuedid = $result;
+                $DB->update_record('badge_criteria_met', $obj, true);
+            }
+
+            if (!$nobake) {
+                // Bake a badge image.
+                $pathhash = badges_bake($issued->uniquehash, $this->id, $userid, true);
+
+                // Notify recipients and badge creators.
+                badges_notify_badge_award($this, $userid, $issued->uniquehash, $pathhash);
+            }
+        }
+    }
+
+    /**
+     * Reviews all badge criteria and checks if badge can be instantly awarded.
+     *
+     * @return int Number of awards
+     */
+    public function review_all_criteria() {
+        global $DB, $CFG;
+        $awards = 0;
+
+        // Raise timelimit as this could take a while for big web sites.
+        core_php_time_limit::raise();
+        raise_memory_limit(MEMORY_HUGE);
+
+        foreach ($this->criteria as $crit) {
+            // Overall criterion is decided when other criteria are reviewed.
+            if ($crit->criteriatype == BADGE_CRITERIA_TYPE_OVERALL) {
+                continue;
+            }
+
+            list($extrajoin, $extrawhere, $extraparams) = $crit->get_completed_criteria_sql();
+            // For site level badges, get all active site users who can earn this badge and haven't got it yet.
+            if ($this->type == BADGE_TYPE_SITE) {
+                $sql = "SELECT DISTINCT u.id, bi.badgeid
+                        FROM {user} u
+                        {$extrajoin}
+                        LEFT JOIN {badge_issued} bi
+                            ON u.id = bi.userid AND bi.badgeid = :badgeid
+                        WHERE bi.badgeid IS NULL AND u.id != :guestid AND u.deleted = 0 " . $extrawhere;
+                $params = array_merge(array('badgeid' => $this->id, 'guestid' => $CFG->siteguest), $extraparams);
+                $toearn = $DB->get_fieldset_sql($sql, $params);
+            } else {
+                // For course level badges, get all users who already earned the badge in this course.
+                // Then find the ones who are enrolled in the course and don't have a badge yet.
+                $earned = $DB->get_fieldset_select(
+                    'badge_issued',
+                    'userid AS id',
+                    'badgeid = :badgeid',
+                    array('badgeid' => $this->id)
+                );
+
+                $wheresql = '';
+                $earnedparams = array();
+                if (!empty($earned)) {
+                    list($earnedsql, $earnedparams) = $DB->get_in_or_equal($earned, SQL_PARAMS_NAMED, 'u', false);
+                    $wheresql = ' WHERE u.id ' . $earnedsql;
+                }
+                list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->get_context(), 'moodle/badges:earnbadge', 0, true);
+                $sql = "SELECT DISTINCT u.id
+                        FROM {user} u
+                        {$extrajoin}
+                        JOIN ({$enrolledsql}) je ON je.id = u.id " . $wheresql . $extrawhere;
+                $params = array_merge($enrolledparams, $earnedparams, $extraparams);
+                $toearn = $DB->get_fieldset_sql($sql, $params);
+            }
+
+            foreach ($toearn as $uid) {
+                $reviewoverall = false;
+                if ($crit->review($uid, true)) {
+                    $crit->mark_complete($uid);
+                    if ($this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->method == BADGE_CRITERIA_AGGREGATION_ANY) {
+                        $this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->mark_complete($uid);
+                        $this->issue($uid);
+                        $awards++;
+                    } else {
+                        $reviewoverall = true;
+                    }
+                } else {
+                    // Will be reviewed some other time.
+                    $reviewoverall = false;
+                }
+                // Review overall if it is required.
+                if ($reviewoverall && $this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->review($uid)) {
+                    $this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->mark_complete($uid);
+                    $this->issue($uid);
+                    $awards++;
+                }
+            }
+        }
+
+        return $awards;
+    }
+
+    /**
+     * Gets an array of completed criteria from 'badge_criteria_met' table.
+     *
+     * @param int $userid Completions for a user
+     * @return array Records of criteria completions
+     */
+    public function get_criteria_completions($userid) {
+        global $DB;
+        $completions = array();
+        $sql = "SELECT bcm.id, bcm.critid
+                FROM {badge_criteria_met} bcm
+                    INNER JOIN {badge_criteria} bc ON bcm.critid = bc.id
+                WHERE bc.badgeid = :badgeid AND bcm.userid = :userid ";
+        $completions = $DB->get_records_sql($sql, array('badgeid' => $this->id, 'userid' => $userid));
+
+        return $completions;
+    }
+
+    /**
+     * Checks if badges has award criteria set up.
+     *
+     * @return boolean A status indicating badge has at least one criterion
+     */
+    public function has_criteria() {
+        if (count($this->criteria) > 0) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Returns badge award criteria
+     *
+     * @return array An array of badge criteria
+     */
+    public function get_criteria() {
+        global $DB;
+        $criteria = array();
+
+        if ($records = (array)$DB->get_records('badge_criteria', array('badgeid' => $this->id))) {
+            foreach ($records as $record) {
+                $criteria[$record->criteriatype] = award_criteria::build((array)$record);
+            }
+        }
+
+        return $criteria;
+    }
+
+    /**
+     * Get aggregation method for badge criteria
+     *
+     * @param int $criteriatype If none supplied, get overall aggregation method (optional)
+     * @return int One of BADGE_CRITERIA_AGGREGATION_ALL or BADGE_CRITERIA_AGGREGATION_ANY
+     */
+    public function get_aggregation_method($criteriatype = 0) {
+        global $DB;
+        $params = array('badgeid' => $this->id, 'criteriatype' => $criteriatype);
+        $aggregation = $DB->get_field('badge_criteria', 'method', $params, IGNORE_MULTIPLE);
+
+        if (!$aggregation) {
+            return BADGE_CRITERIA_AGGREGATION_ALL;
+        }
+
+        return $aggregation;
+    }
+
+    /**
+     * Checks if badge has expiry period or date set up.
+     *
+     * @return boolean A status indicating badge can expire
+     */
+    public function can_expire() {
+        if ($this->expireperiod || $this->expiredate) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Calculates badge expiry date based on either expirydate or expiryperiod.
+     *
+     * @param int $timestamp Time of badge issue
+     * @return int A timestamp
+     */
+    public function calculate_expiry($timestamp) {
+        $expiry = null;
+
+        if (isset($this->expiredate)) {
+            $expiry = $this->expiredate;
+        } else if (isset($this->expireperiod)) {
+            $expiry = $timestamp + $this->expireperiod;
+        }
+
+        return $expiry;
+    }
+
+    /**
+     * Checks if badge has manual award criteria set.
+     *
+     * @return boolean A status indicating badge can be awarded manually
+     */
+    public function has_manual_award_criteria() {
+        foreach ($this->criteria as $criterion) {
+            if ($criterion->criteriatype == BADGE_CRITERIA_TYPE_MANUAL) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Fully deletes the badge or marks it as archived.
+     *
+     * @param boolean $archive Achive a badge without actual deleting of any data.
+     */
+    public function delete($archive = true) {
+        global $DB;
+
+        if ($archive) {
+            $this->status = BADGE_STATUS_ARCHIVED;
+            $this->save();
+
+            // Trigger event, badge archived.
+            $eventparams = array('objectid' => $this->id, 'context' => $this->get_context());
+            $event = \core\event\badge_archived::create($eventparams);
+            $event->trigger();
+            return;
+        }
+
+        $fs = get_file_storage();
+
+        // Remove all issued badge image files and badge awards.
+        // Cannot bulk remove area files here because they are issued in user context.
+        $awards = $this->get_awards();
+        foreach ($awards as $award) {
+            $usercontext = context_user::instance($award->userid);
+            $fs->delete_area_files($usercontext->id, 'badges', 'userbadge', $this->id);
+        }
+        $DB->delete_records('badge_issued', array('badgeid' => $this->id));
+
+        // Remove all badge criteria.
+        $criteria = $this->get_criteria();
+        foreach ($criteria as $criterion) {
+            $criterion->delete();
+        }
+
+        // Delete badge images.
+        $badgecontext = $this->get_context();
+        $fs->delete_area_files($badgecontext->id, 'badges', 'badgeimage', $this->id);
+
+        // Delete endorsements, competencies and related badges.
+        $DB->delete_records('badge_endorsement', array('badgeid' => $this->id));
+        $relatedsql = 'badgeid = :badgeid OR relatedbadgeid = :relatedbadgeid';
+        $relatedparams = array(
+            'badgeid' => $this->id,
+            'relatedbadgeid' => $this->id
+        );
+        $DB->delete_records_select('badge_related', $relatedsql, $relatedparams);
+        $DB->delete_records('badge_alignment', array('badgeid' => $this->id));
+
+        // Finally, remove badge itself.
+        $DB->delete_records('badge', array('id' => $this->id));
+
+        // Trigger event, badge deleted.
+        $eventparams = array('objectid' => $this->id,
+            'context' => $this->get_context(),
+            'other' => array('badgetype' => $this->type, 'courseid' => $this->courseid)
+            );
+        $event = \core\event\badge_deleted::create($eventparams);
+        $event->trigger();
+    }
+
+    /**
+     * Add multiple related badges.
+     *
+     * @param array $relatedids Id of badges.
+     */
+    public function add_related_badges($relatedids) {
+        global $DB;
+        $relatedbadges = array();
+        foreach ($relatedids as $relatedid) {
+            $relatedbadge = new stdClass();
+            $relatedbadge->badgeid = $this->id;
+            $relatedbadge->relatedbadgeid = $relatedid;
+            $relatedbadges[] = $relatedbadge;
+        }
+        $DB->insert_records('badge_related', $relatedbadges);
+    }
+
+    /**
+     * Delete an related badge.
+     *
+     * @param int $relatedid Id related badge.
+     * @return boolean A status for delete an related badge.
+     */
+    public function delete_related_badge($relatedid) {
+        global $DB;
+        $sql = "(badgeid = :badgeid AND relatedbadgeid = :relatedid) OR " .
+               "(badgeid = :relatedid2 AND relatedbadgeid = :badgeid2)";
+        $params = ['badgeid' => $this->id, 'badgeid2' => $this->id, 'relatedid' => $relatedid, 'relatedid2' => $relatedid];
+        return $DB->delete_records_select('badge_related', $sql, $params);
+    }
+
+    /**
+     * Checks if badge has related badges.
+     *
+     * @return boolean A status related badge.
+     */
+    public function has_related() {
+        global $DB;
+        $sql = "SELECT DISTINCT b.id
+                    FROM {badge_related} br
+                    JOIN {badge} b ON (br.relatedbadgeid = b.id OR br.badgeid = b.id)
+                   WHERE (br.badgeid = :badgeid OR br.relatedbadgeid = :badgeid2) AND b.id != :badgeid3";
+        return $DB->record_exists_sql($sql, ['badgeid' => $this->id, 'badgeid2' => $this->id, 'badgeid3' => $this->id]);
+    }
+
+    /**
+     * Get related badges of badge.
+     *
+     * @param boolean $activeonly Do not get the inactive badges when is true.
+     * @return array Related badges information.
+     */
+    public function get_related_badges($activeonly = false) {
+        global $DB;
+
+        $params = array('badgeid' => $this->id, 'badgeid2' => $this->id, 'badgeid3' => $this->id);
+        $query = "SELECT DISTINCT b.id, b.name, b.version, b.language, b.type
+                    FROM {badge_related} br
+                    JOIN {badge} b ON (br.relatedbadgeid = b.id OR br.badgeid = b.id)
+                   WHERE (br.badgeid = :badgeid OR br.relatedbadgeid = :badgeid2) AND b.id != :badgeid3";
+        if ($activeonly) {
+            $query .= " AND b.status <> :status";
+            $params['status'] = BADGE_STATUS_INACTIVE;
+        }
+        $relatedbadges = $DB->get_records_sql($query, $params);
+        return $relatedbadges;
+    }
+
+    /**
+     * Insert/update alignment information of badge.
+     *
+     * @param stdClass $alignment Data of a alignment.
+     * @param int $alignmentid ID alignment.
+     * @return bool|int A status/ID when insert or update data.
+     */
+    public function save_alignment($alignment, $alignmentid = 0) {
+        global $DB;
+
+        $record = $DB->record_exists('badge_alignment', array('id' => $alignmentid));
+        if ($record) {
+            $alignment->id = $alignmentid;
+            return $DB->update_record('badge_alignment', $alignment);
+        } else {
+            return $DB->insert_record('badge_alignment', $alignment, true);
+        }
+    }
+
+    /**
+     * Delete a alignment of badge.
+     *
+     * @param int $alignmentid ID alignment.
+     * @return boolean A status for delete a alignment.
+     */
+    public function delete_alignment($alignmentid) {
+        global $DB;
+        return $DB->delete_records('badge_alignment', array('badgeid' => $this->id, 'id' => $alignmentid));
+    }
+
+    /**
+     * Get alignments of badge.
+     *
+     * @return array List content alignments.
+     */
+    public function get_alignments() {
+        global $DB;
+        return $DB->get_records('badge_alignment', array('badgeid' => $this->id));
+    }
+
+    /**
+     * Insert/update Endorsement information of badge.
+     *
+     * @param stdClass $endorsement Data of an endorsement.
+     * @return bool|int A status/ID when insert or update data.
+     */
+    public function save_endorsement($endorsement) {
+        global $DB;
+        $record = $DB->get_record('badge_endorsement', array('badgeid' => $this->id));
+        if ($record) {
+            $endorsement->id = $record->id;
+            return $DB->update_record('badge_endorsement', $endorsement);
+        } else {
+            return $DB->insert_record('badge_endorsement', $endorsement, true);
+        }
+    }
+
+    /**
+     * Get endorsement of badge.
+     *
+     * @return array|stdClass Endorsement information.
+     */
+    public function get_endorsement() {
+        global $DB;
+        return $DB->get_record('badge_endorsement', array('badgeid' => $this->id));
+    }
+
+    /**
+     * Markdown language support for criteria.
+     *
+     * @return string $output Markdown content to output.
+     */
+    public function markdown_badge_criteria() {
+        $agg = $this->get_aggregation_methods();
+        if (empty($this->criteria)) {
+            return get_string('nocriteria', 'badges');
+        }
+        $overalldescr = '';
+        $overall = $this->criteria[BADGE_CRITERIA_TYPE_OVERALL];
+        if (!empty($overall->description)) {
+                $overalldescr = format_text($overall->description, $overall->descriptionformat,
+                    array('context' => $this->get_context())) . '\n';
+        }
+        // Get the condition string.
+        if (count($this->criteria) == 2) {
+            $condition = get_string('criteria_descr', 'badges');
+        } else {
+            $condition = get_string('criteria_descr_' . BADGE_CRITERIA_TYPE_OVERALL, 'badges',
+                core_text::strtoupper($agg[$this->get_aggregation_method()]));
+        }
+        unset($this->criteria[BADGE_CRITERIA_TYPE_OVERALL]);
+        $items = array();
+        // If only one criterion left, make sure its description goe to the top.
+        if (count($this->criteria) == 1) {
+            $c = reset($this->criteria);
+            if (!empty($c->description)) {
+                $overalldescr = $c->description . '\n';
+            }
+            if (count($c->params) == 1) {
+                $items[] = ' * ' . get_string('criteria_descr_single_' . $c->criteriatype, 'badges') .
+                    $c->get_details();
+            } else {
+                $items[] = '* ' . get_string('criteria_descr_' . $c->criteriatype, 'badges',
+                        core_text::strtoupper($agg[$this->get_aggregation_method($c->criteriatype)])) .
+                    $c->get_details();
+            }
+        } else {
+            foreach ($this->criteria as $type => $c) {
+                $criteriadescr = '';
+                if (!empty($c->description)) {
+                    $criteriadescr = $c->description;
+                }
+                if (count($c->params) == 1) {
+                    $items[] = ' * ' . get_string('criteria_descr_single_' . $type, 'badges') .
+                        $c->get_details() . $criteriadescr;
+                } else {
+                    $items[] = '* ' . get_string('criteria_descr_' . $type, 'badges',
+                            core_text::strtoupper($agg[$this->get_aggregation_method($type)])) .
+                        $c->get_details() . $criteriadescr;
+                }
+            }
+        }
+        return strip_tags($overalldescr . $condition . html_writer::alist($items, array(), 'ul'));
+    }
+
+    /**
+     * Define issuer information by format Open Badges specification version 2.
+     *
+     * @return array Issuer informations of the badge.
+     */
+    public function get_badge_issuer() {
+        $issuer = array();
+        $issuerurl = new moodle_url('/badges/badge_json.php', array('id' => $this->id, 'action' => 0));
+        $issuer['name'] = $this->issuername;
+        $issuer['url'] = $this->issuerurl;
+        $issuer['email'] = $this->issuercontact;
+        $issuer['@context'] = OPEN_BADGES_V2_CONTEXT;
+        $issuer['id'] = $this->issuerurl;
+        $issuer['type'] = OPEN_BADGES_V2_TYPE_ISSUER;
+        return $issuer;
+    }
+}
index 8ef4b6e..df841f7 100644 (file)
@@ -176,7 +176,7 @@ class core_badges_external extends external_api {
             $related = array(
                 'context' => $context,
                 'endorsement' => $endorsement ? $endorsement : null,
-                'alignments' => $alignments,
+                'alignment' => $alignments,
                 'relatedbadges' => $relatedbadges,
             );
 
index 91a431c..d0b24ef 100644 (file)
@@ -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 (file)
index 0000000..4e1e393
--- /dev/null
@@ -0,0 +1,168 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * 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;
+    }
+
+    /**
+     * Return the list of additional properties.
+     *
+     * @return array
+     */
+    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.
+     *
+     * @param renderer_base $output
+     * @return array
+     */
+    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 (file)
index 0000000..0bc32da
--- /dev/null
@@ -0,0 +1,73 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 <dani@moodle.com>
+ * @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 (file)
index 0000000..f16d762
--- /dev/null
@@ -0,0 +1,232 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 ['alignment[]', '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;
+        } else {
+            $mapped->hostedUrl = $data->id;
+        }
+
+        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',
+            'alignment' => 'stdClass[]?',
+            'criteria' => 'stdClass?',
+        );
+    }
+
+    /**
+     * Return the list of additional properties.
+     *
+     * @return array
+     */
+    protected static function define_other_properties() {
+        return array(
+            'alignment' => 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.
+     *
+     * @param renderer_base $output
+     * @return array
+     */
+    protected function get_other_values(renderer_base $output) {
+        global $DB;
+        $result = [];
+
+        if (array_key_exists('alignment', $this->related) && $this->related['alignment'] !== null) {
+            $alignment = [];
+            foreach ($this->related['alignment'] as $alignment) {
+                $exporter = new alignment_exporter($alignment, $this->related);
+                $alignments[] = $exporter->export($output);
+            }
+            $result['alignment'] = $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 (file)
index 0000000..ac79f98
--- /dev/null
@@ -0,0 +1,110 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 (file)
index 0000000..d741031
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 (file)
index 0000000..79bbffa
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * 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',
+        );
+    }
+}
index cf07bd6..7146beb 100644 (file)
@@ -234,7 +234,7 @@ class user_badge_exporter extends exporter {
         return array(
             'context' => 'context',
             'endorsement' => 'stdClass?',
-            'alignments' => 'stdClass[]',
+            'alignment' => 'stdClass[]',
             'relatedbadges' => 'stdClass[]',
         );
     }
@@ -255,7 +255,7 @@ class user_badge_exporter extends exporter {
                 'description' => 'Badge endorsement',
                 'optional' => true,
             ],
-            'alignments' => [
+            'alignment' => [
                 'type' => alignment_exporter::read_properties_definition(),
                 'description' => 'Badge alignments',
                 'multiple' => true,
@@ -277,13 +277,13 @@ class user_badge_exporter extends exporter {
     protected function get_other_values(renderer_base $output) {
         $context = $this->related['context'];
         $endorsement = $this->related['endorsement'];
-        $alignments = $this->related['alignments'];
+        $alignments = $this->related['alignment'];
         $relatedbadges = $this->related['relatedbadges'];
 
         $values = array(
             'badgeurl' => moodle_url::make_webservice_pluginfile_url($context->id, 'badges', 'badgeimage', $this->data->id, '/',
                 'f1')->out(false),
-            'alignments' => array(),
+            'alignment' => array(),
             'relatedbadges' => array(),
         );
 
@@ -295,7 +295,7 @@ class user_badge_exporter extends exporter {
         if (!empty($alignments)) {
             foreach ($alignments as $alignment) {
                 $alignmentexporter = new alignment_exporter($alignment, array('context' => $context));
-                $values['alignments'][] = $alignmentexporter->export($output);
+                $values['alignment'][] = $alignmentexporter->export($output);
             }
         }
 
diff --git a/badges/classes/external/verification_exporter.php b/badges/classes/external/verification_exporter.php
new file mode 100644 (file)
index 0000000..2a2868e
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * 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.',
+            ]
+        ];
+    }
+}
similarity index 60%
rename from badges/backpack_form.php
rename to badges/classes/form/backpack.php
index 391b556..a060d85 100644 (file)
  * @author     Yuliya Bozhko <yuliya.bozhko@totaralms.com>
  */
 
+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', 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);
@@ -78,6 +92,13 @@ 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);
+            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'));
         }
     }
@@ -87,79 +108,25 @@ 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']);
             $check->email = $data['email'];
+            $check->password = $data['backpackpassword'];
+            $check->externalbackpackid = $backpack->id;
 
-            $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);
+                $msg = $bp->get_authentication_error();
+                if (!empty($msg)) {
+                    $errors['email'] .= '<br/><br/>';
+                    $errors['email'] .= get_string('backpackconnectionunexpectedmessage', 'badges', $msg);
                 }
             }
-            $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();
+        return $errors;
     }
 }
similarity index 71%
rename from badges/edit_form.php
rename to badges/classes/form/badge.php
index 4343985..f6d5e09 100644 (file)
  * @author     Yuliya Bozhko <yuliya.bozhko@totaralms.com>
  */
 
+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 (file)
index 0000000..6f221b7
--- /dev/null
@@ -0,0 +1,125 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 <yuliya.bozhko@totaralms.com>
+ */
+
+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.
+ *
+ * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class 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);
+        $sitebackpack = badges_get_site_backpack($backpack->backpackid);
+
+        $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');
+
+        $hasgroups = false;
+        if (!empty($groups)) {
+            foreach ($groups as $group) {
+                // Assertions or badges.
+                $count = 0;
+
+                if ($sitebackpack->apiversion == OPEN_BADGES_V2) {
+                    if (empty($group->published)) {
+                        // Only public collections.
+                        continue;
+                    }
+                }
+                if (!empty($group->assertions)) {
+                    $count = count($group->assertions);
+                }
+                if (!empty($group->badges)) {
+                    $count = count($group->badges);
+                }
+                if (!empty($group->groupId)) {
+                    $group->entityId = $group->groupId;
+                }
+                if (!$hasgroups) {
+                    $mform->addElement('static', 'selectgroup', '', get_string('selectgroup_start', 'badges'));
+                }
+                $hasgroups = true;
+                $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));
+        }
+        if (!$hasgroups) {
+            $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 (file)
index 0000000..7b7d653
--- /dev/null
@@ -0,0 +1,98 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 {
+
+    /**
+     * Create the form.
+     *
+     */
+    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 (file)
index 0000000..6e7d0b5
--- /dev/null
@@ -0,0 +1,94 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 <yuliya.bozhko@totaralms.com>
+ */
+
+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.
+ *
+ * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class message extends moodleform {
+
+    /**
+     * Create the form.
+     */
+    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);
+    }
+}
index 9452e91..511d2f4 100644 (file)
@@ -24,6 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+use \core_badges\badge;
 /**
  * Event observer for badges.
  */
diff --git a/badges/classes/output/badge_alignments.php b/badges/classes/output/badge_alignments.php
new file mode 100644 (file)
index 0000000..c54ddc1
--- /dev/null
@@ -0,0 +1,75 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 <yuliya.bozhko@totaralms.com>
+ */
+
+namespace core_badges\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/badgeslib.php');
+
+use renderable;
+
+/**
+ * Link to external resources this badge is aligned with.
+ *
+ * @copyright  2018 Tung Thai
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Tung Thai <Tung.ThaiDuc@nashtechglobal.com>
+ */
+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 (file)
index 0000000..9261a87
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 <yuliya.bozhko@totaralms.com>
+ */
+
+namespace core_badges\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/badgeslib.php');
+
+use renderable;
+
+/**
+ * Collection of all badges for view.php page
+ *
+ * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class badge_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 (file)
index 0000000..0ee70b0
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 <yuliya.bozhko@totaralms.com>
+ */
+
+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
+ *
+ * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class badge_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 (file)
index 0000000..cd6c843
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 <yuliya.bozhko@totaralms.com>
+ */
+
+namespace core_badges\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/badgeslib.php');
+
+use renderable;
+
+/**
+ * Badge recipients rendering class
+ *
+ * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class badge_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 (file)
index 0000000..c416704
--- /dev/null
@@ -0,0 +1,76 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 <yuliya.bozhko@totaralms.com>
+ */
+
+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 <Tung.ThaiDuc@nashtechglobal.com>
+ */
+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 (file)
index 0000000..36eb732
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 <yuliya.bozhko@totaralms.com>
+ */
+
+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
+ *
+ * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class badge_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 (file)
index 0000000..90086e4
--- /dev/null
@@ -0,0 +1,76 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 (file)
index 0000000..72393f1
--- /dev/null
@@ -0,0 +1,84 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * 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.
+     */
+    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 (file)
index 0000000..e992d9d
--- /dev/null
@@ -0,0 +1,100 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 <yuliya.bozhko@totaralms.com>
+ */
+
+namespace core_badges\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/badgeslib.php');
+
+use renderable;
+
+/**
+ * An external badges for external.php page
+ *
+ * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class 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 (file)
index 0000000..b669a19
--- /dev/null
@@ -0,0 +1,84 @@
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 <yuliya.bozhko@totaralms.com>
+ */
+
+namespace core_badges\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/badgeslib.php');
+
+use renderable;
+
+/**
+ * An issued badges for badge.php page
+ *
+ * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class 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;
+        }
+    }
+}
+
index a0bafb4..cd0c010 100644 (file)
@@ -92,7 +92,7 @@ class provider implements
         $collection->add_database_table('badge_backpack', [
             'userid' => 'privacy:metadata:backpack:userid',
             'email' => 'privacy:metadata:backpack:email',
-            'backpackurl' => 'privacy:metadata:backpack:backpackurl',
+            'externalbackpackid' => 'privacy:metadata:backpack:externalbackpackid',
             'backpackuid' => 'privacy:metadata:backpack:backpackuid',
             // The columns autosync and password are not used.
         ], 'privacy:metadata:backpack');
@@ -575,7 +575,7 @@ class provider implements
             foreach ($recordset as $record) {
                 $data[] = [
                     'email' => $record->email,
-                    'url' => $record->backpackurl,
+                    'externalbackpackid' => $record->externalbackpackid,
                     'uid' => $record->backpackuid
                 ];
             }
index 2521dc1..2307d0d 100644 (file)
 
 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();
index d05dbac..39850a2 100644 (file)
@@ -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 {
index e0b38bd..27c560d 100644 (file)
@@ -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 (file)
index 1fe156f..0000000
+++ /dev/null
@@ -1,161 +0,0 @@
-<?php
-// 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 <http://www.gnu.org/licenses/>.
-
-/**
- * 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 <yuliya.bozhko@totaralms.com>
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-global $CFG;
-require_once($CFG->libdir . '/filelib.php');
-
-// Adopted from https://github.com/jbkc85/openbadges-class-php.
-// Author Jason Cameron <jbkc85@gmail.com>.
-
-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);
-}
index 5a40338..bcb9482 100644 (file)
@@ -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,39 @@ $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($backpack->externalbackpackid);
+    // 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'));
 }
-
+$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');
+    }
+
     // 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 +97,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 +113,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()) {
@@ -130,12 +124,13 @@ if ($backpack) {
         // 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)) {
-            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);
@@ -148,5 +143,6 @@ if ($backpack) {
 
 echo $OUTPUT->header();
 echo $OUTPUT->heading($title);
+echo $warning;
 $form->display();
 echo $OUTPUT->footer();
index 97c4aee..26bffee 100644 (file)
@@ -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;
index 45081bc..9087b2d 100644 (file)
@@ -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();
index 244fef3..0dce38d 100644 (file)
@@ -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();
index d8af01b..066b5e9 100644 (file)
@@ -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'));
index a401c95..7e86ba2 100644 (file)
@@ -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,28 @@ 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)));
+                        $addurl = new moodle_url('#');
+                    } else {
+                        $addurl = new moodle_url('/badges/backpack-add.php', array('hash' => $badge->uniquehash));
+                    }
+                    $icon = new pix_icon('t/backpack', get_string('addtobackpack', 'badges'));
+                    $push = $this->output->action_icon($addurl, $icon, $action);
                 }
 
                 $download = $this->output->action_icon($url, new pix_icon('t/download', get_string('download')));
@@ -260,7 +287,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'))) . " ";
         }
 
@@ -280,8 +307,13 @@ class core_badges_renderer extends plugin_renderer_base {
         return $actions;
     }
 
-    // Outputs issued badge with actions available.
-    protected function render_issued_badge(issued_badge $ibadge) {
+    /**
+     * Render an issued badge.
+     *
+     * @param \core_badges\output\issued_badge $ibadge
+     * @return string
+     */
+    protected function render_issued_badge(\core_badges\output\issued_badge $ibadge) {
         global $USER, $CFG, $DB, $SITE;
         $issued = $ibadge->issued;
         $userinfo = $ibadge->recipient;
@@ -295,7 +327,7 @@ 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' => $badgeimage, 'alt' => $badge->name));
+        $output .= html_writer::empty_tag('img', array('src' => $badgeimage, 'alt' => $badge->name, 'width' => '100'));
         if ($expiration < $now) {
             $output .= $this->output->pix_icon('i/expired',
             get_string('expireddate', 'badges', userdate($issued['expires'])),
@@ -309,15 +341,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');
@@ -445,8 +486,13 @@ class core_badges_renderer extends plugin_renderer_base {
         return $output;
     }
 
-    // Outputs external badge.
-    protected function render_external_badge(external_badge $ibadge) {
+    /**
+     * Render an external badge.
+     *
+     * @param \core_badges\output\external_badge $ibadge
+     * @return string
+     */
+    protected function render_external_badge(\core_badges\output\external_badge $ibadge) {
         $issued = $ibadge->issued;
         $assertion = $issued->assertion;
         $issuer = $assertion->badge->issuer;
@@ -457,7 +503,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, 'width' => '100'));
         if (isset($assertion->expires)) {
             $expiration = !strtotime($assertion->expires) ? s($assertion->expires) : strtotime($assertion->expires);
             if ($expiration < $today) {
@@ -491,7 +540,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 +553,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 +575,29 @@ 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;
+    /**
+     * Render a collection of user badges.
+     *
+     * @param \core_badges\output\badge_user_collection $badges
+     * @return string
+     */
+    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 +613,24 @@ 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;
+            $localhtml .= $htmlpagingbar . $htmllist . $htmlpagingbar . $downloadall;
         } else {
             $localhtml .= $searchform . $this->output->notification(get_string('nobadges', 'badges'));
         }
@@ -570,13 +642,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 .= '<br/><br/>' . $this->print_badges_list($backpack->badges, $USER->id, true, true);
                     }
                 }
@@ -585,13 +661,25 @@ 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) {
+    /**
+     * Render a collection of badges.
+     *
+     * @param \core_badges\output\badge_collection $badges
+     * @return string
+     */
+    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();
@@ -633,8 +721,13 @@ class core_badges_renderer extends plugin_renderer_base {
         return $htmlpagingbar . $htmltable . $htmlpagingbar;
     }
 
-    // Outputs table of badges with actions available.
-    protected function render_badge_management(badge_management $badges) {
+    /**
+     * Render a table of badges.
+     *
+     * @param \core_badges\output\badge_management $badges
+     * @return string
+     */
+    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 +735,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);
@@ -686,7 +780,14 @@ class core_badges_renderer extends plugin_renderer_base {
         return $htmlnew . $htmlpagingbar . $htmltable . $htmlpagingbar;
     }
 
-    // Prints tabs for badge editing.
+    /**
+     * Prints tabs for badge editing.
+     *
+     * @param integer $badgeid The badgeid to edit.
+     * @param context $context The current context.
+     * @param string $current The currently selected tab.
+     * @return string
+     */
     public function print_badge_tabs($badgeid, $context, $current = 'overview') {
         global $DB;
 
@@ -700,7 +801,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')
                     );
         }
@@ -761,6 +862,8 @@ class core_badges_renderer extends plugin_renderer_base {
 
     /**
      * Prints badge status box.
+     *
+     * @param badge $badge
      * @return Either the status box html as a string or null
      */
     public function print_badge_status_box(badge $badge) {
@@ -880,7 +983,12 @@ class core_badges_renderer extends plugin_renderer_base {
         return $overalldescr . $condition . html_writer::alist($items, array(), 'ul');;
     }
 
-    // Prints criteria actions for badge editing.
+    /**
+     * Prints criteria actions for badge editing.
+     *
+     * @param badge $badge
+     * @return string
+     */
     public function print_criteria_actions(badge $badge) {
         $output = '';
         if (!$badge->is_active() && !$badge->is_locked()) {
@@ -910,9 +1018,14 @@ class core_badges_renderer extends plugin_renderer_base {
         return $output;
     }
 
-    // Renders a table with users who have earned the badge.
-    // Based on stamps collection plugin.
-    protected function render_badge_recipients(badge_recipients $recipients) {
+    /**
+     * Renders a table with users who have earned the badge.
+     * Based on stamps collection plugin.
+     *
+     * @param \core_badges\output\badge_recipients $recipients
+     * @return string
+     */
+    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 +1241,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 +1301,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 +1349,15 @@ 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
-     *
-     * @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;
-        }
-    }
-}
-
-/**
- * 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
-  &nbs