Merge branch 'MDL-53980-master-enfix' of git://github.com/mudrd8mz/moodle
authorDavid Monllao <davidm@moodle.com>
Mon, 2 May 2016 02:46:50 +0000 (10:46 +0800)
committerDavid Monllao <davidm@moodle.com>
Mon, 2 May 2016 02:46:50 +0000 (10:46 +0800)
261 files changed:
admin/tool/cohortroles/index.php
admin/tool/cohortroles/settings.php
admin/tool/lp/amd/build/competency_plan_navigation.min.js [new file with mode: 0644]
admin/tool/lp/amd/build/competencyactions.min.js
admin/tool/lp/amd/build/parentcompetency_form.min.js
admin/tool/lp/amd/build/user_competency_plan_popup.min.js [new file with mode: 0644]
admin/tool/lp/amd/src/competency_plan_navigation.js [new file with mode: 0644]
admin/tool/lp/amd/src/competencyactions.js
admin/tool/lp/amd/src/parentcompetency_form.js
admin/tool/lp/amd/src/user_competency_plan_popup.js [new file with mode: 0644]
admin/tool/lp/classes/external.php
admin/tool/lp/classes/external/user_competency_summary_exporter.php
admin/tool/lp/classes/form/competency.php
admin/tool/lp/classes/form/competency_framework.php
admin/tool/lp/classes/form/user_evidence.php
admin/tool/lp/classes/output/competency_plan_navigation.php [new file with mode: 0644]
admin/tool/lp/classes/output/course_competencies_page.php
admin/tool/lp/classes/output/renderer.php
admin/tool/lp/classes/output/user_competency_course_navigation.php
admin/tool/lp/competencyframeworks.php
admin/tool/lp/lang/en/tool_lp.php
admin/tool/lp/styles.css
admin/tool/lp/templates/competency_plan_navigation.mustache [new file with mode: 0644]
admin/tool/lp/templates/course_competencies_page.mustache
admin/tool/lp/templates/course_competency_statistics.mustache
admin/tool/lp/templates/manage_competency_frameworks_page.mustache
admin/tool/lp/templates/manage_templates_page.mustache
admin/tool/lp/templates/plan_page.mustache
admin/tool/lp/templates/plans_page.mustache
admin/tool/lp/templates/template_competencies_page.mustache
admin/tool/lp/templates/template_statistics.mustache
admin/tool/lp/templates/user_evidence_list_page.mustache
admin/tool/lp/templates/user_evidence_page.mustache
admin/tool/lp/tests/behat/behat_tool_lp.php
admin/tool/lp/user_competency_in_plan.php
admin/tool/lpmigrate/db/access.php
admin/webservice/testclient_forms.php
auth/lti/auth.php [new file with mode: 0644]
auth/lti/lang/en/auth_lti.php [new file with mode: 0644]
auth/lti/version.php [new file with mode: 0644]
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_stepslib.php
blocks/activity_modules/tests/behat/block_activity_modules.feature
blocks/feedback/block_feedback.php
blocks/feedback/db/install.php
blocks/feedback/lang/en/block_feedback.php
cache/stores/memcached/lib.php
competency/classes/competency.php
competency/classes/competency_framework.php
competency/tests/competency_test.php [new file with mode: 0644]
competency/tests/plan_test.php
course/externallib.php
enrol/editinstance.php
enrol/editinstance_form.php
enrol/externallib.php
enrol/lti/backup/moodle2/backup_enrol_lti_plugin.class.php [new file with mode: 0644]
enrol/lti/backup/moodle2/restore_enrol_lti_plugin.class.php [new file with mode: 0644]
enrol/lti/classes/helper.php [new file with mode: 0644]
enrol/lti/classes/manage_table.php [new file with mode: 0644]
enrol/lti/classes/task/sync_grades.php [new file with mode: 0644]
enrol/lti/classes/task/sync_members.php [new file with mode: 0644]
enrol/lti/db/access.php [new file with mode: 0644]
enrol/lti/db/install.xml [new file with mode: 0644]
enrol/lti/db/tasks.php [new file with mode: 0644]
enrol/lti/ims-blti/LICENSE.txt [new file with mode: 0644]
enrol/lti/ims-blti/OAuth.php [new file with mode: 0644]
enrol/lti/ims-blti/OAuthBody.php [new file with mode: 0644]
enrol/lti/ims-blti/TrivialOAuthDataStore.php [new file with mode: 0644]
enrol/lti/ims-blti/blti.php [new file with mode: 0644]
enrol/lti/ims-blti/blti_util.php [new file with mode: 0644]
enrol/lti/ims-blti/moodle_readme.txt [new file with mode: 0644]
enrol/lti/index.php [new file with mode: 0644]
enrol/lti/lang/en/enrol_lti.php [new file with mode: 0644]
enrol/lti/lib.php [new file with mode: 0644]
enrol/lti/settings.php [new file with mode: 0644]
enrol/lti/tests/behat/basic_settings.feature [new file with mode: 0644]
enrol/lti/tests/behat/index_page.feature [new file with mode: 0644]
enrol/lti/tests/helper_test.php [new file with mode: 0644]
enrol/lti/thirdpartylibs.xml [new file with mode: 0644]
enrol/lti/tool.php [new file with mode: 0644]
enrol/lti/version.php [new file with mode: 0644]
enrol/manual/db/services.php
enrol/manual/externallib.php
files/externallib.php
files/tests/externallib_test.php
grade/externallib.php [deleted file]
grade/grading/form/guide/amd/build/comment_chooser.min.js
grade/grading/form/guide/amd/src/comment_chooser.js
group/externallib.php
lang/en/competency.php
lang/en/error.php
lib/amd/build/form-autocomplete.min.js
lib/amd/src/form-autocomplete.js
lib/blocklib.php
lib/classes/dataformat/spout_base.php
lib/classes/plugin_manager.php
lib/db/access.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/formslib.php
lib/outputrenderers.php
lib/pagelib.php
lib/tablelib.php
lib/upgrade.txt
message/externallib.php
mod/assign/locallib.php
mod/feedback/README.txt [deleted file]
mod/feedback/TODO.txt [deleted file]
mod/feedback/amd/build/edit.min.js [new file with mode: 0644]
mod/feedback/amd/src/edit.js [new file with mode: 0644]
mod/feedback/analysis.php
mod/feedback/analysis_course.php
mod/feedback/analysis_to_excel.php [deleted file]
mod/feedback/backup/moodle2/backup_feedback_stepslib.php
mod/feedback/backup/moodle2/restore_feedback_stepslib.php
mod/feedback/classes/complete_form.php [new file with mode: 0644]
mod/feedback/classes/completion.php [new file with mode: 0644]
mod/feedback/classes/course_select_form.php [new file with mode: 0644]
mod/feedback/classes/event/course_module_viewed.php
mod/feedback/classes/event/response_deleted.php
mod/feedback/classes/event/response_submitted.php
mod/feedback/classes/output/summary.php [new file with mode: 0644]
mod/feedback/classes/responses_anon_table.php
mod/feedback/classes/responses_table.php
mod/feedback/classes/structure.php [new file with mode: 0644]
mod/feedback/classes/templates_table.php
mod/feedback/complete.php
mod/feedback/db/install.php
mod/feedback/db/install.xml
mod/feedback/db/upgrade.php
mod/feedback/db/upgradelib.php [new file with mode: 0644]
mod/feedback/delete_item.php [deleted file]
mod/feedback/delete_item_form.php [deleted file]
mod/feedback/delete_template.php
mod/feedback/edit.php
mod/feedback/edit_item.php
mod/feedback/item/captcha/lib.php
mod/feedback/item/feedback_item_class.php
mod/feedback/item/feedback_item_form_class.php
mod/feedback/item/info/lib.php
mod/feedback/item/label/lib.php
mod/feedback/item/multichoice/lib.php
mod/feedback/item/multichoice/multichoice_form.php
mod/feedback/item/multichoicerated/lib.php
mod/feedback/item/multichoicerated/multichoicerated_form.php
mod/feedback/item/numeric/lib.php
mod/feedback/item/textarea/lib.php
mod/feedback/item/textfield/lib.php
mod/feedback/lang/en/deprecated.txt
mod/feedback/lang/en/feedback.php
mod/feedback/lib.php
mod/feedback/print.php
mod/feedback/show_entries.php
mod/feedback/styles.css
mod/feedback/tabs.php
mod/feedback/templates/summary.mustache [new file with mode: 0644]
mod/feedback/tests/behat/anonymous.feature
mod/feedback/tests/behat/coursemapping.feature
mod/feedback/tests/behat/export_import.feature
mod/feedback/tests/behat/groups.feature
mod/feedback/tests/behat/multichoice.feature
mod/feedback/tests/behat/non_anonymous.feature
mod/feedback/tests/behat/question_types.feature
mod/feedback/tests/behat/question_types_non_anon.feature [new file with mode: 0644]
mod/feedback/tests/behat/show_nonrespondents.feature
mod/feedback/tests/behat/templates.feature
mod/feedback/tests/events_test.php
mod/feedback/tests/upgradelib_test.php [new file with mode: 0644]
mod/feedback/upgrade.txt
mod/feedback/use_templ.php
mod/feedback/version.php
mod/feedback/view.php
mod/feedback/yui/dragdrop/dragdrop.js
mod/forum/db/services.php
mod/forum/externallib.php
mod/forum/tests/externallib_test.php
mod/lti/ajax.php
mod/lti/amd/build/cartridge_registration_form.min.js [new file with mode: 0644]
mod/lti/amd/build/events.min.js [new file with mode: 0644]
mod/lti/amd/build/external_registration.min.js [new file with mode: 0644]
mod/lti/amd/build/external_registration_return.min.js [new file with mode: 0644]
mod/lti/amd/build/keys.min.js [new file with mode: 0644]
mod/lti/amd/build/tool_card_controller.min.js [new file with mode: 0644]
mod/lti/amd/build/tool_configure_controller.min.js [new file with mode: 0644]
mod/lti/amd/build/tool_proxy.min.js [new file with mode: 0644]
mod/lti/amd/build/tool_type.min.js [new file with mode: 0644]
mod/lti/amd/src/cartridge_registration_form.js [new file with mode: 0644]
mod/lti/amd/src/events.js [new file with mode: 0644]
mod/lti/amd/src/external_registration.js [new file with mode: 0644]
mod/lti/amd/src/external_registration_return.js [new file with mode: 0644]
mod/lti/amd/src/keys.js [new file with mode: 0644]
mod/lti/amd/src/tool_card_controller.js [new file with mode: 0644]
mod/lti/amd/src/tool_configure_controller.js [new file with mode: 0644]
mod/lti/amd/src/tool_proxy.js [new file with mode: 0644]
mod/lti/amd/src/tool_type.js [new file with mode: 0644]
mod/lti/classes/external.php
mod/lti/classes/output/external_registration_return_page.php [new file with mode: 0644]
mod/lti/classes/output/renderer.php [new file with mode: 0644]
mod/lti/classes/output/tool_configure_page.php [new file with mode: 0644]
mod/lti/db/install.xml
mod/lti/db/services.php
mod/lti/db/upgrade.php
mod/lti/edit_form.php
mod/lti/externalregistrationreturn.php [new file with mode: 0644]
mod/lti/instructor_edit_tool_type.php
mod/lti/lang/en/lti.php
mod/lti/lib.php
mod/lti/locallib.php
mod/lti/mod_form.js
mod/lti/mod_form.php
mod/lti/settings.php
mod/lti/styles.css
mod/lti/templates/cartridge_registration_form.mustache [new file with mode: 0644]
mod/lti/templates/external_registration.mustache [new file with mode: 0644]
mod/lti/templates/loader.mustache [new file with mode: 0644]
mod/lti/templates/registration_feedback.mustache [new file with mode: 0644]
mod/lti/templates/tool_card.mustache [new file with mode: 0644]
mod/lti/templates/tool_configure.mustache [new file with mode: 0644]
mod/lti/templates/tool_list.mustache [new file with mode: 0644]
mod/lti/templates/tool_proxy_registration_form.mustache [new file with mode: 0644]
mod/lti/templates/tool_type_capabilities_agree.mustache [new file with mode: 0644]
mod/lti/tests/behat/addtool.feature
mod/lti/tests/behat/addtype.feature [new file with mode: 0644]
mod/lti/tests/behat/toolconfigure.feature [new file with mode: 0644]
mod/lti/tests/externallib_test.php
mod/lti/tests/fixtures/ims_cartridge_basic_lti_link.xml [new file with mode: 0644]
mod/lti/tests/locallib_test.php
mod/lti/toolconfigure.php [new file with mode: 0644]
mod/lti/toolssettings.php
mod/lti/typessettings.php
mod/lti/version.php
mod/quiz/attemptlib.php
mod/wiki/classes/external.php
mod/wiki/db/services.php
mod/wiki/lang/en/wiki.php
mod/wiki/tests/externallib_test.php
mod/wiki/version.php
notes/externallib.php
report/competency/classes/output/user_course_navigation.php
search/classes/engine.php
search/classes/manager.php
search/classes/output/renderer.php
search/engine/solr/classes/engine.php
search/engine/solr/tests/engine_test.php
search/engine/solr/tests/fixtures/testable_engine.php [new file with mode: 0644]
search/index.php
search/tests/fixtures/mock_search_area.php
search/tests/fixtures/mock_search_engine.php
search/tests/generator/lib.php [new file with mode: 0644]
theme/bootstrapbase/less/moodle/modules.less
theme/bootstrapbase/renderers/core_renderer.php
theme/bootstrapbase/style/moodle.css
user/externallib.php
user/tests/externallib_test.php
version.php
webservice/externallib.php
webservice/lib.php
webservice/renderer.php
webservice/upgrade.txt
webservice/upload.php

index a202be6..d53af3e 100644 (file)
@@ -57,15 +57,15 @@ if ($removeid) {
     require_sesskey();
     // We must create them all or none.
     $saved = 0;
-    foreach ($data->userids as $userid) {
-        if (empty($data->cohortids)) {
-            $data->cohortids = array();
-        }
-        foreach ($data->cohortids as $cohortid) {
-            $params = (object) array('userid' => $userid, 'cohortid' => $cohortid, 'roleid' => $data->roleid);
-            $result = \tool_cohortroles\api::create_cohort_role_assignment($params);
-            if ($result) {
-                $saved++;
+    // Loop through userids and cohortids only if both of them are not empty.
+    if (!empty($data->userids) && !empty($data->cohortids)) {
+        foreach ($data->userids as $userid) {
+            foreach ($data->cohortids as $cohortid) {
+                $params = (object) array('userid' => $userid, 'cohortid' => $cohortid, 'roleid' => $data->roleid);
+                $result = \tool_cohortroles\api::create_cohort_role_assignment($params);
+                if ($result) {
+                    $saved++;
+                }
             }
         }
     }
index 98613c1..139a425 100644 (file)
  */
 
 defined('MOODLE_INTERNAL') || die;
-$str = get_string('managecohortroles', 'tool_cohortroles');
-$ADMIN->add('roles', new admin_externalpage('toolcohortroles', $str, '/admin/tool/cohortroles/index.php', 'moodle/role:manage'));
+
+// This tool's required capabilities.
+$capabilities = [
+    'moodle/cohort:view',
+    'moodle/role:manage'
+];
+
+// Check if the user has all of the required capabilities.
+$context = context_system::instance();
+$hasaccess = has_all_capabilities($capabilities, $context);
+
+// Add this admin page only if the user has all of the required capabilities.
+if ($hasaccess) {
+    $str = get_string('managecohortroles', 'tool_cohortroles');
+    $url = new moodle_url('/admin/tool/cohortroles/index.php');
+    $ADMIN->add('roles', new admin_externalpage('toolcohortroles', $str, $url, $capabilities));
+}
diff --git a/admin/tool/lp/amd/build/competency_plan_navigation.min.js b/admin/tool/lp/amd/build/competency_plan_navigation.min.js
new file mode 100644 (file)
index 0000000..1862243
Binary files /dev/null and b/admin/tool/lp/amd/build/competency_plan_navigation.min.js differ
index 76bb677..2ca219d 100644 (file)
Binary files a/admin/tool/lp/amd/build/competencyactions.min.js and b/admin/tool/lp/amd/build/competencyactions.min.js differ
index 0161053..d7732ee 100644 (file)
Binary files a/admin/tool/lp/amd/build/parentcompetency_form.min.js and b/admin/tool/lp/amd/build/parentcompetency_form.min.js differ
diff --git a/admin/tool/lp/amd/build/user_competency_plan_popup.min.js b/admin/tool/lp/amd/build/user_competency_plan_popup.min.js
new file mode 100644 (file)
index 0000000..1c320c8
Binary files /dev/null and b/admin/tool/lp/amd/build/user_competency_plan_popup.min.js differ
diff --git a/admin/tool/lp/amd/src/competency_plan_navigation.js b/admin/tool/lp/amd/src/competency_plan_navigation.js
new file mode 100644 (file)
index 0000000..34a70c7
--- /dev/null
@@ -0,0 +1,74 @@
+// 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/>.
+
+/**
+ * Event click on selecting competency in the competency autocomplete.
+ *
+ * @package    tool_lp
+ * @copyright  2016 Issam Taboubi <issam.taboubi@umontreal.ca>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(['jquery'], function($) {
+
+    /**
+     * CompetencyPlanNavigation
+     *
+     * @param {String} The selector of the competency element.
+     * @param {String} The base url for the page (no params).
+     * @param {Number} The user id
+     * @param {Number} The competency id
+     * @param {Number} The plan id
+     */
+    var CompetencyPlanNavigation = function(competencySelector, baseUrl, userId, competencyId, planId) {
+        this._baseUrl = baseUrl;
+        this._userId = userId + '';
+        this._competencyId = competencyId + '';
+        this._planId = planId;
+        this._ignoreFirstCompetency = true;
+
+        $(competencySelector).on('change', this._competencyChanged.bind(this));
+    };
+
+    /**
+     * The competency was changed in the select list.
+     *
+     * @method _competencyChanged
+     * @param {Event} e
+     */
+    CompetencyPlanNavigation.prototype._competencyChanged = function(e) {
+        if (this._ignoreFirstCompetency) {
+            this._ignoreFirstCompetency = false;
+            return;
+        }
+        var newCompetencyId = $(e.target).val();
+        var queryStr = '?userid=' + this._userId + '&planid=' + this._planId + '&competencyid=' + newCompetencyId;
+        document.location = this._baseUrl + queryStr;
+    };
+
+    /** @type {Number} The id of the competency. */
+    CompetencyPlanNavigation.prototype._competencyId = null;
+    /** @type {Number} The id of the user. */
+    CompetencyPlanNavigation.prototype._userId = null;
+    /** @type {Number} The id of the plan. */
+    CompetencyPlanNavigation.prototype._planId = null;
+    /** @type {String} Plugin base url. */
+    CompetencyPlanNavigation.prototype._baseUrl = null;
+    /** @type {Boolean} Ignore the first change event for competencies. */
+    CompetencyPlanNavigation.prototype._ignoreFirstCompetency = null;
+
+    return /** @alias module:tool_lp/competency_plan_navigation */ CompetencyPlanNavigation;
+
+});
index 2c1c4c3..21e084e 100644 (file)
@@ -628,17 +628,6 @@ define(['jquery',
         }
     };
 
-    /**
-     * Return if the level has a sub level.
-     *
-     * @param  {Number} level The level.
-     * @return {Boolean}
-     * @function hasSubLevel
-     */
-    var hasSubLevel = function(level) {
-        return typeof taxonomiesConstants[level + 1] !== 'undefined';
-    };
-
     /**
      * Return the taxonomy constant for a level.
      *
@@ -764,11 +753,7 @@ define(['jquery',
             var competency = treeModel.getCompetency(id);
 
             level = treeModel.getCompetencyLevel(id);
-            if (!hasSubLevel(level)) {
-                sublevel = false;
-            } else {
-                sublevel = level + 1;
-            }
+            sublevel = level + 1;
 
             actionMenu.show();
             $('[data-region="competencyactions"]').data('competency', competency);
@@ -781,15 +766,12 @@ define(['jquery',
             selectedTitle.text(str);
         });
 
-        if (!sublevel) {
-            btn.hide();
-        } else {
-            strAddTaxonomy(sublevel).then(function(str) {
-                btn.show()
-                    .find('[data-region="term"]')
-                    .text(str);
-            });
-        }
+        strAddTaxonomy(sublevel).then(function(str) {
+            btn.show()
+                .find('[data-region="term"]')
+                .text(str);
+        });
+
         // We handled this event so consume it.
         evt.preventDefault();
         return false;
index 1e0e3d7..2dabb3c 100644 (file)
@@ -30,20 +30,17 @@ define(['jquery', 'core/ajax', 'core/str', 'tool_lp/competencypicker', 'core/tem
      * @param {String} inputHiddenSelector The hidden input field selector.
      * @param {String} staticElementSelector The static element displaying the parent competency.
      * @param {Number} frameworkId The competency framework ID.
-     * @param {Number} frameworkMaxLevel The framework max level.
      * @param {Number} pageContextId The page context ID.
      */
     var ParentCompetencyForm = function(buttonSelector,
                                         inputHiddenSelector,
                                         staticElementSelector,
                                         frameworkId,
-                                        frameworkMaxLevel,
                                         pageContextId) {
         this.buttonSelector = buttonSelector;
         this.inputHiddenSelector = inputHiddenSelector;
         this.staticElementSelector = staticElementSelector;
         this.frameworkId = frameworkId;
-        this.frameworkMaxLevel = frameworkMaxLevel;
         this.pageContextId = pageContextId;
 
         // Register the events.
@@ -58,8 +55,6 @@ define(['jquery', 'core/ajax', 'core/str', 'tool_lp/competencypicker', 'core/tem
     ParentCompetencyForm.prototype.staticElementSelector = null;
     /** @var {Number} The competency framework ID. */
     ParentCompetencyForm.prototype.frameworkId = null;
-    /** @var {Number} The framework max level. */
-    ParentCompetencyForm.prototype.frameworkMaxLevel = null;
     /** @var {Number} The page context ID. */
     ParentCompetencyForm.prototype.pageContextId = null;
 
@@ -103,55 +98,7 @@ define(['jquery', 'core/ajax', 'core/str', 'tool_lp/competencypicker', 'core/tem
             e.preventDefault();
 
             var picker = new Picker(self.pageContextId, self.frameworkId, 'self', false);
-            var maxlevel = self.frameworkMaxLevel;
-            // Override the fetchcompetencies method to filter by max level.
-            picker._fetchCompetencies = function(frameworkId, searchText) {
-                var self = this;
-
-                return ajax.call([
-                    { methodname: 'core_competency_search_competencies', args: {
-                        searchtext: searchText,
-                        competencyframeworkid: frameworkId
-                    }}
-                ])[0].done(function(competencies) {
-
-                    var disabledcompetencies = [];
-                    function addCompetencyChildren(parent, competencies) {
-                        for (var i = 0; i < competencies.length; i++) {
-                            // Check if competency does not exceed the framework max level.
-                            var path = String(competencies[i].path),
-                            level = path.split('/').length - 2;
-                            if (level >= maxlevel && competencies[i].id !== "0") {
-                                disabledcompetencies.push(competencies[i].id);
-                            }
-
-                            if (competencies[i].parentid == parent.id) {
-                                parent.haschildren = true;
-                                competencies[i].children = [];
-                                competencies[i].haschildren = false;
-                                parent.children[parent.children.length] = competencies[i];
-                                addCompetencyChildren(competencies[i], competencies);
-                            }
-                        }
-                    }
 
-                    // Expand the list of competencies into a tree.
-                    var i, tree = [], comp;
-                    for (i = 0; i < competencies.length; i++) {
-                        comp = competencies[i];
-                        if (comp.parentid == "0") { // Loose check for now, because WS returns a string.
-                            comp.children = [];
-                            comp.haschildren = 0;
-                            tree[tree.length] = comp;
-                            addCompetencyChildren(comp, competencies);
-                        }
-                    }
-
-                    self._competencies = tree;
-                    self.setDisallowedCompetencyIDs(disabledcompetencies);
-
-                }.bind(self)).fail(Notification.exception);
-            };
             // Override the render method to make framework selectable.
             picker._render = function() {
                 var self = this;
@@ -185,7 +132,6 @@ define(['jquery', 'core/ajax', 'core/str', 'tool_lp/competencypicker', 'core/tem
          * @param {String} inputHiddenSelector The hidden input field selector.
          * @param {String} staticElementSelector The static element displaying the parent competency.
          * @param {Number} frameworkId The competency framework ID.
-         * @param {Number} frameworkMaxLevel The framework max level.
          * @param {Number} pageContextId The page context ID.
          * @method init
          */
@@ -193,14 +139,12 @@ define(['jquery', 'core/ajax', 'core/str', 'tool_lp/competencypicker', 'core/tem
                         inputSelector,
                         staticElementSelector,
                         frameworkId,
-                        frameworkMaxLevel,
                         pageContextId) {
             // Create instance.
             new ParentCompetencyForm(buttonSelector,
                                     inputSelector,
                                     staticElementSelector,
                                     frameworkId,
-                                    frameworkMaxLevel,
                                     pageContextId);
         }
     };
diff --git a/admin/tool/lp/amd/src/user_competency_plan_popup.js b/admin/tool/lp/amd/src/user_competency_plan_popup.js
new file mode 100644 (file)
index 0000000..87c977c
--- /dev/null
@@ -0,0 +1,130 @@
+// 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 open user competency plan in popup
+ *
+ * @package    report_competency
+ * @copyright  2016 Issam Taboubi <issam.taboubi@umontreal.ca>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(['jquery', 'core/notification', 'core/str', 'core/ajax', 'core/templates', 'tool_lp/dialogue'],
+       function($, notification, str, ajax, templates, Dialogue) {
+
+    /**
+     * UserCompetencyPopup
+     *
+     * @param {String} The regionSelector
+     * @param {String} The userCompetencySelector
+     * @param {Number} The plan ID
+     */
+    var UserCompetencyPopup = function(regionSelector, userCompetencySelector, planId) {
+        this._regionSelector = regionSelector;
+        this._userCompetencySelector = userCompetencySelector;
+        this._planId = planId;
+
+        $(this._regionSelector).on('click', this._userCompetencySelector, this._handleClick.bind(this));
+    };
+
+    /**
+     * Get the data from the closest TR and open the popup.
+     *
+     * @method _handleClick
+     * @param {Event} e
+     */
+    UserCompetencyPopup.prototype._handleClick = function(e) {
+        e.preventDefault();
+        var tr = $(e.target).closest('tr');
+        var competencyId = $(tr).data('competencyid');
+        var userId = $(tr).data('userid');
+        var planId = this._planId;
+
+        var requests = ajax.call([{
+            methodname : 'tool_lp_data_for_user_competency_summary_in_plan',
+            args: { competencyid: competencyId, planid: planId },
+            done: this._contextLoaded.bind(this),
+            fail: notification.exception
+        }]);
+
+        // Log the user competency viewed in plan event.
+        requests[0].then(function (result) {
+            var eventMethodName = 'core_competency_user_competency_viewed_in_plan';
+            // Trigger core_competency_user_competency_plan_viewed event instead if plan is already completed.
+            if (result.plan.iscompleted) {
+                eventMethodName = 'core_competency_user_competency_plan_viewed';
+            }
+            ajax.call([{
+                methodname: eventMethodName,
+                args: {competencyid: competencyId, userid: userId, planid: planId},
+                fail: notification.exception
+            }]);
+        });
+    };
+
+    /**
+     * We loaded the context, now render the template.
+     *
+     * @method _contextLoaded
+     * @param {Object} context
+     */
+    UserCompetencyPopup.prototype._contextLoaded = function(context) {
+        var self = this;
+        templates.render('tool_lp/user_competency_summary_in_plan', context).done(function(html, js) {
+            str.get_string('usercompetencysummary', 'report_competency').done(function(title) {
+                (new Dialogue(title, html, templates.runTemplateJS.bind(templates, js), self._refresh.bind(self), true));
+            }).fail(notification.exception);
+        }).fail(notification.exception);
+    };
+
+    /**
+     * Refresh the page.
+     *
+     * @method _refresh
+     */
+    UserCompetencyPopup.prototype._refresh = function() {
+        var planId = this._planId;
+
+        ajax.call([{
+            methodname : 'tool_lp_data_for_plan_page',
+            args: { planid: planId},
+            done: this._pageContextLoaded.bind(this),
+            fail: notification.exception
+        }]);
+    };
+
+    /**
+     * We loaded the context, now render the template.
+     *
+     * @method _pageContextLoaded
+     * @param {Object} context
+     */
+    UserCompetencyPopup.prototype._pageContextLoaded = function(context) {
+        var self = this;
+        templates.render('tool_lp/plan_page', context).done(function(html, js) {
+            templates.replaceNode(self._regionSelector, html, js);
+        }).fail(notification.exception);
+    };
+
+    /** @type {String} The selector for the region with the user competencies */
+    UserCompetencyPopup.prototype._regionSelector = null;
+    /** @type {String} The selector for the region with a single user competencies */
+    UserCompetencyPopup.prototype._userCompetencySelector = null;
+    /** @type {Number} The plan Id */
+    UserCompetencyPopup.prototype._planId = null;
+
+    return /** @alias module:tool_lp/user_competency_plan_popup */ UserCompetencyPopup;
+
+});
index 61a922f..59608c5 100644 (file)
@@ -404,6 +404,7 @@ class external extends external_api {
             'canmanagecompetencyframeworks' => new external_value(PARAM_BOOL, 'User can manage competency frameworks'),
             'canmanagecoursecompetencies' => new external_value(PARAM_BOOL, 'User can manage linked course competencies'),
             'canconfigurecoursecompetencies' => new external_value(PARAM_BOOL, 'User can configure course competency settings'),
+            'cangradecompetencies' => new external_value(PARAM_BOOL, 'User can grade competencies.'),
             'settings' => course_competency_settings_exporter::get_read_structure(),
             'statistics' => course_competency_statistics_exporter::get_read_structure(),
             'competencies' => new external_multiple_structure(new external_single_structure(array(
index cc43377..b43dde8 100644 (file)
@@ -72,7 +72,7 @@ class user_competency_summary_exporter extends \core_competency\external\exporte
                 'optional' => true
             ),
             'usercompetencyplan' => array(
-                'type' => user_competency_exporter::read_properties_definition(),
+                'type' => user_competency_plan_exporter::read_properties_definition(),
                 'optional' => true
             ),
             'usercompetencycourse' => array(
index e9b5050..915ea6f 100644 (file)
@@ -88,7 +88,6 @@ class competency extends persistent {
                 '#tool_lp_parentcompetency',
                 '#id_parentdesc',
                 $framework->get_id(),
-                \core_competency\competency_framework::get_taxonomies_max_level(),
                 $pagecontextid));
         }
 
index 974c2ba..6dde66c 100644 (file)
@@ -100,7 +100,8 @@ class competency_framework extends persistent {
         $mform->addElement('header', 'taxonomyhdr', get_string('taxonomies', 'tool_lp'));
         $taxonomies = \core_competency\competency_framework::get_taxonomies_list();
         $taxdefaults = array();
-        for ($i = 1; $i <= \core_competency\competency_framework::get_taxonomies_max_level(); $i++) {
+        $taxcount = max($framework ? $framework->get_depth() : 4, 4);
+        for ($i = 1; $i <= $taxcount; $i++) {
             $mform->addElement('select', "taxonomies[$i]", get_string('levela', 'tool_lp', $i), $taxonomies);
             $taxdefaults[$i] = \core_competency\competency_framework::TAXONOMY_COMPETENCY;
         }
index 8faac4d..28de24c 100644 (file)
@@ -58,6 +58,7 @@ class user_evidence extends persistent {
 
         $mform->addElement('url', 'url', get_string('userevidenceurl', 'tool_lp'), array(), array('usefilepicker' => false));
         $mform->setType('url', PARAM_RAW_TRIMMED);      // Can not use PARAM_URL, it silently converts bad URLs to ''.
+        $mform->addHelpButton('url', 'userevidenceurl', 'tool_lp');
 
         $mform->addElement('filemanager', 'files', get_string('userevidencefiles', 'tool_lp'), array(),
             $this->_customdata['fileareaoptions']);
diff --git a/admin/tool/lp/classes/output/competency_plan_navigation.php b/admin/tool/lp/classes/output/competency_plan_navigation.php
new file mode 100644 (file)
index 0000000..58ab38b
--- /dev/null
@@ -0,0 +1,102 @@
+<?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 competency plan page class.
+ *
+ * @package    tool_lp
+ * @copyright  2016 Issam Taboubi <issam.taboubi@umontreal.ca>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_lp\output;
+
+use renderable;
+use renderer_base;
+use templatable;
+use context_course;
+use \core_competency\external\competency_exporter;
+use stdClass;
+
+/**
+ * User competency plan navigation class.
+ *
+ * @package    tool_lp
+ * @copyright  2016 Issam Taboubi <issam.taboubi@umontreal.ca>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class competency_plan_navigation implements renderable, templatable {
+
+    /** @var userid */
+    protected $userid;
+
+    /** @var competencyid */
+    protected $competencyid;
+
+    /** @var planid */
+    protected $planid;
+
+    /** @var baseurl */
+    protected $baseurl;
+
+    /**
+     * Construct.
+     *
+     * @param int $userid
+     * @param int $competencyid
+     * @param int $planid
+     * @param string $baseurl
+     */
+    public function __construct($userid, $competencyid, $planid, $baseurl) {
+        $this->userid = $userid;
+        $this->competencyid = $competencyid;
+        $this->planid = $planid;
+        $this->baseurl = $baseurl;
+    }
+
+    /**
+     * Export the data.
+     *
+     * @param renderer_base $output
+     * @return stdClass
+     */
+    public function export_for_template(renderer_base $output) {
+
+        $data = new stdClass();
+        $data->userid = $this->userid;
+        $data->competencyid = $this->competencyid;
+        $data->planid = $this->planid;
+        $data->baseurl = $this->baseurl;
+
+        $plancompetencies = \core_competency\api::list_plan_competencies($data->planid);
+        $data->competencies = array();
+        $contextcache = array();
+        foreach ($plancompetencies as $plancompetency) {
+            $frameworkid = $plancompetency->competency->get_competencyframeworkid();
+            if (!isset($contextcache[$frameworkid])) {
+                $contextcache[$frameworkid] = $plancompetency->competency->get_context();
+            }
+            $context = $contextcache[$frameworkid];
+            $exporter = new competency_exporter($plancompetency->competency, array('context' => $context));
+            $competency = $exporter->export($output);
+            if ($competency->id == $this->competencyid) {
+                $competency->selected = true;
+            }
+            $data->competencies[] = $competency;
+        }
+        $data->hascompetencies = count($data->competencies);
+        return $data;
+    }
+}
index 45c5cbf..395561f 100644 (file)
@@ -80,6 +80,7 @@ class course_competencies_page implements renderable, templatable {
         $this->coursecompetencylist = api::list_course_competencies($courseid);
         $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);
         $this->coursecompetencysettings = api::read_course_competency_settings($courseid);
         $this->coursecompetencystatistics = new course_competency_statistics($courseid);
 
@@ -183,6 +184,7 @@ class course_competencies_page implements renderable, templatable {
         $data->canmanagecompetencyframeworks = $this->canmanagecompetencyframeworks;
         $data->canmanagecoursecompetencies = $this->canmanagecoursecompetencies;
         $data->canconfigurecoursecompetencies = $this->canconfigurecoursecompetencies;
+        $data->cangradecompetencies = $this->cangradecompetencies;
         $exporter = new course_competency_settings_exporter($this->coursecompetencysettings);
         $data->settings = $exporter->export($output);
         $related = array('context' => $this->context);
index f2c68c0..ca45e29 100644 (file)
@@ -206,6 +206,17 @@ class renderer extends plugin_renderer_base {
         return parent::render_from_template('tool_lp/user_competency_course_navigation', $data);
     }
 
+    /**
+     * Defer to template.
+     *
+     * @param competency_plan_navigation $nav
+     * @return string
+     */
+    public function render_competency_plan_navigation(competency_plan_navigation $nav) {
+        $data = $nav->export_for_template($this);
+        return parent::render_from_template('tool_lp/competency_plan_navigation', $data);
+    }
+
     /**
      * Defer to template.
      *
index ef40ba4..3d716b5 100644 (file)
@@ -85,7 +85,8 @@ class user_competency_course_navigation implements renderable, templatable {
         $data->baseurl = $this->baseurl;
         $data->groupselector = '';
 
-        if (has_capability('moodle/competency:coursecompetencymanage', $context)) {
+        if (has_any_capability(array('moodle/competency:usercompetencyview', 'moodle/competency:coursecompetencymanage'),
+                $context)) {
             $course = $DB->get_record('course', array('id' => $this->courseid));
             $currentgroup = groups_get_course_group($course, true);
             if ($currentgroup !== false) {
index 5b76b5f..8f484ad 100644 (file)
@@ -40,7 +40,6 @@ if (!\core_competency\competency_framework::can_read_context($context)) {
 
 $title = get_string('competencies', 'core_competency');
 $pagetitle = get_string('competencyframeworks', 'tool_lp');
-$pagesubtitle = get_string('listcompetencyframeworkscaption', 'tool_lp');
 
 // Set up the page.
 $PAGE->set_context($context);
@@ -51,7 +50,6 @@ $PAGE->set_heading($title);
 $output = $PAGE->get_renderer('tool_lp');
 echo $output->header();
 echo $output->heading($pagetitle, 2);
-echo $output->heading($pagesubtitle, 3);
 
 $page = new \tool_lp\output\manage_competency_frameworks_page($context);
 echo $output->render($page);
index 7886586..71f4af2 100644 (file)
@@ -280,6 +280,7 @@ $string['userevidencename'] = 'Name';
 $string['userevidencesummary'] = 'Summary';
 $string['userevidenceupdated'] = 'Evidence of prior learning updated';
 $string['userevidenceurl'] = 'URL';
+$string['userevidenceurl_help'] = 'The URL must start with \'http://\' or \'https://\'.';
 $string['viewdetails'] = 'View details';
 $string['visible'] = 'Visible';
 $string['visible_help'] = 'A competency framework can be hidden whilst it is being set up or updated to a new version.';
index c725a95..75dd6b9 100644 (file)
@@ -10,9 +10,8 @@
     vertical-align: top;
 }
 .path-admin-tool-lp .progress {
-    width: 10em;
+    width: 100%;
     display: inline-block;
-    margin-left: 2em;
     margin-right: 2em;
 }
 .dir-rtl.path-admin-tool-lp .progress .bar {
diff --git a/admin/tool/lp/templates/competency_plan_navigation.mustache b/admin/tool/lp/templates/competency_plan_navigation.mustache
new file mode 100644 (file)
index 0000000..53aa5d3
--- /dev/null
@@ -0,0 +1,22 @@
+<div class="pull-right well">
+{{#hascompetencies}}
+<span>
+<label for="competency-nav-{{uniqid}}" class="accesshide">{{#str}}jumptocompetency, tool_lp{{/str}}</label>
+<select id="competency-nav-{{uniqid}}">
+{{#competencies}}
+<option value="{{id}}" {{#selected}}selected="selected"{{/selected}}>{{shortname}} {{idnumber}}</option>
+{{/competencies}}
+</select>
+</span>
+{{/hascompetencies}}
+</form>
+</div>
+{{#js}}
+require(['core/form-autocomplete', 'tool_lp/competency_plan_navigation'], function(autocomplete, nav) {
+    (new nav('#competency-nav-{{uniqid}}', '{{baseurl}}', {{userid}}, {{competencyid}}, {{planid}}));
+{{#hascompetencies}}
+    autocomplete.enhance('#competency-nav-{{uniqid}}', false, false, '{{#str}}jumptocompetency, tool_lp{{/str}}');
+{{/hascompetencies}}
+
+});
+{{/js}}
index 3782d4d..0a65312 100644 (file)
 }}
 <div data-region="coursecompetenciespage">
     <div data-region="actions" class="clearfix">
-        <div class="pull-right">
+        <div class="pull-left">
             {{#canmanagecoursecompetencies}}
                 <button disabled>{{#str}}addcoursecompetencies, tool_lp{{/str}}</button>
             {{/canmanagecoursecompetencies}}
         </div>
     </div>
-<div data-region="configurecoursecompetencies">
-{{#settings.pushratingstouserplans}}
-    <p class="alert">
-    {{#str}}coursecompetencyratingsarepushedtouserplans, tool_lp{{/str}}
-{{/settings.pushratingstouserplans}}
-{{^settings.pushratingstouserplans}}
-    <p class="alert alert-info">
-    {{#str}}coursecompetencyratingsarenotpushedtouserplans, tool_lp{{/str}}
-{{/settings.pushratingstouserplans}}
-{{#canconfigurecoursecompetencies}}
-    <a href="#"
-        data-action="configure-course-competency-settings"
-        data-courseid="{{courseid}}"
-        data-pushratingstouserplans="{{settings.pushratingstouserplans}}"
-        >{{#pix}}t/edit, core, {{#str}}edit{{/str}}{{/pix}}</a>
-    </p>
-{{/canconfigurecoursecompetencies}}
-</div>
+    <div data-region="configurecoursecompetencies">
+    {{#cangradecompetencies}}
+        <p class="alert {{^settings.pushratingstouserplans}}alert-info{{/settings.pushratingstouserplans}}">
+        {{#settings.pushratingstouserplans}}
+            {{#str}}coursecompetencyratingsarepushedtouserplans, tool_lp{{/str}}
+        {{/settings.pushratingstouserplans}}
+        {{^settings.pushratingstouserplans}}
+            {{#str}}coursecompetencyratingsarenotpushedtouserplans, tool_lp{{/str}}
+        {{/settings.pushratingstouserplans}}
+        {{#canconfigurecoursecompetencies}}
+            <a href="#"
+               data-action="configure-course-competency-settings"
+               data-courseid="{{courseid}}"
+               data-pushratingstouserplans="{{settings.pushratingstouserplans}}">
+                {{#pix}}t/edit, core, {{#str}}edit{{/str}}{{/pix}}
+            </a>
+        {{/canconfigurecoursecompetencies}}
+        </p>
+    {{/cangradecompetencies}}
+    </div>
 {{#statistics}}
 {{> tool_lp/course_competency_statistics }}
 {{/statistics}}
index cfb9df3..9202052 100644 (file)
 {{#competencycount}}
 <div data-region="coursecompetencystatistics" class="well">
     {{#canbegradedincourse}}
-    <div>
-        <div class="progresstext">
-            {{#str}}xcompetenciesproficientoutofyincourse, tool_lp, { "x": "{{proficientcompetencycount}}", "y": "{{competencycount}}" } {{/str}}
+    <div class="clearfix">
+        <div class="span6">
+            <div class="progresstext">
+                {{#str}}xcompetenciesproficientoutofyincourse, tool_lp, { "x": "{{proficientcompetencycount}}", "y": "{{competencycount}}" } {{/str}}
+            </div>
         </div>
-        <div class="progress">
-            <div class="bar" style="width: {{proficientcompetencypercentage}}%;">
-                {{proficientcompetencypercentageformatted}} %
+        <div class="span6">
+            <span class="pull-right label label-info">{{proficientcompetencypercentageformatted}} %</span>
+            <div class="progress">
+                <div class="bar" style="width: {{proficientcompetencypercentage}}%;"></div>
             </div>
         </div>
     </div>
index facc7c3..ee99906 100644 (file)
     * navigation - array of strings containing buttons for navigation
 }}
 <div data-region="managecompetencies">
-<div class="btn-group pull-right">
+<div class="btn-group pull-left">
     {{#navigation}}
     {{{.}}}
     {{/navigation}}
 </div>
 <table class="generaltable fullwidth managecompetencies">
+    <caption>{{#str}}listcompetencyframeworkscaption, tool_lp{{/str}}</caption>
     <thead>
         <tr>
             <th scope="col">{{#str}}competencyframeworkname, tool_lp{{/str}}</th>
index eb678c0..82d2b76 100644 (file)
@@ -32,7 +32,7 @@
     * navigation - array of strings containing buttons for navigation
 }}
 <div data-region="managetemplates">
-<div class="btn-group pull-right">
+<div class="btn-group pull-left">
     {{#navigation}}
     {{{.}}}
     {{/navigation}}
index 1b66ee3..aec4a7d 100644 (file)
             <a href="{{pluginbaseurl}}/editplan.php?id={{plan.id}}&amp;userid={{plan.userid}}">{{#pix}}t/edit, core, {{#str}}editplan, tool_lp{{/str}}{{/pix}}</a>
         {{/plan.canbeedited}}
     </h2>
+    {{#plan.canbeedited}}
+    <div data-region="actions" class="clearfix">
+        <div class="pull-left">
+            <!-- Button to add competencies to the plan -->
+            <button class="btn" data-action="add">{{#pix}}t/add{{/pix}} {{#str}}addcompetency, tool_lp{{/str}}</button>
+        </div>
+    </div>
+    {{/plan.canbeedited}}
     <div data-region="plan-summary">
         <dl>
             <dt>{{#str}}status, tool_lp{{/str}}</dt>
                 <dd>{{{plan.description}}}</dd>
             {{/description}}
             <dt>{{#str}}progress, tool_lp{{/str}}</dt>
-            <dd><span class="progresstext">{{#str}}xcompetenciesproficientoutofy, tool_lp, { "x": "{{proficientcompetencycount}}", "y": "{{competencycount}}" }{{/str}}</span>
-                <div class="progress">
-                    <div class="bar" style="width: {{proficientcompetencypercentage}}%;">
-                        {{proficientcompetencypercentageformatted}} %
+            <dd>
+                <div class="span4">
+                    <div class="progresstext">
+                        {{#str}}xcompetenciesproficientoutofy, tool_lp, { "x": "{{proficientcompetencycount}}", "y": "{{competencycount}}" }{{/str}}
+                    </div>
+                </div>
+                <div class="span4">
+                    <span class="pull-right label label-info">{{proficientcompetencypercentageformatted}} %</span>
+                    <div class="progress">
+                        <div class="bar" style="width: {{proficientcompetencypercentage}}%;"></div>
                     </div>
                 </div>
             </dd>
         {{/canpostorhascomments}}
     {{/plan.commentarea}}
     <div data-region="plan-competencies">
-        <div data-region="actions">
-            <div class="pull-right">
-                <!-- Button to add competencies to the plan -->
-                {{#plan.canbeedited}}
-                <button class="btn" data-action="add">{{#pix}}t/add{{/pix}} {{#str}}addcompetency, tool_lp{{/str}}</button>
-                {{/plan.canbeedited}}
-            </div>
-        </div>
         <h3>{{#str}}learningplancompetencies, tool_lp{{/str}}</h3>
         <table class="generaltable fullwidth managecompetencies">
             <thead>
             </thead>
             <tbody class="drag-parentnode">
                 {{#competencies}}
-                <tr class="drag-samenode" data-node="user-competency" data-id="{{competency.id}}" data-competencyid="{{usercompetency.competencyid}}" data-userid="{{usercompetency.userid}}">
+                <tr class="drag-samenode" data-node="user-competency" data-id="{{competency.id}}"
+                        data-competencyid="{{competency.id}}"
+                        data-userid="{{plan.userid}}">
                     <td>
                         {{#plan.canbeedited}}
                         <span class="drag-handlecontainer pull-left"></span>
                         {{/plan.canbeedited}}
-                        <a href="{{pluginbaseurl}}/user_competency_in_plan.php?competencyid={{competency.id}}&amp;userid={{plan.userid}}&amp;planid={{plan.id}}">{{competency.shortname}}</a>
+                        <a data-usercompetency="true" href="#">{{competency.shortname}}</a>
                         <em>{{competency.idnumber}}</em>
                         {{#comppath}}
                             <br>
     </div>
 </div>
 {{#js}}
-require(['tool_lp/competencies', 'tool_lp/planactions', 'tool_lp/user_competency_workflow'], function(mod, actionsMod, UserCompWorkflow) {
+require(['tool_lp/competencies', 'tool_lp/planactions', 'tool_lp/user_competency_workflow', 'tool_lp/user_competency_plan_popup'], function(mod, actionsMod, UserCompWorkflow, Popup) {
     var planActions = new actionsMod('plan');
 
     (new mod({{plan.id}}, 'plan', {{contextid}}));
+    (new Popup('[data-region=plan-page]', '[data-usercompetency=true]', {{plan.id}}));
     planActions.registerEvents();
 
     var ucw = new UserCompWorkflow();
index 6aee2d6..78d1c1d 100644 (file)
@@ -34,7 +34,7 @@
 }}
 
 <div data-region="plans">
-<div class="btn-group pull-right">
+<div class="btn-group pull-left">
     {{#navigation}}
     {{{.}}}
     {{/navigation}}
index fad4227..6095c71 100644 (file)
             <a href="{{pluginbaseurl}}/edittemplate.php?id={{template.id}}&amp;pagecontextid={{pagecontextid}}">{{#pix}}t/edit, core, {{#str}}edittemplate, tool_lp{{/str}}{{/pix}}</a>
         {{/template.canmanage}}
     </h2>
-    <h3>{{#str}}templatecompetencies, tool_lp{{/str}}</h3>
-    {{#statistics}}
-        {{> tool_lp/template_statistics }}
-    {{/statistics}}
     {{#canmanagetemplatecompetencies}}
     <div data-region="actions" class="clearfix">
-        <div class="pull-right">
+        <div class="pull-left">
             <button disabled>{{#str}}addtemplatecompetencies, tool_lp{{/str}}</button>
         </div>
     </div>
     {{/canmanagetemplatecompetencies}}
+    <h3>{{#str}}templatecompetencies, tool_lp{{/str}}</h3>
+    {{#statistics}}
+        {{> tool_lp/template_statistics }}
+    {{/statistics}}
     <div data-region="templatecompetencies">
         <div class="managecompetencies">
             <div class="drag-parentnode">
index ab1da3a..66caf50 100644 (file)
 }}
 {{#competencycount}}
 <div data-region="templatestatistics" class="well">
-    <div>
-        <div class="progresstext">
-            {{#str}}xcompetencieslinkedoutofy, tool_lp, { "x": "{{linkedcompetencycount}}", "y": "{{competencycount}}" } {{/str}}
+    <div class="clearfix">
+        <div class="span4">
+            <div class="progresstext">
+                {{#str}}xcompetencieslinkedoutofy, tool_lp, { "x": "{{linkedcompetencycount}}", "y": "{{competencycount}}" } {{/str}}
+            </div>
         </div>
-        <div class="progress">
-            <div class="bar" style="width: {{linkedcompetencypercentage}}%;">
-                {{linkedcompetencypercentageformatted}} %
+        <div class="span6">
+            <span class="pull-right label label-info">{{linkedcompetencypercentageformatted}} %</span>
+            <div class="progress">
+                <div class="bar" style="width: {{linkedcompetencypercentage}}%;"></div>
             </div>
         </div>
     </div>
     {{#plancount}}
-    <div>
-        <div class="progresstext">
-            {{#str}}xplanscompletedoutofy, tool_lp, { "x": "{{completedplancount}}", "y": "{{plancount}}" } {{/str}}
+    <div class="clearfix">
+        <div class="span4">
+            <div class="progresstext">
+                {{#str}}xplanscompletedoutofy, tool_lp, { "x": "{{completedplancount}}", "y": "{{plancount}}" } {{/str}}
+            </div>
         </div>
-        <div class="progress">
-            <div class="bar" style="width: {{completedplanpercentage}}%;">
-                {{completedplanpercentageformatted}} %
+        <div class="span6">
+            <span class="pull-right label label-info">{{completedplanpercentageformatted}} %</span>
+            <div class="progress">
+                <div class="bar" style="width: {{completedplanpercentage}}%;">
+
+                </div>
             </div>
         </div>
     </div>
     {{/plancount}}
     {{#usercompetencyplancount}}
-    <div>
-        <div class="progresstext">
-            {{#str}}averageproficiencyrate, tool_lp, {{proficientusercompetencyplanpercentageformatted}} {{/str}}
+    <div  class="clearfix">
+        <div class="span4">
+            <div class="progresstext">
+                {{#str}}averageproficiencyrate, tool_lp, {{proficientusercompetencyplanpercentageformatted}} {{/str}}
+            </div>
         </div>
-        <div class="progress">
-            <div class="bar" style="width: {{proficientusercompetencyplanpercentage}}%;">
-                {{proficientusercompetencyplanpercentageformatted}} %
+        <div class="span6">
+            <span class="pull-right label label-info">{{proficientusercompetencyplanpercentageformatted}} %</span>
+            <div class="progress">
+                <div class="bar" style="width: {{proficientusercompetencyplanpercentage}}%;"></div>
             </div>
         </div>
     </div>
index 3b2b28f..4b11153 100644 (file)
@@ -33,7 +33,7 @@
 }}
 
 <div data-region="user-evidence-list">
-<div class="btn-group pull-right">
+<div class="btn-group pull-left">
     {{#navigation}}
         {{{.}}}
     {{/navigation}}
index 99ca923..40876be 100644 (file)
             <a href="{{pluginbaseurl}}/user_evidence_edit.php?id={{id}}&amp;userid={{userid}}">{{#pix}}t/edit, core, {{#str}}editthisuserevidence, tool_lp{{/str}}{{/pix}}</a>
         {{/canmanage}}
     </h2>
+    {{#canmanage}}
+    <div data-region="actions" class="clearfix">
+        <div class="pull-left">
+            {{#userhasplan}}
+                <button class="btn" data-action="link-competency">{{#pix}}t/add{{/pix}} {{#str}}linkcompetencies, tool_lp{{/str}}</button>
+            {{/userhasplan}}
+        </div>
+    </div>
+    {{/canmanage}}
 
     <div data-region="user-evidence-summary">
         {{#description}}
                 {{/usercompetencies}}
             </tbody>
         </table>
-
-        <div data-region="actions">
-            <div class="pull-right">
-                {{#canmanage}}
-                    {{#userhasplan}}
-                    <button class="btn" data-action="link-competency">{{#pix}}t/add{{/pix}} {{#str}}linkcompetencies, tool_lp{{/str}}</button>
-                    {{/userhasplan}}
-                {{/canmanage}}
-            </div>
-        </div>
     </div>
 </div>
 
index 6b0bf40..0a5fea5 100644 (file)
@@ -25,8 +25,6 @@
 
 require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
 
-use Behat\Behat\Context\Step\Given as Given;
-
 /**
  * Step definition for learning plan system.
  *
@@ -44,17 +42,12 @@ class behat_tool_lp extends behat_base {
      *
      * @param string $nodetext
      * @param string $rowname
-     * @return Given[] array of steps
      */
     public function click_on_edit_menu_of_the_row($nodetext, $rowname) {
-        $steps = array();
-
-        $xpathtarget = "//ul//li//ul//li[@class='tool-lp-menu-item']//a[contains(.,'". $nodetext ."')]";
+        $xpathtarget = "//ul//li//ul//li[@class='tool-lp-menu-item']//a[contains(.,'" . $nodetext . "')]";
 
-        $steps[] = new Given('I click on "Edit" "link" in the "' . $this->escape($rowname) . '" "table_row"');
-        $steps[] = new Given('I click on "'.$xpathtarget.'" "xpath_element" in the "' . $this->escape($rowname) . '" "table_row"');
-
-        return $steps;
+        $this->execute('behat_general::i_click_on_in_the', [get_string('edit'), 'link', $this->escape($rowname), 'table_row']);
+        $this->execute('behat_general::i_click_on_in_the', [$xpathtarget, 'xpath_element', $this->escape($rowname), 'table_row']);
     }
 
     /**
@@ -63,16 +56,11 @@ class behat_tool_lp extends behat_base {
      * @Given /^I select "([^"]*)" of the competency tree$/
      *
      * @param string $competencyname
-     * @return Given[] array of steps
      */
     public function select_of_the_competency_tree($competencyname) {
-        $steps = array();
-
-        $xpathtarget = "//li[@role='tree-item']//span[contains(.,'". $competencyname ."')]";
+        $xpathtarget = "//li[@role='tree-item']//span[contains(.,'" . $competencyname . "')]";
 
-        $steps[] = new Given('I click on "' . $xpathtarget . '" "xpath_element"');
-
-        return $steps;
+        $this->execute('behat_general::i_click_on', [$xpathtarget, 'xpath_element']);
     }
 
     /**
@@ -81,15 +69,10 @@ class behat_tool_lp extends behat_base {
      * @Given /^I click on "([^"]*)" item in the autocomplete list$/
      *
      * @param string $item
-     * @return Given[] array of steps
      */
     public function i_click_on_item_in_the_autocomplete_list($item) {
-        $steps = array();
-
-        $xpathtarget = "//ul[@class='form-autocomplete-suggestions']//li//span//span[contains(.,'". $item ."')]";
-
-        $steps[] = new Given('I click on "' . $xpathtarget . '" "xpath_element"');
+        $xpathtarget = "//ul[@class='form-autocomplete-suggestions']//li//span//span[contains(.,'" . $item . "')]";
 
-        return $steps;
+        $this->execute('behat_general::i_click_on', [$xpathtarget, 'xpath_element']);
     }
 }
index 1a1cd28..3a48a47 100644 (file)
@@ -46,7 +46,11 @@ list($title, $subtitle) = \tool_lp\page_helper::setup_for_plan($userid, $url, $p
 $output = $PAGE->get_renderer('tool_lp');
 echo $output->header();
 echo $output->heading($title);
+// User competency plan navigation.
+$baseurl = new moodle_url('/admin/tool/lp/user_competency_in_plan.php');
+$nav = new \tool_lp\output\competency_plan_navigation($userid, $competencyid, $planid, $baseurl);
 
+echo $output->render($nav);
 $page = new \tool_lp\output\user_competency_summary_in_plan($competencyid, $planid);
 echo $output->render($page);
 // Trigger the viewed event.
index f81d224..d9658aa 100644 (file)
@@ -31,6 +31,5 @@ $capabilities = array(
         'archetypes' => array(
             'manager' => CAP_ALLOW
         ),
-        'clonepermissionsfrom' => 'moodle/site:config'
     ),
 );
index cc958c2..b01b78b 100644 (file)
@@ -28,768 +28,6 @@ class webservice_test_client_form extends moodleform {
 
 // === Test client forms ===
 
-class moodle_user_create_users_form extends moodleform {
-    public function definition() {
-        global $CFG;
-
-        $mform = $this->_form;
-
-
-        $mform->addElement('header', 'wstestclienthdr', get_string('testclient', 'webservice'));
-
-        //note: these values are intentionally PARAM_RAW - we want users to test any rubbish as parameters
-        $data = $this->_customdata;
-        if ($data['authmethod'] == 'simple') {
-            $mform->addElement('text', 'wsusername', 'wsusername');
-            $mform->setType('wsusername', core_user::get_property_type('username'));
-            $mform->addElement('text', 'wspassword', 'wspassword');
-            $mform->setType('wspassword', core_user::get_property_type('password'));
-        } else if ($data['authmethod'] == 'token') {
-            $mform->addElement('text', 'token', 'token');
-            $mform->setType('token', PARAM_RAW_TRIMMED);
-        }
-
-        $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', core_user::get_property_type('auth'));
-
-        /// specific to the create users function
-        $mform->addElement('text', 'username', 'username');
-        $mform->setType('username', core_user::get_property_type('username'));
-        $mform->addElement('text', 'password', 'password');
-        $mform->setType('password', core_user::get_property_type('password'));
-        $mform->addElement('text', 'firstname', 'firstname');
-        $mform->setType('firstname', core_user::get_property_type('firstname'));
-        $mform->addElement('text', 'lastname', 'lastname');
-        $mform->setType('lastname', core_user::get_property_type('lastname'));
-        $mform->addElement('text', 'email', 'email');
-        $mform->setType('email', core_user::get_property_type('email'));
-
-        $mform->addElement('text', 'customfieldtype', 'customfieldtype');
-        $mform->setType('customfieldtype', PARAM_RAW);
-        $mform->addElement('text', 'customfieldvalue', 'customfieldvalue');
-        $mform->setType('customfieldvalue', PARAM_RAW);
-
-        $mform->addElement('hidden', 'function');
-        $mform->setType('function', PARAM_PLUGIN);
-
-        $mform->addElement('hidden', 'protocol');
-        $mform->setType('protocol', PARAM_ALPHA);
-
-
-
-        $mform->addElement('static', 'warning', '', get_string('executewarnign', 'webservice'));
-
-        $this->add_action_buttons(true, get_string('execute', 'webservice'));
-    }
-
-    public function get_params() {
-        if (!$data = $this->get_data()) {
-            return null;
-        }
-
-        //set customfields
-        if (!empty($data->customfieldtype)) {
-            $data->customfields = array(array('type' => $data->customfieldtype, 'value' => $data->customfieldvalue));
-        }
-
-        // remove unused from form data
-        unset($data->submitbutton);
-        unset($data->protocol);
-        unset($data->function);
-        unset($data->wsusername);
-        unset($data->wspassword);
-        unset($data->token);
-        unset($data->authmethod);
-        unset($data->customfieldtype);
-        unset($data->customfieldvalue);
-
-        $params = array();
-        $params['users'] = array();
-        $params['users'][] = (array)$data;
-
-        return $params;
-    }
-}
-
-
-class moodle_user_update_users_form extends moodleform {
-    public function definition() {
-        global $CFG;
-
-        $mform = $this->_form;
-
-
-        $mform->addElement('header', 'wstestclienthdr', get_string('testclient', 'webservice'));
-
-        //note: these values are intentionally PARAM_RAW - we want users to test any rubbish as parameters
-        $data = $this->_customdata;
-        if ($data['authmethod'] == 'simple') {
-            $mform->addElement('text', 'wsusername', 'wsusername');
-            $mform->setType('wsusername', core_user::get_property_type('username'));
-            $mform->addElement('text', 'wspassword', 'wspassword');
-            $mform->setType('wspassword', core_user::get_property_type('password'));
-        } else if ($data['authmethod'] == 'token') {
-            $mform->addElement('text', 'token', 'token');
-            $mform->setType('token', PARAM_RAW_TRIMMED);
-        }
-
-        $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', core_user::get_property_type('auth'));
-
-        /// specific to the create users function
-        $mform->addElement('text', 'id', 'id');
-        $mform->addRule('id', get_string('required'), 'required', null, 'client');
-        $mform->setType('id', core_user::get_property_type('id'));
-        $mform->addElement('text', 'username', 'username');
-        $mform->setType('username', core_user::get_property_type('username'));
-        $mform->addElement('text', 'password', 'password');
-        $mform->setType('password', core_user::get_property_type('password'));
-        $mform->addElement('text', 'firstname', 'firstname');
-        $mform->setType('firstname', core_user::get_property_type('firstname'));
-        $mform->addElement('text', 'lastname', 'lastname');
-        $mform->setType('lastname', core_user::get_property_type('lastname'));
-        $mform->addElement('text', 'email', 'email');
-        $mform->setType('email', core_user::get_property_type('email'));
-
-
-        $mform->addElement('text', 'customfieldtype', 'customfieldtype');
-        $mform->setType('customfieldtype', PARAM_RAW);
-        $mform->addElement('text', 'customfieldvalue', 'customfieldvalue');
-        $mform->setType('customfieldvalue', PARAM_RAW);
-
-        $mform->addElement('hidden', 'function');
-        $mform->setType('function', PARAM_PLUGIN);
-
-        $mform->addElement('hidden', 'protocol');
-        $mform->setType('protocol', PARAM_ALPHA);
-
-
-
-        $mform->addElement('static', 'warning', '', get_string('executewarnign', 'webservice'));
-
-        $this->add_action_buttons(true, get_string('execute', 'webservice'));
-    }
-
-    public function get_params() {
-        if (!$data = $this->get_data()) {
-            return null;
-        }
-
-        //set customfields
-        if (!empty($data->customfieldtype)) {
-            $data->customfields = array(array('type' => $data->customfieldtype, 'value' => $data->customfieldvalue));
-        }
-
-        // remove unused from form data
-        unset($data->submitbutton);
-        unset($data->protocol);
-        unset($data->function);
-        unset($data->wsusername);
-        unset($data->wspassword);
-        unset($data->token);
-        unset($data->authmethod);
-        unset($data->customfieldtype);
-        unset($data->customfieldvalue);
-
-        foreach($data as $key => $value) {
-            if (empty($value)) {
-                 unset($data->{$key});
-            }
-        }
-
-        $params = array();
-        $params['users'] = array();
-        $params['users'][] = (array)$data;
-
-        return $params;
-    }
-}
-
-
-class moodle_user_delete_users_form extends moodleform {
-    public function definition() {
-        global $CFG;
-
-        $mform = $this->_form;
-
-
-        $mform->addElement('header', 'wstestclienthdr', get_string('testclient', 'webservice'));
-
-        //note: these values are intentionally PARAM_RAW - we want users to test any rubbish as parameters
-        $data = $this->_customdata;
-        if ($data['authmethod'] == 'simple') {
-            $mform->addElement('text', 'wsusername', 'wsusername');
-            $mform->setType('wsusername', core_user::get_property_type('username'));
-            $mform->addElement('text', 'wspassword', 'wspassword');
-            $mform->setType('wspassword', core_user::get_property_type('password'));
-        } else if ($data['authmethod'] == 'token') {
-            $mform->addElement('text', 'token', 'token');
-            $mform->setType('token', PARAM_RAW_TRIMMED);
-        }
-
-        $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', core_user::get_property_type('auth'));
-
-        /// beginning of specific code to the create users function
-        $mform->addElement('text', 'userids[0]', 'userids[0]');
-        $mform->addElement('text', 'userids[1]', 'userids[1]');
-        $mform->addElement('text', 'userids[2]', 'userids[2]');
-        $mform->addElement('text', 'userids[3]', 'userids[3]');
-        $mform->setType('userids', core_user::get_property_type('id'));
-        /// end of specific code to the create users function
-
-        $mform->addElement('hidden', 'function');
-        $mform->setType('function', PARAM_PLUGIN);
-
-        $mform->addElement('hidden', 'protocol');
-        $mform->setType('protocol', PARAM_ALPHA);
-
-        $mform->addElement('static', 'warning', '', get_string('executewarnign', 'webservice'));
-
-        $this->add_action_buttons(true, get_string('execute', 'webservice'));
-    }
-
-    public function get_params() {
-        if (!$data = $this->get_data()) {
-            return null;
-        }
-        // remove unused from form data
-        unset($data->submitbutton);
-        unset($data->protocol);
-        unset($data->function);
-        unset($data->wsusername);
-        unset($data->wspassword);
-        unset($data->token);
-        unset($data->authmethod);
-
-        ///  beginning of specific code to the create users form
-        $params = array();
-        $params['userids'] = array();
-        for ($i=0; $i<10; $i++) {
-            if (empty($data->userids[$i])) {
-                continue;
-            }
-            $params['userids'][] = $data->userids[$i];
-        }
-        /// end of specific code to the create users function
-
-        return $params;
-    }
-}
-
-
-class moodle_user_get_users_by_id_form extends moodleform {
-    public function definition() {
-        global $CFG;
-
-        $mform = $this->_form;
-
-
-        $mform->addElement('header', 'wstestclienthdr', get_string('testclient', 'webservice'));
-
-        //note: these values are intentionally PARAM_RAW - we want users to test any rubbish as parameters
-        $data = $this->_customdata;
-        if ($data['authmethod'] == 'simple') {
-            $mform->addElement('text', 'wsusername', 'wsusername');
-            $mform->setType('wsusername', core_user::get_property_type('username'));
-            $mform->addElement('text', 'wspassword', 'wspassword');
-            $mform->setType('wspassword', core_user::get_property_type('password'));
-        } else if ($data['authmethod'] == 'token') {
-            $mform->addElement('text', 'token', 'token');
-            $mform->setType('token', PARAM_RAW_TRIMMED);
-        }
-
-        $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', core_user::get_property_type('auth'));
-
-        /// beginning of specific code to the create users function
-        $mform->addElement('text', 'userids[0]', 'userids[0]');
-        $mform->addElement('text', 'userids[1]', 'userids[1]');
-        $mform->addElement('text', 'userids[2]', 'userids[2]');
-        $mform->addElement('text', 'userids[3]', 'userids[3]');
-        $mform->setType('userids', core_user::get_property_type('id'));
-        /// end of specific code to the create users function
-
-        $mform->addElement('hidden', 'function');
-        $mform->setType('function', PARAM_PLUGIN);
-
-        $mform->addElement('hidden', 'protocol');
-        $mform->setType('protocol', PARAM_ALPHA);
-
-
-
-        $mform->addElement('static', 'warning', '', get_string('executewarnign', 'webservice'));
-
-        $this->add_action_buttons(true, get_string('execute', 'webservice'));
-    }
-
-    public function get_params() {
-        if (!$data = $this->get_data()) {
-            return null;
-        }
-        // remove unused from form data
-        unset($data->submitbutton);
-        unset($data->protocol);
-        unset($data->function);
-        unset($data->wsusername);
-        unset($data->wspassword);
-        unset($data->token);
-        unset($data->authmethod);
-
-        ///  beginning of specific code to the create users form
-        $params = array();
-        $params['userids'] = array();
-        for ($i=0; $i<10; $i++) {
-            if (empty($data->userids[$i])) {
-                continue;
-            }
-            $params['userids'][] = $data->userids[$i];
-        }
-        /// end of specific code to the create users function
-
-        return $params;
-    }
-}
-
-class moodle_group_create_groups_form extends moodleform {
-    public function definition() {
-        global $CFG;
-
-        $mform = $this->_form;
-
-
-        $mform->addElement('header', 'wstestclienthdr', get_string('testclient', 'webservice'));
-
-        //note: these values are intentionally PARAM_RAW - we want users to test any rubbish as parameters
-        $data = $this->_customdata;
-        if ($data['authmethod'] == 'simple') {
-            $mform->addElement('text', 'wsusername', 'wsusername');
-            $mform->setType('wsusername', core_user::get_property_type('username'));
-            $mform->addElement('text', 'wspassword', 'wspassword');
-            $mform->setType('wspassword', core_user::get_property_type('password'));
-        } else if ($data['authmethod'] == 'token') {
-            $mform->addElement('text', 'token', 'token');
-            $mform->setType('token', PARAM_RAW_TRIMMED);
-        }
-
-        $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', core_user::get_property_type('auth'));
-
-        $mform->addElement('text', 'courseid', 'courseid');
-        $mform->setType('courseid', PARAM_INT);
-        $mform->addElement('text', 'name', 'name');
-        $mform->setType('name', PARAM_TEXT);
-        $mform->addElement('text', 'description', 'description');
-        $mform->setType('description', PARAM_TEXT);
-        $mform->addElement('text', 'enrolmentkey', 'enrolmentkey');
-        $mform->setType('enrolmentkey', PARAM_RAW);
-
-        $mform->addElement('hidden', 'function');
-        $mform->setType('function', PARAM_PLUGIN);
-
-        $mform->addElement('hidden', 'protocol');
-        $mform->setType('protocol', PARAM_ALPHA);
-
-
-
-        $mform->addElement('static', 'warning', '', get_string('executewarnign', 'webservice'));
-
-        $this->add_action_buttons(true, get_string('execute', 'webservice'));
-    }
-
-    public function get_params() {
-        if (!$data = $this->get_data()) {
-            return null;
-        }
-        // remove unused from form data
-        unset($data->submitbutton);
-        unset($data->protocol);
-        unset($data->function);
-        unset($data->wsusername);
-        unset($data->wspassword);
-        unset($data->token);
-        unset($data->authmethod);
-
-        $params = array();
-        $params['groups'] = array();
-        $params['groups'][] = (array)$data;
-
-        return $params;
-    }
-}
-
-class moodle_group_get_groups_form extends moodleform {
-    public function definition() {
-        global $CFG;
-
-        $mform = $this->_form;
-
-        $mform->addElement('header', 'wstestclienthdr', get_string('testclient', 'webservice'));
-
-        //note: these values are intentionally PARAM_RAW - we want users to test any rubbish as parameters
-        $data = $this->_customdata;
-        if ($data['authmethod'] == 'simple') {
-            $mform->addElement('text', 'wsusername', 'wsusername');
-            $mform->setType('wsusername', core_user::get_property_type('username'));
-            $mform->addElement('text', 'wspassword', 'wspassword');
-            $mform->setType('wspassword', core_user::get_property_type('password'));
-        } else if ($data['authmethod'] == 'token') {
-            $mform->addElement('text', 'token', 'token');
-            $mform->setType('token', PARAM_RAW_TRIMMED);
-        }
-
-        $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', core_user::get_property_type('auth'));
-        $mform->addElement('text', 'groupids[0]', 'groupids[0]');
-        $mform->addElement('text', 'groupids[1]', 'groupids[1]');
-        $mform->addElement('text', 'groupids[2]', 'groupids[2]');
-        $mform->addElement('text', 'groupids[3]', 'groupids[3]');
-        $mform->setType('groupids', PARAM_INT);
-
-        $mform->addElement('hidden', 'function');
-        $mform->setType('function', PARAM_PLUGIN);
-
-        $mform->addElement('hidden', 'protocol');
-        $mform->setType('protocol', PARAM_ALPHA);
-
-        $this->add_action_buttons(true, get_string('execute', 'webservice'));
-    }
-
-    public function get_params() {
-        if (!$data = $this->get_data()) {
-            return null;
-        }
-        // remove unused from form data
-        unset($data->submitbutton);
-        unset($data->protocol);
-        unset($data->function);
-        unset($data->wsusername);
-        unset($data->wspassword);
-        unset($data->token);
-        unset($data->authmethod);
-
-        $params = array();
-        $params['groupids'] = array();
-        for ($i=0; $i<10; $i++) {
-            if (empty($data->groupids[$i])) {
-                continue;
-            }
-            $params['groupids'][] = $data->groupids[$i];
-        }
-
-        return $params;
-    }
-}
-
-class moodle_group_get_course_groups_form extends moodleform {
-    public function definition() {
-        global $CFG;
-
-        $mform = $this->_form;
-
-        $mform->addElement('header', 'wstestclienthdr', get_string('testclient', 'webservice'));
-
-        //note: these values are intentionally PARAM_RAW - we want users to test any rubbish as parameters
-        $data = $this->_customdata;
-        if ($data['authmethod'] == 'simple') {
-            $mform->addElement('text', 'wsusername', 'wsusername');
-            $mform->setType('wsusername', core_user::get_property_type('username'));
-            $mform->addElement('text', 'wspassword', 'wspassword');
-            $mform->setType('wspassword', core_user::get_property_type('password'));
-        } else if ($data['authmethod'] == 'token') {
-            $mform->addElement('text', 'token', 'token');
-            $mform->setType('token', PARAM_RAW_TRIMMED);
-        }
-
-        $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', core_user::get_property_type('auth'));
-        $mform->addElement('text', 'courseid', 'courseid');
-
-        $mform->addElement('hidden', 'function');
-        $mform->setType('function', PARAM_PLUGIN);
-
-        $mform->addElement('hidden', 'protocol');
-        $mform->setType('protocol', PARAM_ALPHA);
-
-        $this->add_action_buttons(true, get_string('execute', 'webservice'));
-    }
-
-    public function get_params() {
-        if (!$data = $this->get_data()) {
-            return null;
-        }
-        // remove unused from form data
-        unset($data->submitbutton);
-        unset($data->protocol);
-        unset($data->function);
-        unset($data->wsusername);
-        unset($data->wspassword);
-        unset($data->token);
-        unset($data->authmethod);
-
-        $params = array();
-        $params['courseid'] = $data->courseid;
-
-        return $params;
-    }
-}
-
-class moodle_group_delete_groups_form extends moodleform {
-    public function definition() {
-        global $CFG;
-
-        $mform = $this->_form;
-
-        $mform->addElement('header', 'wstestclienthdr', get_string('testclient', 'webservice'));
-
-        //note: these values are intentionally PARAM_RAW - we want users to test any rubbish as parameters
-        $data = $this->_customdata;
-        if ($data['authmethod'] == 'simple') {
-            $mform->addElement('text', 'wsusername', 'wsusername');
-            $mform->setType('wsusername', core_user::get_property_type('username'));
-            $mform->addElement('text', 'wspassword', 'wspassword');
-            $mform->setType('wspassword', core_user::get_property_type('password'));
-        } else if ($data['authmethod'] == 'token') {
-            $mform->addElement('text', 'token', 'token');
-            $mform->setType('token', PARAM_RAW_TRIMMED);
-        }
-
-        $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', core_user::get_property_type('auth'));
-        $mform->addElement('text', 'groupids[0]', 'groupids[0]');
-        $mform->addElement('text', 'groupids[1]', 'groupids[1]');
-        $mform->addElement('text', 'groupids[2]', 'groupids[2]');
-        $mform->addElement('text', 'groupids[3]', 'groupids[3]');
-        $mform->setType('groupids', PARAM_INT);
-
-        $mform->addElement('hidden', 'function');
-        $mform->setType('function', PARAM_PLUGIN);
-
-        $mform->addElement('hidden', 'protocol');
-        $mform->setType('protocol', PARAM_ALPHA);
-
-        $mform->addElement('static', 'warning', '', get_string('executewarnign', 'webservice'));
-
-        $this->add_action_buttons(true, get_string('execute', 'webservice'));
-    }
-
-    public function get_params() {
-        if (!$data = $this->get_data()) {
-            return null;
-        }
-        // remove unused from form data
-        unset($data->submitbutton);
-        unset($data->protocol);
-        unset($data->function);
-        unset($data->wsusername);
-        unset($data->wspassword);
-        unset($data->token);
-        unset($data->authmethod);
-
-        $params = array();
-        $params['groupids'] = array();
-        for ($i=0; $i<10; $i++) {
-            if (empty($data->groupids[$i])) {
-                continue;
-            }
-            $params['groupids'][] = $data->groupids[$i];
-        }
-
-        return $params;
-    }
-}
-
-class moodle_group_get_groupmembers_form extends moodleform {
-    public function definition() {
-        global $CFG;
-
-        $mform = $this->_form;
-
-        $mform->addElement('header', 'wstestclienthdr', get_string('testclient', 'webservice'));
-
-        //note: these values are intentionally PARAM_RAW - we want users to test any rubbish as parameters
-        $data = $this->_customdata;
-        if ($data['authmethod'] == 'simple') {
-            $mform->addElement('text', 'wsusername', 'wsusername');
-            $mform->setType('wsusername', core_user::get_property_type('username'));
-            $mform->addElement('text', 'wspassword', 'wspassword');
-            $mform->setType('wspassword', core_user::get_property_type('password'));
-        } else if ($data['authmethod'] == 'token') {
-            $mform->addElement('text', 'token', 'token');
-            $mform->setType('token', PARAM_RAW_TRIMMED);
-        }
-
-        $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', core_user::get_property_type('auth'));
-        $mform->addElement('text', 'groupids[0]', 'groupids[0]');
-        $mform->addElement('text', 'groupids[1]', 'groupids[1]');
-        $mform->addElement('text', 'groupids[2]', 'groupids[2]');
-        $mform->addElement('text', 'groupids[3]', 'groupids[3]');
-        $mform->setType('groupids', PARAM_INT);
-
-        $mform->addElement('hidden', 'function');
-        $mform->setType('function', PARAM_PLUGIN);
-
-        $mform->addElement('hidden', 'protocol');
-        $mform->setType('protocol', PARAM_ALPHA);
-
-        $this->add_action_buttons(true, get_string('execute', 'webservice'));
-    }
-
-    public function get_params() {
-        if (!$data = $this->get_data()) {
-            return null;
-        }
-        // remove unused from form data
-        unset($data->submitbutton);
-        unset($data->protocol);
-        unset($data->function);
-        unset($data->wsusername);
-        unset($data->wspassword);
-        unset($data->token);
-        unset($data->authmethod);
-
-        $params = array();
-        $params['groupids'] = array();
-        for ($i=0; $i<10; $i++) {
-            if (empty($data->groupids[$i])) {
-                continue;
-            }
-            $params['groupids'][] = $data->groupids[$i];
-        }
-
-        return $params;
-    }
-}
-
-class moodle_group_add_groupmembers_form extends moodleform {
-    public function definition() {
-        global $CFG;
-
-        $mform = $this->_form;
-
-        $mform->addElement('header', 'wstestclienthdr', get_string('testclient', 'webservice'));
-
-        //note: these values are intentionally PARAM_RAW - we want users to test any rubbish as parameters
-        $data = $this->_customdata;
-        if ($data['authmethod'] == 'simple') {
-            $mform->addElement('text', 'wsusername', 'wsusername');
-            $mform->setType('wsusername', core_user::get_property_type('username'));
-            $mform->addElement('text', 'wspassword', 'wspassword');
-            $mform->setType('wspassword', core_user::get_property_type('password'));
-        } else if ($data['authmethod'] == 'token') {
-            $mform->addElement('text', 'token', 'token');
-            $mform->setType('token', PARAM_RAW_TRIMMED);
-        }
-
-        $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', core_user::get_property_type('auth'));
-        $mform->addElement('text', 'userid[0]', 'userid[0]');
-        $mform->addElement('text', 'groupid[0]', 'groupid[0]');
-        $mform->addElement('text', 'userid[1]', 'userid[1]');
-        $mform->addElement('text', 'groupid[1]', 'groupid[1]');
-        $mform->setType('userid', core_user::get_property_type('id'));
-        $mform->setType('groupids', PARAM_INT);
-
-        $mform->addElement('hidden', 'function');
-        $mform->setType('function', PARAM_PLUGIN);
-
-        $mform->addElement('hidden', 'protocol');
-        $mform->setType('protocol', PARAM_ALPHA);
-
-        $this->add_action_buttons(true, get_string('execute', 'webservice'));
-    }
-
-    public function get_params() {
-        if (!$data = $this->get_data()) {
-            return null;
-        }
-        // remove unused from form data
-        unset($data->submitbutton);
-        unset($data->protocol);
-        unset($data->function);
-        unset($data->wsusername);
-        unset($data->wspassword);
-        unset($data->token);
-        unset($data->authmethod);
-
-        $params = array();
-        $params['members'] = array();
-        for ($i=0; $i<10; $i++) {
-            if (empty($data->groupid[$i]) or empty($data->userid[$i])) {
-                continue;
-            }
-            $params['members'][] = array('userid'=>$data->userid[$i], 'groupid'=>$data->groupid[$i]);
-        }
-
-        return $params;
-    }
-}
-
-class moodle_group_delete_groupmembers_form extends moodleform {
-    public function definition() {
-        global $CFG;
-
-        $mform = $this->_form;
-
-        $mform->addElement('header', 'wstestclienthdr', get_string('testclient', 'webservice'));
-
-        //note: these values are intentionally PARAM_RAW - we want users to test any rubbish as parameters
-        $data = $this->_customdata;
-        if ($data['authmethod'] == 'simple') {
-            $mform->addElement('text', 'wsusername', 'wsusername');
-            $mform->setType('wsusername', core_user::get_property_type('username'));
-            $mform->addElement('text', 'wspassword', 'wspassword');
-            $mform->setType('wspassword', core_user::get_property_type('password'));
-        } else if ($data['authmethod'] == 'token') {
-            $mform->addElement('text', 'token', 'token');
-            $mform->setType('token', PARAM_RAW_TRIMMED);
-        }
-
-        $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', core_user::get_property_type('auth'));
-        $mform->addElement('text', 'userid[0]', 'userid[0]');
-        $mform->addElement('text', 'groupid[0]', 'groupid[0]');
-        $mform->addElement('text', 'userid[1]', 'userid[1]');
-        $mform->addElement('text', 'groupid[1]', 'groupid[1]');
-        $mform->setType('userid', PARAM_INT);
-        $mform->setType('groupids', PARAM_INT);
-
-        $mform->addElement('hidden', 'function');
-        $mform->setType('function', PARAM_PLUGIN);
-
-        $mform->addElement('hidden', 'protocol');
-        $mform->setType('protocol', PARAM_ALPHA);
-
-        $this->add_action_buttons(true, get_string('execute', 'webservice'));
-    }
-
-    public function get_params() {
-        if (!$data = $this->get_data()) {
-            return null;
-        }
-        // remove unused from form data
-        unset($data->submitbutton);
-        unset($data->protocol);
-        unset($data->function);
-        unset($data->wsusername);
-        unset($data->wspassword);
-        unset($data->token);
-        unset($data->authmethod);
-
-        $params = array();
-        $params['members'] = array();
-        for ($i=0; $i<10; $i++) {
-            if (empty($data->groupid[$i]) or empty($data->userid[$i])) {
-                continue;
-            }
-            $params['members'][] = array('userid'=>$data->userid[$i], 'groupid'=>$data->groupid[$i]);
-        }
-
-        return $params;
-    }
-}
-
 /**
  * Form class for create_categories() web service function test.
  *
diff --git a/auth/lti/auth.php b/auth/lti/auth.php
new file mode 100644 (file)
index 0000000..b6eee11
--- /dev/null
@@ -0,0 +1,55 @@
+<?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/>.
+
+/**
+ * LTI Authentication plugin.
+ *
+ * @package auth_lti
+ * @copyright 2016 Mark Nelson <markn@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/authlib.php');
+
+/**
+ * LTI Authentication plugin.
+ *
+ * @package auth_lti
+ * @copyright 2016 Mark Nelson <markn@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class auth_plugin_lti extends auth_plugin_base {
+
+    /**
+     * Constructor.
+     */
+    public function __construct() {
+        $this->authtype = 'lti';
+    }
+
+    /**
+     * Users can not log in via the traditional login form.
+     *
+     * @param string $username The username
+     * @param string $password The password
+     * @return bool Authentication success or failure
+     */
+    public function user_login($username, $password) {
+        return false;
+    }
+}
diff --git a/auth/lti/lang/en/auth_lti.php b/auth/lti/lang/en/auth_lti.php
new file mode 100644 (file)
index 0000000..9b1d030
--- /dev/null
@@ -0,0 +1,26 @@
+<?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/>.
+
+/**
+ * Strings for component 'auth_lti', language 'en'.
+ *
+ * @package auth_lti
+ * @copyright 2016 Mark Nelson <markn@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['auth_ltidescription'] = 'The LTI authentication plugin enables your site to behave as an LTI provider - this plugin works in conjunction with the LTI enrolment plugin by allowing external users to access a course or individual activities.';
+$string['pluginname'] = 'LTI';
diff --git a/auth/lti/version.php b/auth/lti/version.php
new file mode 100644 (file)
index 0000000..bee4bf0
--- /dev/null
@@ -0,0 +1,29 @@
+<?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/>.
+
+/**
+ * LTI authentication plugin version information
+ *
+ * @package auth_lti
+ * @copyright 2016 Mark Nelson <markn@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version = 2016040800; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->requires = 2016040700; // Requires this Moodle version (3.1).
+$plugin->component = 'auth_lti'; // Full name of the plugin (used for diagnostics).
index 9367cf3..2ace0f3 100644 (file)
@@ -1541,6 +1541,8 @@ class backup_activity_logstores_structure_step extends backup_course_logstores_s
 class backup_course_competencies_structure_step extends backup_structure_step {
 
     protected function define_structure() {
+        $userinfo = $this->get_setting_value('users');
+
         $wrapper = new backup_nested_element('course_competencies');
 
         $settings = new backup_nested_element('settings', array('id'), array('pushratingstouserplans'));
@@ -1554,11 +1556,11 @@ class backup_course_competencies_structure_step extends backup_structure_step {
         $competencies = new backup_nested_element('competencies');
         $wrapper->add_child($competencies);
 
-        $competency = new backup_nested_element('competency', null, array('idnumber', 'ruleoutcome',
-            'sortorder', 'frameworkidnumber'));
+        $competency = new backup_nested_element('competency', null, array('id', 'idnumber', 'ruleoutcome',
+            'sortorder', 'frameworkid', 'frameworkidnumber'));
         $competencies->add_child($competency);
 
-        $sql = 'SELECT c.idnumber, cc.ruleoutcome, cc.sortorder, f.idnumber AS frameworkidnumber
+        $sql = 'SELECT c.id, c.idnumber, cc.ruleoutcome, cc.sortorder, f.id AS frameworkid, f.idnumber AS frameworkidnumber
                   FROM {' . \core_competency\course_competency::TABLE . '} cc
                   JOIN {' . \core_competency\competency::TABLE . '} c ON c.id = cc.competencyid
                   JOIN {' . \core_competency\competency_framework::TABLE . '} f ON f.id = c.competencyframeworkid
@@ -1566,6 +1568,21 @@ class backup_course_competencies_structure_step extends backup_structure_step {
               ORDER BY cc.sortorder';
         $competency->set_source_sql($sql, array('courseid' => backup::VAR_COURSEID));
 
+        $usercomps = new backup_nested_element('user_competencies');
+        $wrapper->add_child($usercomps);
+        if ($userinfo) {
+            $usercomp = new backup_nested_element('user_competency', null, array('userid', 'competencyid',
+                'proficiency', 'grade'));
+            $usercomps->add_child($usercomp);
+
+            $sql = 'SELECT ucc.userid, ucc.competencyid, ucc.proficiency, ucc.grade
+                      FROM {' . \core_competency\user_competency_course::TABLE . '} ucc
+                     WHERE ucc.courseid = :courseid
+                       AND ucc.grade IS NOT NULL';
+            $usercomp->set_source_sql($sql, array('courseid' => backup::VAR_COURSEID));
+            $usercomp->annotate_ids('user', 'userid');
+        }
+
         return $wrapper;
     }
 }
index 2de6165..861dcae 100644 (file)
@@ -3066,10 +3066,15 @@ class restore_course_competencies_structure_step extends restore_structure_step
      * @return array
      */
     protected function define_structure() {
+        $userinfo = $this->get_setting_value('users');
         $paths = array(
             new restore_path_element('course_competency', '/course_competencies/competencies/competency'),
-            new restore_path_element('course_competency_settings', '/course_competencies/settings')
+            new restore_path_element('course_competency_settings', '/course_competencies/settings'),
         );
+        if ($userinfo) {
+            $paths[] = new restore_path_element('user_competency_course',
+                '/course_competencies/user_competencies/user_competency');
+        }
         return $paths;
     }
 
@@ -3080,22 +3085,27 @@ class restore_course_competencies_structure_step extends restore_structure_step
      */
     public function process_course_competency_settings($data) {
         global $DB;
-
         $data = (object) $data;
+
+        // We do not restore the course settings during merge.
+        $target = $this->get_task()->get_target();
+        if ($target == backup::TARGET_CURRENT_ADDING || $target == backup::TARGET_EXISTING_ADDING) {
+            return;
+        }
+
         $courseid = $this->task->get_courseid();
-        $exists = \core_competency\course_competency_settings::get_record(array('courseid' => $courseid));
+        $exists = \core_competency\course_competency_settings::record_exists_select('courseid = :courseid',
+            array('courseid' => $courseid));
 
-        // Now update or insert.
+        // Strangely the course settings already exist, let's just leave them as is then.
         if ($exists) {
-            $settings = $exists;
-            $settings->set_pushratingstouserplans($data->pushratingstouserplans);
-            return $settings->update();
-        } else {
-            $data = (object) array('courseid' => $courseid, 'pushratingstouserplans' => $data->pushratingstouserplans);
-            $settings = new \core_competency\course_competency_settings(0, $data);
-            $result = $settings->create();
-            return !empty($result);
+            $this->log('Course competency settings not restored, existing settings have been found.', backup::LOG_WARNING);
+            return;
         }
+
+        $data = (object) array('courseid' => $courseid, 'pushratingstouserplans' => $data->pushratingstouserplans);
+        $settings = new \core_competency\course_competency_settings(0, $data);
+        $settings->create();
     }
 
     /**
@@ -3116,6 +3126,7 @@ class restore_course_competencies_structure_step extends restore_structure_step
         if (!$competency) {
             return;
         }
+        $this->set_mapping(\core_competency\competency::TABLE, $data->id, $competency->get_id());
 
         $params = array(
             'competencyid' => $competency->get_id(),
@@ -3133,6 +3144,41 @@ class restore_course_competencies_structure_step extends restore_structure_step
         }
     }
 
+    /**
+     * Process the user competency course.
+     *
+     * @param array $data The data.
+     */
+    public function process_user_competency_course($data) {
+        global $USER, $DB;
+        $data = (object) $data;
+
+        $data->competencyid = $this->get_mappingid(\core_competency\competency::TABLE, $data->competencyid);
+        if (!$data->competencyid) {
+            // This is strange, the competency does not belong to the course.
+            return;
+        } else if ($data->grade === null) {
+            // We do not need to do anything when there is no grade.
+            return;
+        }
+
+        $data->userid = $this->get_mappingid('user', $data->userid);
+        $shortname = $DB->get_field('course', 'shortname', array('id' => $this->task->get_courseid()), MUST_EXIST);
+
+        // The method add_evidence also sets the course rating.
+        \core_competency\api::add_evidence($data->userid,
+                                           $data->competencyid,
+                                           $this->task->get_contextid(),
+                                           \core_competency\evidence::ACTION_OVERRIDE,
+                                           'evidence_courserestored',
+                                           'core_competency',
+                                           $shortname,
+                                           false,
+                                           null,
+                                           $data->grade,
+                                           $USER->id);
+    }
+
     /**
      * Execute conditions.
      *
@@ -3140,6 +3186,11 @@ class restore_course_competencies_structure_step extends restore_structure_step
      */
     protected function execute_condition() {
 
+        // Do not restore when competencies are disabled.
+        if (!\core_competency\api::is_enabled()) {
+            return false;
+        }
+
         // Do not execute if the competencies XML file is not found.
         $fullpath = $this->task->get_taskbasepath();
         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
@@ -3210,6 +3261,11 @@ class restore_activity_competencies_structure_step extends restore_structure_ste
      */
     protected function execute_condition() {
 
+        // Do not restore when competencies are disabled.
+        if (!\core_competency\api::is_enabled()) {
+            return false;
+        }
+
         // Do not execute if the competencies XML file is not found.
         $fullpath = $this->task->get_taskbasepath();
         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
index a2aedb6..b57d54d 100644 (file)
@@ -4,11 +4,6 @@ Feature: Block activity modules
   As a manager
   I can add activities block in a course or on the frontpage
 
-  Background:
-    Given I log in as "admin"
-    And I navigate to "Manage activities" node in "Site administration > Plugins > Activity modules"
-    And I click on "//a[@title=\"Show\"]" "xpath_element" in the "Feedback" "table_row"
-
   Scenario: Add activities block on the frontpage
     Given the following "activities" exist:
       | activity   | name                        | intro                              | course               | idnumber    |
@@ -34,8 +29,9 @@ Feature: Block activity modules
       | wiki       | Frontpage wiki name         | Frontpage wiki description         | Acceptance test site | wiki0       |
       | workshop   | Frontpage workshop name     | Frontpage workshop description     | Acceptance test site | workshop0   |
 
+    When I log in as "admin"
     And I am on site homepage
-    When I follow "Turn editing on"
+    And I follow "Turn editing on"
     And I add the "Activities" block
     And I click on "Assignments" "link" in the "Activities" "block"
     Then I should see "Frontpage assignment name"
@@ -112,7 +108,8 @@ Feature: Block activity modules
       | wiki       | Test wiki name         | Test wiki description         | C1     | wiki1       |
       | workshop   | Test workshop name     | Test workshop description     | C1     | workshop1   |
 
-    When I follow "Courses"
+    When I log in as "admin"
+    And I follow "Courses"
     And I follow "Course 1"
     And I turn editing mode on
     And I add the "Activities" block
index fd06623..add5034 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-if (is_file($CFG->dirroot.'/mod/feedback/lib.php')) {
-    require_once($CFG->dirroot.'/mod/feedback/lib.php');
-    define('FEEDBACK_BLOCK_LIB_IS_OK', true);
-}
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/mod/feedback/lib.php');
 
 class block_feedback extends block_list {
 
@@ -49,11 +48,6 @@ class block_feedback extends block_list {
         $this->content->icons = array();
         $this->content->footer = '';
 
-        if (!defined('FEEDBACK_BLOCK_LIB_IS_OK')) {
-            $this->content->items = array(get_string('missing_feedback_module', 'block_feedback'));
-            return $this->content;
-        }
-
         $courseid = $this->page->course->id;
         if ($courseid <= 0) {
             $courseid = SITEID;
index 559248b..737c9e3 100644 (file)
@@ -25,8 +25,5 @@
 function xmldb_block_feedback_install() {
     global $DB;
 
-/// Disable this block by default (because Feedback is not technically part of 2.0)
-    $DB->set_field('block', 'visible', 0, array('name'=>'feedback'));
-
 }
 
index 982755e..4999f02 100644 (file)
@@ -24,5 +24,4 @@
 
 $string['feedback'] = 'Feedback';
 $string['feedback:addinstance'] = 'Add a new feedback block';
-$string['missing_feedback_module'] = 'This blocks relies on the Feedback activity module, but that module is not present!';
 $string['pluginname'] = 'Feedback';
index 9ecb706..84ec315 100644 (file)
@@ -508,9 +508,14 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
      * @return array
      */
     protected static function get_prefixed_keys(Memcached $connection, $prefix) {
+        $connkeys = $connection->getAllKeys();
+        if (empty($connkeys)) {
+            return array();
+        }
+
         $keys = array();
         $start = strlen($prefix);
-        foreach ($connection->getAllKeys() as $key) {
+        foreach ($connkeys as $key) {
             if (strpos($key, $prefix) === 0) {
                 $keys[] = substr($key, $start);
             }
index 101d29a..05f193d 100644 (file)
@@ -506,10 +506,6 @@ class competency extends persistent {
         } else if (!preg_match('@/([0-9]+/)+@', $value)) {
             // The format of the path is not correct.
             return new lang_string('invaliddata', 'error');
-
-        } else if ((substr_count($value, '/') - 1) > competency_framework::get_taxonomies_max_level()) {
-            // Validate the depth of the path.
-            return new lang_string('invaliddata', 'error');
         }
 
         return true;
@@ -697,6 +693,29 @@ class competency extends persistent {
         return $rules;
     }
 
+    /**
+     * Return the current depth of a competency framework.
+     *
+     * @param int $frameworkid The framework ID.
+     * @return int
+     */
+    public static function get_framework_depth($frameworkid) {
+        global $DB;
+        $totallength = $DB->sql_length('path');
+        $trimmedlength = $DB->sql_length("REPLACE(path, '/', '')");
+        $sql = "SELECT ($totallength - $trimmedlength - 1) AS depth
+                  FROM {" . self::TABLE . "}
+                 WHERE competencyframeworkid = :id
+              ORDER BY depth DESC";
+        $record = $DB->get_record_sql($sql, array('id' => $frameworkid), IGNORE_MULTIPLE);
+        if (!$record) {
+            $depth = 0;
+        } else {
+            $depth = $record->depth;
+        }
+        return $depth;
+    }
+
     /**
      * Build a framework tree with competency nodes.
      *
index 79d8979..5e51dcc 100644 (file)
@@ -133,6 +133,16 @@ class competency_framework extends persistent {
 
     }
 
+    /**
+     * Return the current depth of a competency framework.
+     *
+     * @see competency::get_framework_depth()
+     * @return int
+     */
+    public function get_depth() {
+        return competency::get_framework_depth($this->get_id());
+    }
+
     /**
      * Return the scale.
      *
@@ -176,8 +186,8 @@ class competency_framework extends persistent {
         unset($taxonomies[0]);
 
         // Ensure that we do not return empty levels.
-        for ($i = 1; $i <= self::get_taxonomies_max_level(); $i++) {
-            if (empty($taxonomies[$i])) {
+        foreach ($taxonomies as $i => $taxonomy) {
+            if (empty($taxonomy)) {
                 $taxonomies[$i] = self::TAXONOMY_COMPETENCY;
             }
         }
@@ -335,10 +345,6 @@ class competency_framework extends persistent {
     protected function validate_taxonomies($value) {
         $terms = explode(',', $value);
 
-        if (count($terms) > self::get_taxonomies_max_level()) {
-            return new lang_string('invaliddata', 'error');
-        }
-
         foreach ($terms as $term) {
             if (!empty($term) && !array_key_exists($term, self::get_taxonomies_list())) {
                 return new lang_string('invalidtaxonomy', 'core_competency', $term);
@@ -410,18 +416,6 @@ class competency_framework extends persistent {
         return self::get_taxonomies_list()[$constant];
     }
 
-    /**
-     * Return the maximum number of taxonomy levels.
-     *
-     * This is a method and not a constant because we want to make it easy to adapt
-     * to the number of levels desired in the future.
-     *
-     * @return int
-     */
-    public static function get_taxonomies_max_level() {
-        return 6;
-    }
-
     /**
      * Get the list of all taxonomies.
      *
diff --git a/competency/tests/competency_test.php b/competency/tests/competency_test.php
new file mode 100644 (file)
index 0000000..caff731
--- /dev/null
@@ -0,0 +1,71 @@
+<?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/>.
+
+/**
+ * Competency tests.
+ *
+ * @package    core_competency
+ * @copyright  2016 Frédéric Massart - FMCorz.net
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+global $CFG;
+
+use core_competency\competency;
+
+/**
+ * Competency testcase.
+ *
+ * @package    core_competency
+ * @copyright  2016 Frédéric Massart - FMCorz.net
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_competency_competency_testcase extends advanced_testcase {
+
+    public function test_get_framework_depth() {
+        $this->resetAfterTest();
+
+        $ccg = $this->getDataGenerator()->get_plugin_generator('core_competency');
+        $f1 = $ccg->create_framework();
+        $f2 = $ccg->create_framework();
+        $f3 = $ccg->create_framework();
+        $f4 = $ccg->create_framework();
+
+        $f1c1 = $ccg->create_competency(['competencyframeworkid' => $f1->get_id()]);
+        $f1c11 = $ccg->create_competency(['competencyframeworkid' => $f1->get_id(), 'parentid' => $f1c1->get_id()]);
+        $f1c111 = $ccg->create_competency(['competencyframeworkid' => $f1->get_id(), 'parentid' => $f1c11->get_id()]);
+        $f1c1111 = $ccg->create_competency(['competencyframeworkid' => $f1->get_id(), 'parentid' => $f1c111->get_id()]);
+
+        $f2c1 = $ccg->create_competency(['competencyframeworkid' => $f2->get_id()]);
+        $f2c2 = $ccg->create_competency(['competencyframeworkid' => $f2->get_id()]);
+        $f2c21 = $ccg->create_competency(['competencyframeworkid' => $f2->get_id(), 'parentid' => $f2c2->get_id()]);
+        $f2c22 = $ccg->create_competency(['competencyframeworkid' => $f2->get_id(), 'parentid' => $f2c2->get_id()]);
+        $f2c211 = $ccg->create_competency(['competencyframeworkid' => $f2->get_id(), 'parentid' => $f2c21->get_id()]);
+        $f2c221 = $ccg->create_competency(['competencyframeworkid' => $f2->get_id(), 'parentid' => $f2c22->get_id()]);
+        $f2c222 = $ccg->create_competency(['competencyframeworkid' => $f2->get_id(), 'parentid' => $f2c22->get_id()]);
+        $f2c223 = $ccg->create_competency(['competencyframeworkid' => $f2->get_id(), 'parentid' => $f2c22->get_id()]);
+        $f2c3 = $ccg->create_competency(['competencyframeworkid' => $f2->get_id()]);
+
+        $f3c1 = $ccg->create_competency(['competencyframeworkid' => $f3->get_id()]);
+
+        $this->assertEquals(4, competency::get_framework_depth($f1->get_id()));
+        $this->assertEquals(3, competency::get_framework_depth($f2->get_id()));
+        $this->assertEquals(1, competency::get_framework_depth($f3->get_id()));
+        $this->assertEquals(0, competency::get_framework_depth($f4->get_id()));
+    }
+
+}
index 7391ada..865a5e2 100644 (file)
@@ -429,16 +429,17 @@ class core_competency_plan_testcase extends advanced_testcase {
         // Reopening plan: with due date in the future => duedate unchanged.
         $record = $plan->to_record();
         $record->status = plan::STATUS_COMPLETE;
-        $record->duedate = time() + plan::DUEDATE_THRESHOLD + 10;
+        $duedate = time() + plan::DUEDATE_THRESHOLD + 10;
+        $record->duedate = $duedate;
         $DB->update_record(plan::TABLE, $record);
 
         $success = core_competency\api::reopen_plan($plan->get_id());
         $this->assertTrue($success);
         $plan->read();
 
-        // Check that the due date has not changed, but allow for PHP Unit latency.
-        $this->assertTrue($plan->get_duedate() >= time() + plan::DUEDATE_THRESHOLD + 10);
-        $this->assertTrue($plan->get_duedate() <= time() + plan::DUEDATE_THRESHOLD + 15);
+        // Check that the due date has not changed.
+        $this->assertNotEquals(0, $plan->get_duedate());
+        $this->assertEquals($duedate, $plan->get_duedate());
     }
 
     public function test_get_by_user_and_competency() {
index 7c5d975..da654a2 100644 (file)
@@ -2495,108 +2495,3 @@ class core_course_external extends external_api {
     }
 
 }
-
-/**
- * Deprecated course external functions
- *
- * @package    core_course
- * @copyright  2009 Petr Skodak
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- * @since Moodle 2.0
- * @deprecated Moodle 2.2 MDL-29106 - Please do not use this class any more.
- * @see core_course_external
- */
-class moodle_course_external extends external_api {
-
-    /**
-     * Returns description of method parameters
-     *
-     * @return external_function_parameters
-     * @since Moodle 2.0
-     * @deprecated Moodle 2.2 MDL-29106 - Please do not call this function any more.
-     * @see core_course_external::get_courses_parameters()
-     */
-    public static function get_courses_parameters() {
-        return core_course_external::get_courses_parameters();
-    }
-
-    /**
-     * Get courses
-     *
-     * @param array $options
-     * @return array
-     * @since Moodle 2.0
-     * @deprecated Moodle 2.2 MDL-29106 - Please do not call this function any more.
-     * @see core_course_external::get_courses()
-     */
-    public static function get_courses($options) {
-        return core_course_external::get_courses($options);
-    }
-
-    /**
-     * Returns description of method result value
-     *
-     * @return external_description
-     * @since Moodle 2.0
-     * @deprecated Moodle 2.2 MDL-29106 - Please do not call this function any more.
-     * @see core_course_external::get_courses_returns()
-     */
-    public static function get_courses_returns() {
-        return core_course_external::get_courses_returns();
-    }
-
-    /**
-     * Marking the method as deprecated.
-     *
-     * @return bool
-     */
-    public static function get_courses_is_deprecated() {
-        return true;
-    }
-
-    /**
-     * Returns description of method parameters
-     *
-     * @return external_function_parameters
-     * @since Moodle 2.0
-     * @deprecated Moodle 2.2 MDL-29106 - Please do not call this function any more.
-     * @see core_course_external::create_courses_parameters()
-     */
-    public static function create_courses_parameters() {
-        return core_course_external::create_courses_parameters();
-    }
-
-    /**
-     * Create  courses
-     *
-     * @param array $courses
-     * @return array courses (id and shortname only)
-     * @since Moodle 2.0
-     * @deprecated Moodle 2.2 MDL-29106 - Please do not call this function any more.
-     * @see core_course_external::create_courses()
-     */
-    public static function create_courses($courses) {
-        return core_course_external::create_courses($courses);
-    }
-
-    /**
-     * Returns description of method result value
-     *
-     * @return external_description
-     * @since Moodle 2.0
-     * @deprecated Moodle 2.2 MDL-29106 - Please do not call this function any more.
-     * @see core_course_external::create_courses_returns()
-     */
-    public static function create_courses_returns() {
-        return core_course_external::create_courses_returns();
-    }
-
-    /**
-     * Marking the method as deprecated.
-     *
-     * @return bool
-     */
-    public static function create_courses_is_deprecated() {
-        return true;
-    }
-}
index 38d3ae7..12cea2e 100644 (file)
@@ -28,7 +28,7 @@ require_once('editinstance_form.php');
 $courseid   = required_param('courseid', PARAM_INT);
 $type   = required_param('type', PARAM_COMPONENT);
 $instanceid = optional_param('id', 0, PARAM_INT);
-
+$return = optional_param('returnurl', 0, PARAM_LOCALURL);
 $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
 $context = context_course::instance($course->id, MUST_EXIST);
 
@@ -43,7 +43,10 @@ require_capability('enrol/' . $type . ':config', $context);
 $PAGE->set_url('/enrol/editinstance.php', array('courseid' => $course->id, 'id' => $instanceid, 'type' => $type));
 $PAGE->set_pagelayout('admin');
 
-$return = new moodle_url('/enrol/instances.php', array('id' => $course->id));
+if (empty($return)) {
+    $return = new moodle_url('/enrol/instances.php', array('id' => $course->id));
+}
+
 if (!enrol_is_enabled($type)) {
     redirect($return);
 }
@@ -62,7 +65,7 @@ if ($instanceid) {
     $instance->status   = ENROL_INSTANCE_ENABLED; // Do not use default for automatically created instances here.
 }
 
-$mform = new enrol_instance_edit_form(null, array($instance, $plugin, $context, $type));
+$mform = new enrol_instance_edit_form(null, array($instance, $plugin, $context, $type, $return));
 
 if ($mform->is_cancelled()) {
     redirect($return);
index 728a7e8..53ab92f 100644 (file)
@@ -45,7 +45,7 @@ class enrol_instance_edit_form extends moodleform {
 
         $mform = $this->_form;
 
-        list($instance, $plugin, $context, $type) = $this->_customdata;
+        list($instance, $plugin, $context, $type, $returnurl) = $this->_customdata;
 
         $mform->addElement('header', 'header', get_string('pluginname', 'enrol_' . $type));
 
@@ -60,6 +60,10 @@ class enrol_instance_edit_form extends moodleform {
         $mform->setType('type', PARAM_COMPONENT);
         $instance->type = $type;
 
+        $mform->addElement('hidden', 'returnurl');
+        $mform->setType('returnurl', PARAM_LOCALURL);
+        $mform->setConstant('returnurl', $returnurl);
+
         $this->add_action_buttons(true, ($instance->id ? null : get_string('addinstance', 'enrol')));
 
         $this->set_data($instance);
index 959df21..8f8347b 100644 (file)
@@ -839,303 +839,3 @@ class core_role_external extends external_api {
         return null;
     }
 }
-
-
-/**
- * Deprecated enrol and role external functions
- *
- * @package    core_enrol
- * @copyright  2010 Jerome Mouneyrac
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- * @since Moodle 2.0
- * @deprecated Moodle 2.2 MDL-29106 - Please do not use this class any more.
- * @see core_enrol_external
- * @see core_role_external
- */
-class moodle_enrol_external extends external_api {
-
-
-    /**
-     * Returns description of method parameters
-     *
-     * @return external_function_parameters
-     * @since Moodle 2.0
-     * @deprecated Moodle 2.2 MDL-29106 - Please do not call this function any more.
-     * @see core_enrol_external::get_enrolled_users_parameters()
-     */
-    public static function get_enrolled_users_parameters() {
-        return new external_function_parameters(
-            array(
-                'courseid'       => new external_value(PARAM_INT, 'Course id'),
-                'withcapability' => new external_value(PARAM_CAPABILITY, 'User should have this capability', VALUE_DEFAULT, null),
-                'groupid'        => new external_value(PARAM_INT, 'Group id, null means all groups', VALUE_DEFAULT, null),
-                'onlyactive'     => new external_value(PARAM_INT, 'True means only active, false means all participants', VALUE_DEFAULT, 0),
-            )
-        );
-    }
-
-    /**
-     * Get list of course participants.
-     *
-     * @param int $courseid
-     * @param text $withcapability
-     * @param int $groupid
-     * @param bool $onlyactive
-     * @return array of course participants
-     * @since Moodle 2.0
-     * @deprecated Moodle 2.2 MDL-29106 - Please do not call this function any more.
-     * @see core_enrol_external::get_enrolled_users()
-     */
-    public static function get_enrolled_users($courseid, $withcapability = null, $groupid = null, $onlyactive = false) {
-        global $DB, $CFG, $USER;
-
-        // Do basic automatic PARAM checks on incoming data, using params description
-        // If any problems are found then exceptions are thrown with helpful error messages
-        $params = self::validate_parameters(self::get_enrolled_users_parameters(), array(
-            'courseid'=>$courseid,
-            'withcapability'=>$withcapability,
-            'groupid'=>$groupid,
-            'onlyactive'=>$onlyactive)
-        );
-
-        $coursecontext = context_course::instance($params['courseid'], IGNORE_MISSING);
-        if ($courseid == SITEID) {
-            $context = context_system::instance();
-        } else {
-            $context = $coursecontext;
-        }
-
-        try {
-            self::validate_context($context);
-        } catch (Exception $e) {
-            $exceptionparam = new stdClass();
-            $exceptionparam->message = $e->getMessage();
-            $exceptionparam->courseid = $params['courseid'];
-            throw new moodle_exception('errorcoursecontextnotvalid' , 'webservice', '', $exceptionparam);
-        }
-
-        if ($courseid == SITEID) {
-            require_capability('moodle/site:viewparticipants', $context);
-        } else {
-            require_capability('moodle/course:viewparticipants', $context);
-        }
-
-        if ($withcapability) {
-            require_capability('moodle/role:review', $coursecontext);
-        }
-        if ($groupid && groups_is_member($groupid)) {
-            require_capability('moodle/site:accessallgroups', $coursecontext);
-        }
-        if ($onlyactive) {
-            require_capability('moodle/course:enrolreview', $coursecontext);
-        }
-
-        list($sqlparams, $params) =  get_enrolled_sql($coursecontext, $withcapability, $groupid, $onlyactive);
-        $sql = "SELECT ue.userid, e.courseid, u.firstname, u.lastname, u.username, c.id as usercontextid
-                  FROM {user_enrolments} ue
-                  JOIN {enrol} e ON (e.id = ue.enrolid)
-                  JOIN {user} u ON (ue.userid = u.id)
-                  JOIN {context} c ON (u.id = c.instanceid AND contextlevel = " . CONTEXT_USER . ")
-                  WHERE e.courseid = :courseid AND ue.userid IN ($sqlparams)
-                  GROUP BY ue.userid, e.courseid, u.firstname, u.lastname, u.username, c.id";
-        $params['courseid'] = $courseid;
-        $enrolledusers = $DB->get_records_sql($sql, $params);
-        $result = array();
-        $isadmin = is_siteadmin($USER);
-        $canviewfullnames = has_capability('moodle/site:viewfullnames', $context);
-        foreach ($enrolledusers as $enrolleduser) {
-            $profilimgurl = moodle_url::make_pluginfile_url($enrolleduser->usercontextid, 'user', 'icon', NULL, '/', 'f1');
-            $profilimgurlsmall = moodle_url::make_pluginfile_url($enrolleduser->usercontextid, 'user', 'icon', NULL, '/', 'f2');
-            $resultuser = array(
-                'courseid' => $enrolleduser->courseid,
-                'userid' => $enrolleduser->userid,
-                'fullname' => fullname($enrolleduser),
-                'profileimgurl' => $profilimgurl->out(false),
-                'profileimgurlsmall' => $profilimgurlsmall->out(false)
-            );
-            // check if we can return username
-            if ($isadmin) {
-                $resultuser['username'] = $enrolleduser->username;
-            }
-            // check if we can return first and last name
-            if ($isadmin or $canviewfullnames) {
-                $resultuser['firstname'] = $enrolleduser->firstname;
-                $resultuser['lastname'] = $enrolleduser->lastname;
-            }
-            $result[] = $resultuser;
-        }
-
-        return $result;
-    }
-
-    /**
-     * Returns description of method result value
-     *
-     * @return external_description
-     * @since Moodle 2.0
-     * @deprecated Moodle 2.2 MDL-29106 - Please do not call this function any more.
-     * @see core_enrol_external::get_enrolled_users_returns()
-     */
-    public static function get_enrolled_users_returns() {
-        return new external_multiple_structure(
-            new external_single_structure(
-                array(
-                    'courseid' => new external_value(PARAM_INT, 'id of course'),
-                    'userid' => new external_value(PARAM_INT, 'id of user'),
-                    'firstname' => new external_value(PARAM_RAW, 'first name of user', VALUE_OPTIONAL),
-                    'lastname' => new external_value(PARAM_RAW, 'last name of user', VALUE_OPTIONAL),
-                    'fullname' => new external_value(PARAM_RAW, 'fullname of user'),
-                    'username' => new external_value(PARAM_RAW, 'username of user', VALUE_OPTIONAL),
-                    'profileimgurl' => new external_value(PARAM_URL, 'url of the profile image'),
-                    'profileimgurlsmall' => new external_value(PARAM_URL, 'url of the profile image (small version)')
-                )
-            )
-        );
-    }
-
-    /**
-     * Marking the method as deprecated.
-     *
-     * @return bool
-     */
-    public static function get_enrolled_users_is_deprecated() {
-        return true;
-    }
-
-    /**
-     * Returns description of method parameters
-     *
-     * @return external_function_parameters
-     * @since Moodle 2.1
-     * @deprecated Moodle 2.2 MDL-29106 - Please do not call this function any more.
-     * @see core_enrol_external::get_users_courses_parameters()
-     */
-    public static function get_users_courses_parameters() {
-        return core_enrol_external::get_users_courses_parameters();
-    }
-
-    /**
-     * Get list of courses user is enrolled in (only active enrolments are returned).
-     * Please note the current user must be able to access the course, otherwise the course is not included.
-     *
-     * @param int $userid
-     * @return array of courses
-     * @since Moodle 2.1
-     * @deprecated Moodle 2.2 MDL-29106 - Please do not call this function any more.
-     * @see use core_enrol_external::get_users_courses()
-     */
-    public static function get_users_courses($userid) {
-        return core_enrol_external::get_users_courses($userid);
-    }
-
-    /**
-     * Returns description of method result value
-     *
-     * @return external_description
-     * @since Moodle 2.1
-     * @deprecated Moodle 2.2 MDL-29106 - Please do not call this function any more.
-     * @see core_enrol_external::get_users_courses_returns()
-     */
-    public static function get_users_courses_returns() {
-        return core_enrol_external::get_users_courses_returns();
-    }
-
-    /**
-     * Marking the method as deprecated.
-     *
-     * @return bool
-     */
-    public static function get_users_courses_is_deprecated() {
-        return true;
-    }
-
-    /**
-     * Returns description of method parameters
-     *
-     * @return external_function_parameters
-     * @since Moodle 2.0
-     * @deprecated Moodle 2.2 MDL-29106 - Please do not call this function any more.
-     * @see core_role_external::assign_roles_parameters()
-     */
-    public static function role_assign_parameters() {
-        return core_role_external::assign_roles_parameters();
-    }
-
-    /**
-     * Manual role assignments to users
-     *
-     * @param array $assignments An array of manual role assignment
-     * @since Moodle 2.0
-     * @deprecated Moodle 2.2 MDL-29106 - Please do not call this function any more.
-     * @see core_role_external::assign_roles()
-     */
-    public static function role_assign($assignments) {
-        return core_role_external::assign_roles($assignments);
-    }
-
-    /**
-     * Returns description of method result value
-     *
-     * @return null
-     * @since Moodle 2.0
-     * @deprecated Moodle 2.2 MDL-29106 - Please do not call this function any more.
-     * @see core_role_external::assign_roles_returns()
-     */
-    public static function role_assign_returns() {
-        return core_role_external::assign_roles_returns();
-    }
-
-    /**
-     * Marking the method as deprecated.
-     *
-     * @return bool
-     */
-    public static function role_assign_is_deprecated() {
-        return true;
-    }
-
-    /**
-     * Returns description of method parameters
-     *
-     * @return external_function_parameters
-     * @since Moodle 2.0
-     * @deprecated Moodle 2.2 MDL-29106 - Please do not call this function any more.
-     * @see core_role_external::unassign_roles_parameters()
-     */
-    public static function role_unassign_parameters() {
-        return core_role_external::unassign_roles_parameters();
-    }
-
-     /**
-     * Unassign roles from users
-     *
-     * @param array $unassignments An array of unassignment
-     * @since Moodle 2.0
-     * @deprecated Moodle 2.2 MDL-29106 - Please do not call this function any more.
-     * @see core_role_external::unassign_roles()
-     */
-    public static function role_unassign($unassignments) {
-         return core_role_external::unassign_roles($unassignments);
-    }
-
-   /**
-     * Returns description of method result value
-     *
-     * @return null
-     * @since Moodle 2.0
-     * @deprecated Moodle 2.2 MDL-29106 - Please do not call this function any more.
-     * @see core_role_external::unassign_roles_returns()
-     */
-    public static function role_unassign_returns() {
-        return core_role_external::unassign_roles_returns();
-    }
-
-    /**
-     * Marking the method as deprecated.
-     *
-     * @return bool
-     */
-    public static function role_unassign_is_deprecated() {
-        return true;
-    }
-}
diff --git a/enrol/lti/backup/moodle2/backup_enrol_lti_plugin.class.php b/enrol/lti/backup/moodle2/backup_enrol_lti_plugin.class.php
new file mode 100644 (file)
index 0000000..6cd5497
--- /dev/null
@@ -0,0 +1,71 @@
+<?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/>.
+
+/**
+ * Defines the backup_enrol_lti_plugin class.
+ *
+ * @package   enrol_lti
+ * @copyright 2016 Mark Nelson <markn@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Define all the backup steps.
+ *
+ * @package   enrol_lti
+ * @copyright 2016 Mark Nelson <markn@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class backup_enrol_lti_plugin extends backup_enrol_plugin {
+
+    /**
+     * Defines the other LTI enrolment structures to append.
+     *
+     * @return backup_plugin_element
+     */
+    public function define_enrol_plugin_structure() {
+        // Get the parent we will be adding these elements to.
+        $plugin = $this->get_plugin_element();
+
+        // Define our elements.
+        $tool = new backup_nested_element('tool', array('id'), array(
+            'enrolid', 'contextid', 'institution', 'lang', 'timezone', 'maxenrolled', 'maildisplay', 'city',
+            'country', 'gradesync', 'gradesynccompletion', 'membersync', 'membersyncmode',  'roleinstructor',
+            'rolelearner', 'secret', 'timecreated', 'timemodified'));
+
+        $users = new backup_nested_element('users');
+
+        $user = new backup_nested_element('user', array('id'), array(
+            'userid', 'toolid', 'serviceurl', 'sourceid', 'consumerkey', 'consumersecret', 'membershipurl',
+            'membershipsid'));
+
+        // Build elements hierarchy.
+        $plugin->add_child($tool);
+        $tool->add_child($users);
+        $users->add_child($user);
+
+        // Set sources to populate the data.
+        $tool->set_source_table('enrol_lti_tools',
+            array('enrolid' => backup::VAR_PARENTID));
+
+        // Users are only added only if users included.
+        if ($this->task->get_setting_value('users')) {
+            $user->set_source_table('enrol_lti_users', array('toolid' => backup::VAR_PARENTID));
+        }
+    }
+}
diff --git a/enrol/lti/backup/moodle2/restore_enrol_lti_plugin.class.php b/enrol/lti/backup/moodle2/restore_enrol_lti_plugin.class.php
new file mode 100644 (file)
index 0000000..1225223
--- /dev/null
@@ -0,0 +1,115 @@
+<?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/>.
+
+/**
+ * Defines the restore_enrol_lti_plugin class.
+ *
+ * @package   enrol_lti
+ * @copyright 2016 Mark Nelson <markn@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Define all the restore steps.
+ *
+ * @package   enrol_lti
+ * @copyright 2016 Mark Nelson <markn@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class restore_enrol_lti_plugin extends restore_enrol_plugin {
+
+    /**
+     * @var array $tools Stores the IDs of the newly created tools.
+     */
+    protected $tools = array();
+
+    /**
+     * Declares the enrol LTI XML paths attached to the enrol element
+     *
+     * @return array of {@link restore_path_element}
+     */
+    protected function define_enrol_plugin_structure() {
+
+        $paths = array();
+        $paths[] = new restore_path_element('enrol_lti_tool', $this->connectionpoint->get_path() . '/tool');
+        $paths[] = new restore_path_element('enrol_lti_users', $this->connectionpoint->get_path() . '/tool/users/user');
+
+        return $paths;
+    }
+
+    /**
+     * Processes LTI tools element data
+     *
+     * @param array|stdClass $data
+     */
+    public function process_enrol_lti_tool($data) {
+        global $DB;
+
+        $data = (object) $data;
+
+        // Store the old id.
+        $oldid = $data->id;
+
+        // Change the values before we insert it.
+        $data->timecreated = time();
+        $data->timemodified = $data->timecreated;
+
+        // Now we can insert the new record.
+        $data->id = $DB->insert_record('enrol_lti_tools', $data);
+
+        // Add the array of tools we need to process later.
+        $this->tools[$data->id] = $data;
+
+        // Set up the mapping.
+        $this->set_mapping('enrol_lti_tool', $oldid, $data->id);
+    }
+
+    /**
+     * Processes LTI users element data
+     *
+     * @param array|stdClass $data The data to insert as a comment
+     */
+    public function process_enrol_lti_users($data) {
+        global $DB;
+
+        $data = (object) $data;
+
+        $data->userid = $this->get_mappingid('user', $data->userid);
+        $data->toolid = $this->get_mappingid('enrol_lti_tool', $data->toolid);
+        $data->timecreated = time();
+
+        $DB->insert_record('enrol_lti_users', $data);
+    }
+
+    /**
+     * This function is executed after all the tasks in the plan have been finished.
+     * This must be done here because the activities have not been restored yet.
+     */
+    public function after_restore_enrol() {
+        global $DB;
+
+        // Need to go through and change the values.
+        foreach ($this->tools as $tool) {
+            $updatetool = new stdClass();
+            $updatetool->id = $tool->id;
+            $updatetool->enrolid = $this->get_mappingid('enrol', $tool->enrolid);
+            $updatetool->contextid = $this->get_mappingid('context', $tool->contextid);
+            $DB->update_record('enrol_lti_tools', $updatetool);
+        }
+    }
+}
diff --git a/enrol/lti/classes/helper.php b/enrol/lti/classes/helper.php
new file mode 100644 (file)
index 0000000..f6a006a
--- /dev/null
@@ -0,0 +1,378 @@
+<?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/>.
+
+/**
+ * LTI enrolment plugin helper.
+ *
+ * @package enrol_lti
+ * @copyright 2016 Mark Nelson <markn@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace enrol_lti;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * LTI enrolment plugin helper class.
+ *
+ * @package enrol_lti
+ * @copyright 2016 Mark Nelson <markn@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class helper {
+    /*
+     * The value used when we want to enrol new members and unenrol old ones.
+     */
+    const MEMBER_SYNC_ENROL_AND_UNENROL = 1;
+
+    /*
+     * The value used when we want to enrol new members only.
+     */
+    const MEMBER_SYNC_ENROL_NEW = 2;
+
+    /*
+     * The value used when we want to unenrol missing users.
+     */
+    const MEMBER_SYNC_UNENROL_MISSING = 3;
+
+    /**
+     * Code for when an enrolment was successful.
+     */
+    const ENROLMENT_SUCCESSFUL = true;
+
+    /**
+     * Error code for enrolment when max enrolled reached.
+     */
+    const ENROLMENT_MAX_ENROLLED = 'maxenrolledreached';
+
+    /**
+     * Error code for enrolment has not started.
+     */
+    const ENROLMENT_NOT_STARTED = 'enrolmentnotstarted';
+
+    /**
+     * Error code for enrolment when enrolment has finished.
+     */
+    const ENROLMENT_FINISHED = 'enrolmentfinished';
+
+    /**
+     * Error code for when an image file fails to upload.
+     */
+    const PROFILE_IMAGE_UPDATE_SUCCESSFUL = true;
+
+    /**
+     * Error code for when an image file fails to upload.
+     */
+    const PROFILE_IMAGE_UPDATE_FAILED = 'profileimagefailed';
+
+    /**
+     * Creates a unique username.
+     *
+     * @param string $consumerkey Consumer key
+     * @param string $ltiuserid External tool user id
+     * @return string The new username
+     */
+    public static function create_username($consumerkey, $ltiuserid) {
+        if (!empty($ltiuserid) && !empty($consumerkey)) {
+            $userkey = $consumerkey . ':' . $ltiuserid;
+        } else {
+            $userkey = false;
+        }
+
+        return 'enrol_lti' . sha1($consumerkey . '::' . $userkey);
+    }
+
+    /**
+     * Adds default values for the user object based on the tool provided.
+     *
+     * @param \stdClass $tool
+     * @param \stdClass $user
+     * @return \stdClass The $user class with added default values
+     */
+    public static function assign_user_tool_data($tool, $user) {
+        global $CFG;
+
+        $user->city = (!empty($tool->city)) ? $tool->city : "";
+        $user->country = (!empty($tool->country)) ? $tool->country : "";
+        $user->institution = (!empty($tool->institution)) ? $tool->institution : "";
+        $user->timezone = (!empty($tool->timezone)) ? $tool->timezone : "";
+        if (isset($tool->maildisplay)) {
+            $user->maildisplay = $tool->maildisplay;
+        } else if (isset($CFG->defaultpreference_maildisplay)) {
+            $user->maildisplay = $CFG->defaultpreference_maildisplay;
+        } else {
+            $user->maildisplay = 2;
+        }
+        $user->mnethostid = $CFG->mnet_localhost_id;
+        $user->confirmed = 1;
+        $user->lang = $tool->lang;
+
+        return $user;
+    }
+
+    /**
+     * Compares two users.
+     *
+     * @param \stdClass $newuser The new user
+     * @param \stdClass $olduser The old user
+     * @return bool True if both users are the same
+     */
+    public static function user_match($newuser, $olduser) {
+        if ($newuser->firstname != $olduser->firstname) {
+            return false;
+        }
+        if ($newuser->lastname != $olduser->lastname) {
+            return false;
+        }
+        if ($newuser->email != $olduser->email) {
+            return false;
+        }
+        if ($newuser->city != $olduser->city) {
+            return false;
+        }
+        if ($newuser->country != $olduser->country) {
+            return false;
+        }
+        if ($newuser->institution != $olduser->institution) {
+            return false;
+        }
+        if ($newuser->timezone != $olduser->timezone) {
+            return false;
+        }
+        if ($newuser->maildisplay != $olduser->maildisplay) {
+            return false;
+        }
+        if ($newuser->mnethostid != $olduser->mnethostid) {
+            return false;
+        }
+        if ($newuser->confirmed != $olduser->confirmed) {
+            return false;
+        }
+        if ($newuser->lang != $olduser->lang) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Updates the users profile image.
+     *
+     * @param int $userid the id of the user
+     * @param string $url the url of the image
+     * @return bool|string true if successful, else a string explaining why it failed
+     */
+    public static function update_user_profile_image($userid, $url) {
+        global $CFG, $DB;
+
+        require_once($CFG->libdir . '/filelib.php');
+        require_once($CFG->libdir . '/gdlib.php');
+
+        $fs = get_file_storage();
+
+        $context = \context_user::instance($userid, MUST_EXIST);
+        $fs->delete_area_files($context->id, 'user', 'newicon');
+
+        $filerecord = array(
+            'contextid' => $context->id,
+            'component' => 'user',
+            'filearea' => 'newicon',
+            'itemid' => 0,
+            'filepath' => '/'
+        );
+
+        $urlparams = array(
+            'calctimeout' => false,
+            'timeout' => 5,
+            'skipcertverify' => true,
+            'connecttimeout' => 5
+        );
+
+        if (!$iconfiles = $fs->create_file_from_url($filerecord, $url, $urlparams)) {
+            return self::PROFILE_IMAGE_UPDATE_FAILED;
+        }
+
+        $iconfile = $fs->get_area_files($context->id, 'user', 'newicon', false, 'itemid', false);
+
+        // There should only be one.
+        $iconfile = reset($iconfile);
+
+        // Something went wrong while creating temp file - remove the uploaded file.
+        if (!$iconfile = $iconfile->copy_content_to_temp()) {
+            $fs->delete_area_files($context->id, 'user', 'newicon');
+            return self::PROFILE_IMAGE_UPDATE_FAILED;
+        }
+
+        // Copy file to temporary location and the send it for processing icon.
+        $newpicture = (int) process_new_icon($context, 'user', 'icon', 0, $iconfile);
+        // Delete temporary file.
+        @unlink($iconfile);
+        // Remove uploaded file.
+        $fs->delete_area_files($context->id, 'user', 'newicon');
+        // Set the user's picture.
+        $DB->set_field('user', 'picture', $newpicture, array('id' => $userid));
+        return self::PROFILE_IMAGE_UPDATE_SUCCESSFUL;
+    }
+
+    /**
+     * Enrol a user in a course.
+     *
+     * @param \stdclass $tool The tool object (retrieved using self::get_lti_tool() or self::get_lti_tools())
+     * @param int $userid The user id
+     * @return bool|string returns true if successful, else an error code
+     */
+    public static function enrol_user($tool, $userid) {
+        global $DB;
+
+        // Check if the user enrolment exists.
+        if (!$DB->record_exists('user_enrolments', array('enrolid' => $tool->enrolid, 'userid' => $userid))) {
+            // Check if the maximum enrolled limit has been met.
+            if ($tool->maxenrolled) {
+                if ($DB->count_records('user_enrolments', array('enrolid' => $tool->enrolid)) >= $tool->maxenrolled) {
+                    return self::ENROLMENT_MAX_ENROLLED;
+                }
+            }
+            // Check if the enrolment has not started.
+            if ($tool->enrolstartdate && time() < $tool->enrolstartdate) {
+                return self::ENROLMENT_NOT_STARTED;
+            }
+            // Check if the enrolment has finished.
+            if ($tool->enrolenddate && time() > $tool->enrolenddate) {
+                return self::ENROLMENT_FINISHED;
+            }
+
+            $timeend = 0;
+            if ($tool->enrolperiod) {
+                $timeend = time() + $tool->enrolperiod;
+            }
+
+            // Finally, enrol the user.
+            $instance = new \stdClass();
+            $instance->id = $tool->enrolid;
+            $instance->courseid = $tool->courseid;
+            $instance->enrol = 'lti';
+            $instance->status = $tool->status;
+            $ltienrol = enrol_get_plugin('lti');
+            $ltienrol->enrol_user($instance, $userid, null, time(), $timeend);
+        }
+
+        return self::ENROLMENT_SUCCESSFUL;
+    }
+
+    /**
+     * Returns the LTI tool.
+     *
+     * @param int $toolid
+     * @return \stdClass the tool
+     */
+    public static function get_lti_tool($toolid) {
+        global $DB;
+
+        $sql = "SELECT elt.*, e.name, e.courseid, e.status, e.enrolstartdate, e.enrolenddate, e.enrolperiod
+                  FROM {enrol_lti_tools} elt
+                  JOIN {enrol} e
+                    ON elt.enrolid = e.id
+                 WHERE elt.id = :tid";
+
+        return $DB->get_record_sql($sql, array('tid' => $toolid), MUST_EXIST);
+    }
+
+    /**
+     * Returns the LTI tools requested.
+     *
+     * @param array $params The list of SQL params (eg. array('columnname' => value, 'columnname2' => value)).
+     * @param int $limitfrom return a subset of records, starting at this point (optional).
+     * @param int $limitnum return a subset comprising this many records in total
+     * @return array of tools
+     */
+    public static function get_lti_tools($params = array(), $limitfrom = 0, $limitnum = 0) {
+        global $DB;
+
+        $sql = "SELECT elt.*, e.name, e.courseid, e.status, e.enrolstartdate, e.enrolenddate, e.enrolperiod
+                  FROM {enrol_lti_tools} elt
+                  JOIN {enrol} e
+                    ON elt.enrolid = e.id";
+        if ($params) {
+            $where = "WHERE";
+            foreach ($params as $colname => $value) {
+                $sql .= " $where $colname = :$colname";
+                $where = "AND";
+            }
+        }
+        $sql .= " ORDER BY elt.timecreated";
+
+        return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
+    }
+
+    /**
+     * Returns the number of LTI tools.
+     *
+     * @param array $params The list of SQL params (eg. array('columnname' => value, 'columnname2' => value)).
+     * @return int The number of tools
+     */
+    public static function count_lti_tools($params = array()) {
+        global $DB;
+
+        $sql = "SELECT COUNT(*)
+                  FROM {enrol_lti_tools} elt
+                  JOIN {enrol} e
+                    ON elt.enrolid = e.id";
+        if ($params) {
+            $where = "WHERE";
+            foreach ($params as $colname => $value) {
+                $sql .= " $where $colname = :$colname";
+                $where = "AND";
+            }
+        }
+
+        return $DB->count_records_sql($sql, $params);
+    }
+
+    /**
+     * Create a IMS POX body request for sync grades.
+     *
+     * @param string $source Sourceid required for the request
+     * @param float $grade User final grade
+     * @return string
+     */
+    public static function create_service_body($source, $grade) {
+        return '<?xml version="1.0" encoding="UTF-8"?>
+            <imsx_POXEnvelopeRequest xmlns="http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0">
+              <imsx_POXHeader>
+                <imsx_POXRequestHeaderInfo>
+                  <imsx_version>V1.0</imsx_version>
+                  <imsx_messageIdentifier>' . (time()) . '</imsx_messageIdentifier>
+                </imsx_POXRequestHeaderInfo>
+              </imsx_POXHeader>
+              <imsx_POXBody>
+                <replaceResultRequest>
+                  <resultRecord>
+                    <sourcedGUID>
+                      <sourcedId>' . $source . '</sourcedId>
+                    </sourcedGUID>
+                    <result>
+                      <resultScore>
+                        <language>en-us</language>
+                        <textString>' . $grade . '</textString>
+                      </resultScore>
+                    </result>
+                  </resultRecord>
+                </replaceResultRequest>
+              </imsx_POXBody>
+            </imsx_POXEnvelopeRequest>';
+    }
+}
diff --git a/enrol/lti/classes/manage_table.php b/enrol/lti/classes/manage_table.php
new file mode 100644 (file)
index 0000000..d334483
--- /dev/null
@@ -0,0 +1,218 @@
+<?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/>.
+
+/**
+ * Displays enrolment LTI instances.
+ *
+ * @package    enrol_lti
+ * @copyright  2016 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace enrol_lti;
+
+defined('MOODLE_INTERNAL') || die;
+
+global $CFG;
+
+require_once($CFG->libdir . '/tablelib.php');
+
+/**
+ * Handles displaying enrolment LTI instances.
+ *
+ * @package    enrol_lti
+ * @copyright  2016 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class manage_table extends \table_sql {
+
+    /**
+     * @var \enrol_plugin $ltiplugin
+     */
+    protected $ltiplugin;
+
+    /**
+     * @var bool $ltienabled
+     */
+    protected $ltienabled;
+
+    /**
+     * @var bool $canconfig
+     */
+    protected $canconfig;
+
+    /**
+     * @var int $courseid The course id.
+     */
+    protected $courseid;
+
+    /**
+     * Sets up the table.
+     *
+     * @param string $courseid The id of the course.
+     */
+    public function __construct($courseid) {
+        parent::__construct('enrol_lti_manage_table');
+
+        $this->define_columns(array(
+            'name',
+            'url',
+            'secret',
+            'edit'
+        ));
+        $this->define_headers(array(
+            get_string('name'),
+            get_string('url'),
+            get_string('secret', 'enrol_lti'),
+            get_string('edit')
+        ));
+        $this->collapsible(false);
+        $this->sortable(false);
+
+        // Set the variables we need access to.
+        $this->ltiplugin = enrol_get_plugin('lti');
+        $this->ltienabled = enrol_is_enabled('lti');
+        $this->canconfig = has_capability('moodle/course:enrolconfig', \context_course::instance($courseid));
+        $this->courseid = $courseid;
+    }
+
+    /**
+     * Generate the name column.
+     *
+     * @param \stdClass $tool event data.
+     * @return string
+     */
+    public function col_name($tool) {
+        if (empty($tool->name)) {
+            $toolcontext = \context::instance_by_id($tool->contextid);
+            $name = $toolcontext->get_context_name();
+        } else {
+            $name = $tool->name;
+        };
+
+        return $this->get_display_text($tool, $name);
+    }
+
+    /**
+     * Generate the URL column.
+     *
+     * @param \stdClass $tool event data.
+     * @return string
+     */
+    public function col_url($tool) {
+        $url = new \moodle_url('/enrol/lti/tool.php', array('id' => $tool->id));
+        return $this->get_display_text($tool, $url);
+    }
+
+    /**
+     * Generate the secret column.
+     *
+     * @param \stdClass $tool event data.
+     * @return string
+     */
+    public function col_secret($tool) {
+        return $this->get_display_text($tool, $tool->secret);
+    }
+
+
+    /**
+     * Generate the edit column.
+     *
+     * @param \stdClass $tool event data.
+     * @return string
+     */
+    public function col_edit($tool) {
+        global $OUTPUT;
+
+        $buttons = array();
+
+        $instance = new \stdClass();
+        $instance->id = $tool->enrolid;
+        $instance->courseid = $tool->courseid;
+        $instance->enrol = 'lti';
+        $instance->status = $tool->status;
+
+        $strdelete = get_string('delete');
+        $strenable = get_string('enable');
+        $strdisable = get_string('disable');
+
+        $url = new \moodle_url('/enrol/lti/index.php', array('sesskey' => sesskey(), 'courseid' => $this->courseid));
+
+        if ($this->ltiplugin->can_delete_instance($instance)) {
+            $aurl = new \moodle_url($url, array('action' => 'delete', 'instanceid' => $instance->id));
+            $buttons[] = $OUTPUT->action_icon($aurl, new \pix_icon('t/delete', $strdelete, 'core',
+                array('class' => 'iconsmall')));
+        }
+
+        if ($this->ltienabled && $this->ltiplugin->can_hide_show_instance($instance)) {
+            if ($instance->status == ENROL_INSTANCE_ENABLED) {
+                $aurl = new \moodle_url($url, array('action' => 'disable', 'instanceid' => $instance->id));
+                $buttons[] = $OUTPUT->action_icon($aurl, new \pix_icon('t/hide', $strdisable, 'core',
+                    array('class' => 'iconsmall')));
+            } else if ($instance->status == ENROL_INSTANCE_DISABLED) {
+                $aurl = new \moodle_url($url, array('action' => 'enable', 'instanceid' => $instance->id));
+                $buttons[] = $OUTPUT->action_icon($aurl, new \pix_icon('t/show', $strenable, 'core',
+                    array('class' => 'iconsmall')));
+            }
+        }
+
+        if ($this->ltienabled && $this->canconfig) {
+            $linkparams = array(
+                'courseid' => $instance->courseid,
+                'id' => $instance->id, 'type' => $instance->enrol,
+                'returnurl' => new \moodle_url('/enrol/lti/index.php', array('courseid' => $this->courseid))
+            );
+            $editlink = new \moodle_url("/enrol/editinstance.php", $linkparams);
+            $buttons[] = $OUTPUT->action_icon($editlink, new \pix_icon('t/edit', get_string('edit'), 'core',
+                array('class' => 'iconsmall')));
+        }
+
+        return implode(' ', $buttons);
+    }
+
+    /**
+     * Query the reader. Store results in the object for use by build_table.
+     *
+     * @param int $pagesize size of page for paginated displayed table.
+     * @param bool $useinitialsbar do you want to use the initials bar.
+     */
+    public function query_db($pagesize, $useinitialsbar = true) {
+        $total = \enrol_lti\helper::count_lti_tools(array('courseid' => $this->courseid));
+        $this->pagesize($pagesize, $total);
+        $tools = \enrol_lti\helper::get_lti_tools(array('courseid' => $this->courseid), $this->get_page_start(),
+            $this->get_page_size());
+        $this->rawdata = $tools;
+        // Set initial bars.
+        if ($useinitialsbar) {
+            $this->initialbars($total > $pagesize);
+        }
+    }
+
+    /**
+     * Returns text to display in the columns.
+     *
+     * @param \stdClass $tool the tool
+     * @param string $text the text to alter
+     * @return string
+     */
+    protected function get_display_text($tool, $text) {
+        if ($tool->status != ENROL_INSTANCE_ENABLED) {
+            return \html_writer::tag('span', $text, array('class' => 'dimmed_text'));
+        }
+
+        return $text;
+    }
+}
diff --git a/enrol/lti/classes/task/sync_grades.php b/enrol/lti/classes/task/sync_grades.php
new file mode 100644 (file)
index 0000000..944de37
--- /dev/null
@@ -0,0 +1,192 @@
+<?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/>.
+
+/**
+ * Handles synchronising grades for the enrolment LTI.
+ *
+ * @package    enrol_lti
+ * @copyright  2016 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace enrol_lti\task;
+
+/**
+ * Task for synchronising grades for the enrolment LTI.
+ *
+ * @package    enrol_lti
+ * @copyright  2016 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class sync_grades extends \core\task\scheduled_task {
+
+    /**
+     * Get a descriptive name for this task.
+     *
+     * @return string
+     */
+    public function get_name() {
+        return get_string('tasksyncgrades', 'enrol_lti');
+    }
+
+    /**
+     * Performs the synchronisation of grades.
+     *
+     * @return bool|void
+     */
+    public function execute() {
+        global $DB, $CFG;
+
+        require_once($CFG->dirroot . '/enrol/lti/ims-blti/OAuth.php');
+        require_once($CFG->dirroot . '/enrol/lti/ims-blti/OAuthBody.php');
+        require_once($CFG->dirroot . '/lib/completionlib.php');
+        require_once($CFG->libdir . '/gradelib.php');
+        require_once($CFG->dirroot . '/grade/querylib.php');
+
+        // Check if the authentication plugin is disabled.
+        if (!is_enabled_auth('lti')) {
+            mtrace('Skipping task - ' . get_string('pluginnotenabled', 'auth', get_string('pluginname', 'auth_lti')));
+            return true;
+        }
+
+        // Check if the enrolment plugin is disabled - isn't really necessary as the task should not run if
+        // the plugin is disabled, but there is no harm in making sure core hasn't done something wrong.
+        if (!enrol_is_enabled('lti')) {
+            mtrace('Skipping task - ' . get_string('enrolisdisabled', 'enrol_lti'));
+            return true;
+        }
+
+        // Get all the enabled tools.
+        if ($tools = \enrol_lti\helper::get_lti_tools(array('status' => ENROL_INSTANCE_ENABLED, 'gradesync' => 1))) {
+            foreach ($tools as $tool) {
+                mtrace("Starting - Grade sync for shared tool '$tool->id' for the course '$tool->courseid'.");
+
+                // Variables to keep track of information to display later.
+                $usercount = 0;
+                $sendcount = 0;
+
+                // We check for all the users - users can access the same tool from different consumers.
+                if ($ltiusers = $DB->get_records('enrol_lti_users', array('toolid' => $tool->id), 'lastaccess DESC')) {
+                    $completion = new \completion_info(get_course($tool->courseid));
+                    foreach ($ltiusers as $ltiuser) {
+                        $mtracecontent = "for the user '$ltiuser->userid' in the tool '$tool->id' for the course " .
+                            "'$tool->courseid'";
+
+                        $usercount = $usercount + 1;
+
+                        // Check if we do not have a serviceurl - this can happen if the sync process has an unexpected error.
+                        if (empty($ltiuser->serviceurl)) {
+                            mtrace("Skipping - Empty serviceurl $mtracecontent.");
+                            continue;
+                        }
+
+                        // Check if we do not have a sourceid - this can happen if the sync process has an unexpected error.
+                        if (empty($ltiuser->sourceid)) {
+                            mtrace("Skipping - Empty sourceid $mtracecontent.");
+                            continue;
+                        }
+
+                        // Need a valid context to continue.
+                        if (!$context = \context::instance_by_id($tool->contextid)) {
+                            mtrace("Failed - Invalid contextid '$tool->contextid' for the tool '$tool->id'.");
+                            continue;
+                        }
+
+                        // Ok, let's get the grade.
+                        $grade = false;
+                        if ($context->contextlevel == CONTEXT_COURSE) {
+                            // Check if the user did not completed the course when it was required.
+                            if ($tool->gradesynccompletion && !$completion->is_course_complete($ltiuser->userid)) {
+                                mtrace("Skipping - Course not completed $mtracecontent.");
+                                continue;
+                            }
+
+                            // Get the grade.
+                            if ($grade = grade_get_course_grade($ltiuser->userid, $tool->courseid)) {
+                                $grademax = floatval($grade->item->grademax);
+                                $grade = $grade->grade;
+                            }
+                        } else if ($context->contextlevel == CONTEXT_MODULE) {
+                            $cm = get_coursemodule_from_id(false, $context->instanceid, 0, false, MUST_EXIST);
+
+                            if ($tool->gradesynccompletion) {
+                                $data = $completion->get_data($cm, false, $ltiuser->userid);
+                                if ($data->completionstate != COMPLETION_COMPLETE_PASS &&
+                                    $data->completionstate != COMPLETION_COMPLETE) {
+                                    mtrace("Skipping - Activity not completed $mtracecontent.");
+                                    continue;
+                                }
+                            }
+
+                            $grades = grade_get_grades($cm->course, 'mod', $cm->modname, $cm->instance, $ltiuser->userid);
+                            if (!empty($grades->items[0]->grades)) {
+                                $grade = reset($grades->items[0]->grades);
+                                if (!empty($grade->item)) {
+                                    $grademax = floatval($grade->item->grademax);
+                                } else {
+                                    $grademax = floatval($grades->items[0]->grademax);
+                                }
+                                $grade = $grade->grade;
+                            }
+                        }
+
+                        if ($grade === false || $grade === null || strlen($grade) < 1) {
+                            mtrace("Skipping - Invalid grade $mtracecontent.");
+                            continue;
+                        }
+
+                        // No need to be dividing by zero.
+                        if (empty($grademax)) {
+                            mtrace("Skipping - Invalid grade $mtracecontent.");
+                            continue;
+                        }
+
+                        // This can happen if the sync process has an unexpected error.
+                        if ($grade == $ltiuser->lastgrade) {
+                            mtrace("Not sent - The grade $mtracecontent was not sent as the grades are the same.");
+                            continue;
+                        }
+
+                        // Sync with the external system.
+                        $floatgrade = $grade / $grademax;
+                        $body = \enrol_lti\helper::create_service_body($ltiuser->sourceid, $floatgrade);
+
+                        try {
+                            $response = sendOAuthBodyPOST('POST', $ltiuser->serviceurl,
+                                $ltiuser->consumerkey, $ltiuser->consumersecret, 'application/xml', $body);
+                        } catch (\Exception $e) {
+                            mtrace("Failed - The grade '$floatgrade' $mtracecontent failed to send.");
+                            mtrace($e->getMessage());
+                            continue;
+                        }
+
+                        if (strpos(strtolower($response), 'success') !== false) {
+                            $DB->set_field('enrol_lti_users', 'lastgrade', intval($grade), array('id' => $ltiuser->id));
+                            mtrace("Success - The grade '$floatgrade' $mtracecontent was sent.");
+                            $sendcount = $sendcount + 1;
+                        } else {
+                            mtrace("Failed - The grade '$floatgrade' $mtracecontent failed to send.");
+                        }
+
+                    }
+                }
+                mtrace("Completed - Synced grades for tool '$tool->id' in the course '$tool->courseid'. " .
+                    "Processed $usercount users; sent $sendcount grades.");
+                mtrace("");
+            }
+        }
+    }
+}
diff --git a/enrol/lti/classes/task/sync_members.php b/enrol/lti/classes/task/sync_members.php
new file mode 100644 (file)
index 0000000..a1c3872
--- /dev/null
@@ -0,0 +1,245 @@
+<?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/>.
+
+/**
+ * Handles synchronising members using the enrolment LTI.
+ *
+ * @package    enrol_lti
+ * @copyright  2016 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace enrol_lti\task;
+
+/**
+ * Task for synchronising members using the enrolment LTI.
+ *
+ * @package    enrol_lti
+ * @copyright  2016 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class sync_members extends \core\task\scheduled_task {
+
+    /**
+     * The LTI message type.
+     */
+    const LTI_MESSAGE_TYPE = 'basic-lis-readmembershipsforcontext';
+
+    /**
+     * The LTI version.
+     */
+    const LTI_VERSION = 'LTI-1p0';
+
+    /**
+     * Get a descriptive name for this task.
+     *
+     * @return string
+     */
+    public function get_name() {
+        return get_string('tasksyncmembers', 'enrol_lti');
+    }
+
+    /**
+     * Performs the synchronisation of members.
+     *
+     * @return bool|void
+     */
+    public function execute() {
+        global $CFG, $DB;
+
+        require_once($CFG->dirroot . '/enrol/lti/ims-blti/OAuth.php');
+        require_once($CFG->dirroot . '/enrol/lti/ims-blti/OAuthBody.php');
+
+        // Check if the authentication plugin is disabled.
+        if (!is_enabled_auth('lti')) {
+            mtrace('Skipping task - ' . get_string('pluginnotenabled', 'auth', get_string('pluginname', 'auth_lti')));
+            return true;
+        }
+
+        // Check if the enrolment plugin is disabled - isn't really necessary as the task should not run if
+        // the plugin is disabled, but there is no harm in making sure core hasn't done something wrong.
+        if (!enrol_is_enabled('lti')) {
+            mtrace('Skipping task - ' . get_string('enrolisdisabled', 'enrol_lti'));
+            return true;
+        }
+
+        // Get all the enabled tools.
+        if ($tools = \enrol_lti\helper::get_lti_tools(array('status' => ENROL_INSTANCE_ENABLED, 'membersync' => 1))) {
+            $ltiplugin = enrol_get_plugin('lti');
+            $consumers = array();
+            $currentusers = array();
+            $userphotos = array();
+            foreach ($tools as $tool) {
+                mtrace("Starting - Member sync for shared tool '$tool->id' for the course '$tool->courseid'.");
+
+                // Variables to keep track of information to display later.
+                $usercount = 0;
+                $enrolcount = 0;
+                $unenrolcount = 0;
+
+                // We check for all the users - users can access the same tool from different consumers.
+                if ($ltiusers = $DB->get_records('enrol_lti_users', array('toolid' => $tool->id), 'lastaccess DESC')) {
+                    foreach ($ltiusers as $ltiuser) {
+                        $mtracecontent = "for the user '$ltiuser->userid' in the tool '$tool->id' for the course " .
+                            "'$tool->courseid'";
+                        $usercount++;
+
+                        // Check if we do not have a membershipsurl - this can happen if the sync process has an unexpected error.
+                        if (!$ltiuser->membershipsurl) {
+                            mtrace("Skipping - Empty membershipsurl $mtracecontent.");
+                            continue;
+                        }
+
+                        // Check if we do not have a membershipsid - this can happen if the sync process has an unexpected error.
+                        if (!$ltiuser->membershipsid) {
+                            mtrace("Skipping - Empty membershipsid $mtracecontent.");
+                            continue;
+                        }
+
+                        $consumer = sha1($ltiuser->membershipsurl . ':' . $ltiuser->membershipsid . ':' .
+                            $ltiuser->consumerkey . ':' . $ltiuser->consumersecret);
+                        if (in_array($consumer, $consumers)) {
+                            // We have already synchronised with this consumer.
+                            continue;
+                        }
+
+                        $consumers[] = $consumer;
+
+                        $params = array(
+                            'lti_message_type' => self::LTI_MESSAGE_TYPE,
+                            'id' => $ltiuser->membershipsid,
+                            'lti_version' => self::LTI_VERSION
+                        );
+
+                        mtrace("Calling memberships url '$ltiuser->membershipsurl' with body '" .
+                            json_encode($params) . "'");
+
+                        try {
+                            $response = sendOAuthParamsPOST('POST', $ltiuser->membershipsurl, $ltiuser->consumerkey,
+                                $ltiuser->consumersecret, 'application/x-www-form-urlencoded', $params);
+                        } catch (\Exception $e) {
+                            mtrace("Skipping - No response received $mtracecontent from '$ltiuser->membershipsurl'");
+                            mtrace($e->getMessage());
+                            continue;
+                        }
+
+                        // Check the response from the consumer.
+                        $data = new \SimpleXMLElement($response);
+
+                        // Check if we did not receive a valid response.
+                        if (empty($data->statusinfo)) {
+                            mtrace("Skipping - Bad response received $mtracecontent from '$ltiuser->membershipsurl'");
+                            mtrace('Skipping - Error parsing the XML received \'' . substr($response, 0, 125) .
+                                '\' ... (Displaying only 125 chars)');
+                            continue;
+                        }
+
+                        // Check if we did not receive a valid response.
+                        if (strpos(strtolower($data->statusinfo->codemajor), 'success') === false) {
+                            mtrace('Skipping - Error received from the remote system: ' . $data->statusinfo->codemajor
+                                . ' ' . $data->statusinfo->severity . ' ' . $data->statusinfo->codeminor);
+                            continue;
+                        }
+
+                        $members = $data->memberships->member;
+                        mtrace(count($members) . ' members received.');
+                        foreach ($members as $member) {
+                            // Set the user data.
+                            $user = new \stdClass();
+                            $user->username = \enrol_lti\helper::create_username($ltiuser->consumerkey, $member->user_id);
+                            $user->firstname = \core_user::clean_field($member->person_name_given, 'firstname');
+                            $user->lastname = \core_user::clean_field($member->person_name_family, 'lastname');
+                            $user->email = \core_user::clean_field($member->person_contact_email_primary, 'email');
+
+                            // Get the user data from the LTI consumer.
+                            $user = \enrol_lti\helper::assign_user_tool_data($tool, $user);
+
+                            if (!$dbuser = $DB->get_record('user', array('username' => $user->username, 'deleted' => 0))) {
+                                if ($tool->membersyncmode == \enrol_lti\helper::MEMBER_SYNC_ENROL_AND_UNENROL ||
+                                    $tool->membersyncmode == \enrol_lti\helper::MEMBER_SYNC_ENROL_NEW) {
+                                    // If the email was stripped/not set then fill it with a default one. This
+                                    // stops the user from being redirected to edit their profile page.
+                                    if (empty($user->email)) {
+                                        $user->email = $user->username .  "@example.com";
+                                    }
+
+                                    $user->auth = 'lti';
+                                    $user->id = user_create_user($user);
+
+                                    // Add the information to the necessary arrays.
+                                    $currentusers[] = $user->id;
+                                    $userphotos[$user->id] = $member->user_image;
+                                }
+                            } else {
+                                // If email is empty remove it, so we don't update the user with an empty email.
+                                if (empty($user->email)) {
+                                    unset($user->email);
+                                }
+
+                                $user->id = $dbuser->id;
+                                user_update_user($user);
+
+                                // Add the information to the necessary arrays.
+                                $currentusers[] = $user->id;
+                                $userphotos[$user->id] = $member->user_image;
+                            }
+                            if ($tool->membersyncmode == \enrol_lti\helper::MEMBER_SYNC_ENROL_AND_UNENROL ||
+                                $tool->membersyncmode == \enrol_lti\helper::MEMBER_SYNC_ENROL_NEW) {
+                                // Enrol the user in the course.
+                                \enrol_lti\helper::enrol_user($tool, $user->id);
+                            }
+                        }
+                    }
+                    // Now we check if we have to unenrol users who were not listed.
+                    if ($tool->membersyncmode == \enrol_lti\helper::MEMBER_SYNC_ENROL_AND_UNENROL ||
+                        $tool->membersyncmode == \enrol_lti\helper::MEMBER_SYNC_UNENROL_MISSING) {
+                        // Go through the users and check if any were never listed, if so, remove them.
+                        foreach ($ltiusers as $ltiuser) {
+                            if (!in_array($ltiuser->userid, $currentusers)) {
+                                $instance = new \stdClass();
+                                $instance->id = $tool->enrolid;
+                                $instance->courseid = $tool->courseid;
+                                $instance->enrol = 'lti';
+                                $ltiplugin->unenrol_user($instance, $ltiuser->id);
+                            }
+                        }
+                    }
+                }
+                mtrace("Completed - Synced members for tool '$tool->id' in the course '$tool->courseid'. " .
+                     "Processed $usercount users; enrolled $enrolcount members; unenrolled $unenrolcount members.");
+                mtrace("");
+            }
+
+            // Sync the user profile photos.
+            mtrace("Started - Syncing user profile images.");
+            $counter = 0;
+            if (!empty($userphotos)) {
+                foreach ($userphotos as $userid => $url) {
+                    if ($url) {
+                        $result = \enrol_lti\helper::update_user_profile_image($userid, $url);
+                        if ($result === \enrol_lti\helper::PROFILE_IMAGE_UPDATE_SUCCESSFUL) {
+                            $counter++;
+                            mtrace("Profile image succesfully downloaded and created for user '$userid' from $url.");
+                        } else {
+                            mtrace($result);
+                        }
+                    }
+                }
+            }
+            mtrace("Completed - Synced $counter profile images.");
+        }
+    }
+}
diff --git a/enrol/lti/db/access.php b/enrol/lti/db/access.php
new file mode 100644 (file)
index 0000000..b1a16e9
--- /dev/null
@@ -0,0 +1,47 @@
+<?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/>.
+
+/**
+ * Capabilities for LTI enrolment plugin.
+ *
+ * @package enrol_lti
+ * @copyright 2016 Mark Nelson <markn@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$capabilities = array(
+
+    /* Add, edit or remove lti enrol instance. */
+    'enrol/lti:config' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_COURSE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW,
+        )
+    ),
+
+    'enrol/lti:unenrol' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_COURSE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW,
+        )
+    ),
+);
diff --git a/enrol/lti/db/install.xml b/enrol/lti/db/install.xml
new file mode 100644 (file)
index 0000000..9a22184
--- /dev/null
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<XMLDB PATH="enrol/lti/db" VERSION="20160322" COMMENT="XMLDB file for Moodle enrol/lti"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
+>
+  <TABLES>
+    <TABLE NAME="enrol_lti_tools" COMMENT="List of tools provided to the remote system">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="enrolid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="contextid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="institution" TYPE="char" LENGTH="40" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="lang" TYPE="char" LENGTH="30" NOTNULL="true" DEFAULT="en" SEQUENCE="false"/>
+        <FIELD NAME="timezone" TYPE="char" LENGTH="100" NOTNULL="true" DEFAULT="99" SEQUENCE="false"/>
+        <FIELD NAME="maxenrolled" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="maildisplay" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="2" SEQUENCE="false"/>
+        <FIELD NAME="city" TYPE="char" LENGTH="120" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="country" TYPE="char" LENGTH="2" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="gradesync" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="gradesynccompletion" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="membersync" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="membersyncmode" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="roleinstructor" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="rolelearner" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="secret" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="enrolid" TYPE="foreign" FIELDS="enrolid" REFTABLE="enrol" REFFIELDS="id"/>
+        <KEY NAME="contextid" TYPE="foreign" FIELDS="contextid" REFTABLE="context" REFFIELDS="id"/>
+      </KEYS>
+    </TABLE>
+    <TABLE NAME="enrol_lti_users" COMMENT="User access log and gradeback data">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="toolid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="serviceurl" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="sourceid" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="consumerkey" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="consumersecret" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="membershipsurl" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="membershipsid" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="lastgrade" TYPE="number" LENGTH="10" NOTNULL="false" SEQUENCE="false" DECIMALS="5" COMMENT="The last grade that was sent"/>
+        <FIELD NAME="lastaccess" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="The time the user last accessed"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="The time the user was created"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
+        <KEY NAME="toolid" TYPE="foreign" FIELDS="toolid" REFTABLE="enrol_lti_tools" REFFIELDS="id"/>
+      </KEYS>
+    </TABLE>
+  </TABLES>
+</XMLDB>
diff --git a/enrol/lti/db/tasks.php b/enrol/lti/db/tasks.php
new file mode 100644 (file)
index 0000000..9c8b9f2
--- /dev/null
@@ -0,0 +1,44 @@
+<?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/>.
+
+/**
+ * Enrol LTI tasks.
+ *
+ * @package    enrol_lti
+ * @copyright  2016 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$tasks = array(
+    array(
+        'classname' => 'enrol_lti\task\sync_grades',
+        'blocking' => 0,
+        'minute' => '*/30',
+        'hour' => '*',
+        'day' => '*',
+        'dayofweek' => '*',
+        'month' => '*'
+    ),
+    array(
+        'classname' => 'enrol_lti\task\sync_members',
+        'blocking' => 0,
+        'minute' => '*/30',
+        'hour' => '*',
+        'day' => '*',
+        'dayofweek' => '*',
+        'month' => '*'
+    ),
+);
diff --git a/enrol/lti/ims-blti/LICENSE.txt b/enrol/lti/ims-blti/LICENSE.txt
new file mode 100644 (file)
index 0000000..89f0591
--- /dev/null
@@ -0,0 +1,22 @@
+The MIT License
+
+Copyright (c) 2007 Andy Smith
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
diff --git a/enrol/lti/ims-blti/OAuth.php b/enrol/lti/ims-blti/OAuth.php
new file mode 100644 (file)
index 0000000..3077787
--- /dev/null
@@ -0,0 +1,808 @@
+<?php
+// vim: foldmethod=marker
+
+$OAuth_last_computed_siguature = false;
+
+/* Generic exception class
+ */
+class OAuthException extends \Exception {
+  // pass
+}
+
+class OAuthConsumer {
+  public $key;
+  public $secret;
+
+  function __construct($key, $secret, $callback_url=NULL) {
+    $this->key = $key;
+    $this->secret = $secret;
+    $this->callback_url = $callback_url;
+  }
+
+  function __toString() {
+    return "OAuthConsumer[key=$this->key,secret=$this->secret]";
+  }
+}
+
+class OAuthToken {
+  // access tokens and request tokens
+  public $key;
+  public $secret;
+
+  /**
+   * key = the token
+   * secret = the token secret
+   */
+  function __construct($key, $secret) {
+    $this->key = $key;
+    $this->secret = $secret;
+  }
+
+  /**
+   * generates the basic string serialization of a token that a server
+   * would respond to request_token and access_token calls with
+   */
+  function to_string() {
+    return "oauth_token=" .
+           OAuthUtil::urlencode_rfc3986($this->key) .
+           "&oauth_token_secret=" .
+           OAuthUtil::urlencode_rfc3986($this->secret);
+  }
+
+  function __toString() {
+    return $this->to_string();
+  }
+}
+
+class OAuthSignatureMethod {
+  public function check_signature(&$request, $consumer, $token, $signature) {
+    $built = $this->build_signature($request, $consumer, $token);
+    return $built == $signature;
+  }
+}
+
+class OAuthSignatureMethod_HMAC_SHA1 extends OAuthSignatureMethod {
+  function get_name() {
+    return "HMAC-SHA1";
+  }
+
+  public function build_signature($request, $consumer, $token) {
+    global $OAuth_last_computed_signature;
+    $OAuth_last_computed_signature = false;
+
+    $base_string = $request->get_signature_base_string();
+    $request->base_string = $base_string;
+
+    $key_parts = array(
+      $consumer->secret,
+      ($token) ? $token->secret : ""
+    );
+
+    $key_parts = OAuthUtil::urlencode_rfc3986($key_parts);
+    $key = implode('&', $key_parts);
+
+    $computed_signature = base64_encode(hash_hmac('sha1', $base_string, $key, true));
+    $OAuth_last_computed_signature = $computed_signature;
+    return $computed_signature;
+  }
+
+}
+
+class OAuthSignatureMethod_PLAINTEXT extends OAuthSignatureMethod {
+  public function get_name() {
+    return "PLAINTEXT";
+  }
+
+  public function build_signature($request, $consumer, $token) {
+    $sig = array(
+      OAuthUtil::urlencode_rfc3986($consumer->secret)
+    );
+
+    if ($token) {
+      array_push($sig, OAuthUtil::urlencode_rfc3986($token->secret));
+    } else {
+      array_push($sig, '');
+    }
+
+    $raw = implode("&", $sig);
+    // for debug purposes
+    $request->base_string = $raw;
+
+    return OAuthUtil::urlencode_rfc3986($raw);
+  }
+}
+
+class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod {
+  public function get_name() {
+    return "RSA-SHA1";
+  }
+
+  protected function fetch_public_cert(&$request) {
+    // not implemented yet, ideas are:
+    // (1) do a lookup in a table of trusted certs keyed off of consumer
+    // (2) fetch via http using a url provided by the requester
+    // (3) some sort of specific discovery code based on request
+    //
+    // either way should return a string representation of the certificate
+    throw Exception("fetch_public_cert not implemented");
+  }
+
+  protected function fetch_private_cert(&$request) {
+    // not implemented yet, ideas are:
+    // (1) do a lookup in a table of trusted certs keyed off of consumer
+    //
+    // either way should return a string representation of the certificate
+    throw Exception("fetch_private_cert not implemented");
+  }
+
+  public function build_signature(&$request, $consumer, $token) {
+    $base_string = $request->get_signature_base_string();
+    $request->base_string = $base_string;
+
+    // Fetch the private key cert based on the request
+    $cert = $this->fetch_private_cert($request);
+
+    // Pull the private key ID from the certificate
+    $privatekeyid = openssl_get_privatekey($cert);
+
+    // Sign using the key
+    $ok = openssl_sign($base_string, $signature, $privatekeyid);
+
+    // Release the key resource
+    openssl_free_key($privatekeyid);
+
+    return base64_encode($signature);
+  }
+
+  public function check_signature(&$request, $consumer, $token, $signature) {
+    $decoded_sig = base64_decode($signature);
+
+    $base_string = $request->get_signature_base_string();
+
+    // Fetch the public key cert based on the request
+    $cert = $this->fetch_public_cert($request);
+
+    // Pull the public key ID from the certificate
+    $publickeyid = openssl_get_publickey($cert);
+
+    // Check the computed signature against the one passed in the query
+    $ok = openssl_verify($base_string, $decoded_sig, $publickeyid);
+
+    // Release the key resource
+    openssl_free_key($publickeyid);
+
+    return $ok == 1;
+  }
+}
+
+class OAuthRequest {
+  private $parameters;
+  private $http_method;
+  private $http_url;
+  // for debug purposes
+  public $base_string;
+  public static $version = '1.0';
+  public static $POST_INPUT = 'php://input';
+
+  function __construct($http_method, $http_url, $parameters=NULL) {
+    @$parameters or $parameters = array();
+    $this->parameters = $parameters;
+    $this->http_method = $http_method;
+    $this->http_url = $http_url;
+  }
+
+
+  /**
+   * attempt to build up a request from what was passed to the server
+   */
+  public static function from_request($http_method=NULL, $http_url=NULL, $parameters=NULL) {
+    $scheme = (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != "on")
+              ? 'http'
+              : 'https';
+    $port = "";
+    if ( $_SERVER['SERVER_PORT'] != "80" && $_SERVER['SERVER_PORT'] != "443" &&
+        strpos(':', $_SERVER['HTTP_HOST']) < 0 ) {
+      $port =  ':' . $_SERVER['SERVER_PORT'] ;
+    }
+    @$http_url or $http_url = $scheme .
+                              '://' . $_SERVER['HTTP_HOST'] .
+                              $port .
+                              $_SERVER['REQUEST_URI'];
+    @$http_method or $http_method = $_SERVER['REQUEST_METHOD'];
+
+    // We weren't handed any parameters, so let's find the ones relevant to
+    // this request.
+    // If you run XML-RPC or similar you should use this to provide your own
+    // parsed parameter-list
+    if (!$parameters) {
+      // Find request headers
+      $request_headers = OAuthUtil::get_headers();
+
+      // Parse the query-string to find GET parameters
+      $parameters = OAuthUtil::parse_parameters($_SERVER['QUERY_STRING']);
+
+      $ourpost = $_POST;
+      // Deal with magic_quotes
+      // http://www.php.net/manual/en/security.magicquotes.disabling.php
+      if ( get_magic_quotes_gpc() ) {
+         $outpost = array();
+         foreach ($_POST as $k => $v) {
+            $v = stripslashes($v);
+            $ourpost[$k] = $v;
+         }
+      }
+     // Add POST Parameters if they exist
+      $parameters = array_merge($parameters, $ourpost);
+
+      // We have a Authorization-header with OAuth data. Parse the header
+      // and add those overriding any duplicates from GET or POST
+      if (@substr($request_headers['Authorization'], 0, 6) == "OAuth ") {
+        $header_parameters = OAuthUtil::split_header(
+          $request_headers['Authorization']
+        );
+        $parameters = array_merge($parameters, $header_parameters);
+      }
+
+    }
+
+    return new OAuthRequest($http_method, $http_url, $parameters);
+  }
+
+  /**
+   * pretty much a helper function to set up the request
+   */
+  public static function from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters=NULL) {
+    @$parameters or $parameters = array();
+    $defaults = array("oauth_version" => OAuthRequest::$version,
+                      "oauth_nonce" => OAuthRequest::generate_nonce(),
+                      "oauth_timestamp" => OAuthRequest::generate_timestamp(),
+                      "oauth_consumer_key" => $consumer->key);
+    if ($token)
+      $defaults['oauth_token'] = $token->key;
+
+    $parameters = array_merge($defaults, $parameters);
+
+    // Parse the query-string to find and add GET parameters
+    $parts = parse_url($http_url);
+    if ( !empty($parts['query']) ) {
+      $qparms = OAuthUtil::parse_parameters($parts['query']);
+      $parameters = array_merge($qparms, $parameters);
+    }
+
+
+    return new OAuthRequest($http_method, $http_url, $parameters);
+  }
+
+  public function set_parameter($name, $value, $allow_duplicates = true) {
+    if ($allow_duplicates && isset($this->parameters[$name])) {
+      // We have already added parameter(s) with this name, so add to the list
+      if (is_scalar($this->parameters[$name])) {
+        // This is the first duplicate, so transform scalar (string)
+        // into an array so we can add the duplicates
+        $this->parameters[$name] = array($this->parameters[$name]);
+      }
+
+      $this->parameters[$name][] = $value;
+    } else {
+      $this->parameters[$name] = $value;
+    }
+  }
+
+  public function get_parameter($name) {
+    return isset($this->parameters[$name]) ? $this->parameters[$name] : null;
+  }
+
+  public function get_parameters() {
+    return $this->parameters;
+  }
+
+  public function unset_parameter($name) {
+    unset($this->parameters[$name]);
+  }
+
+  /**
+   * The request parameters, sorted and concatenated into a normalized string.
+   * @return string
+   */
+  public function get_signable_parameters() {
+    // Grab all parameters
+    $params = $this->parameters;
+
+    // Remove oauth_signature if present
+    // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.")
+    if (isset($params['oauth_signature'])) {
+      unset($params['oauth_signature']);
+    }
+
+    return OAuthUtil::build_http_query($params);
+  }
+
+  /**
+   * Returns the base string of this request
+   *
+   * The base string defined as the method, the url
+   * and the parameters (normalized), each urlencoded
+   * and the concated with &.
+   */
+  public function get_signature_base_string() {
+    $parts = array(
+      $this->get_normalized_http_method(),
+      $this->get_normalized_http_url(),
+      $this->get_signable_parameters()
+    );
+
+    $parts = OAuthUtil::urlencode_rfc3986($parts);
+
+    return implode('&', $parts);
+  }
+
+  /**
+   * just uppercases the http method
+   */
+  public function get_normalized_http_method() {
+    return strtoupper($this->http_method);
+  }
+
+  /**
+   * parses the url and rebuilds it to be
+   * scheme://host/path
+   */
+  public function get_normalized_http_url() {
+    $parts = parse_url($this->http_url);
+
+    $port = @$parts['port'];
+    $scheme = $parts['scheme'];
+    $host = $parts['host'];
+    $path = @$parts['path'];
+
+    $port or $port = ($scheme == 'https') ? '443' : '80';
+
+    if (($scheme == 'https' && $port != '443')
+        || ($scheme == 'http' && $port != '80')) {
+      $host = "$host:$port";
+    }
+    return "$scheme://$host$path";
+  }
+
+  /**
+   * builds a url usable for a GET request
+   */
+  public function to_url() {
+    $post_data = $this->to_postdata();
+    $out = $this->get_normalized_http_url();
+    if ($post_data) {
+      $out .= '?'.$post_data;
+    }
+    return $out;
+  }
+
+  /**
+   * builds the data one would send in a POST request
+   */
+  public function to_postdata() {
+    return OAuthUtil::build_http_query($this->parameters);
+  }
+
+  /**
+   * builds the Authorization: header
+   */
+  public function to_header() {
+    $out ='Authorization: OAuth realm=""';
+    $total = array();
+    foreach ($this->parameters as $k => $v) {
+      if (substr($k, 0, 5) != "oauth") continue;
+      if (is_array($v)) {
+        throw new OAuthException('Arrays not supported in headers');
+      }
+      $out .= ',' .
+              OAuthUtil::urlencode_rfc3986($k) .
+              '="' .
+              OAuthUtil::urlencode_rfc3986($v) .
+              '"';
+    }
+    return $out;
+  }
+
+  public function __toString() {
+    return $this->to_url();
+  }
+
+
+  public function sign_request($signature_method, $consumer, $token) {
+    $this->set_parameter(
+      "oauth_signature_method",
+      $signature_method->get_name(),
+      false
+    );
+    $signature = $this->build_signature($signature_method, $consumer, $token);
+    $this->set_parameter("oauth_signature", $signature, false);
+  }
+
+  public function build_signature($signature_method, $consumer, $token) {
+    $signature = $signature_method->build_signature($this, $consumer, $token);
+    return $signature;
+  }
+
+  /**
+   * util function: current timestamp
+   */
+  private static function generate_timestamp() {
+    return time();
+  }
+
+  /**
+   * util function: current nonce
+   */
+  private static function generate_nonce() {
+    $mt = microtime();
+    $rand = mt_rand();
+
+    return md5($mt . $rand); // md5s look nicer than numbers
+  }
+}
+
+class OAuthServer {
+  protected $timestamp_threshold = 300; // in seconds, five minutes
+  protected $version = 1.0;             // hi blaine
+  protected $signature_methods = array();
+
+  protected $data_store;
+
+  function __construct($data_store) {
+    $this->data_store = $data_store;
+  }
+
+  public function add_signature_method($signature_method) {
+    $this->signature_methods[$signature_method->get_name()] =
+      $signature_method;
+  }
+
+  // high level functions
+
+  /**
+   * process a request_token request
+   * returns the request token on success
+   */
+  public function fetch_request_token(&$request) {
+    $this->get_version($request);
+
+    $consumer = $this->get_consumer($request);
+
+    // no token required for the initial token request
+    $token = NULL;
+
+    $this->check_signature($request, $consumer, $token);
+
+    $new_token = $this->data_store->new_request_token($consumer);
+
+    return $new_token;
+  }
+
+  /**
+   * process an access_token request
+   * returns the access token on success
+   */
+  public function fetch_access_token(&$request) {
+    $this->get_version($request);
+
+    $consumer = $this->get_consumer($request);
+
+    // requires authorized request token
+    $token = $this->get_token($request, $consumer, "request");
+
+
+    $this->check_signature($request, $consumer, $token);
+
+    $new_token = $this->data_store->new_access_token($token, $consumer);
+
+    return $new_token;
+  }
+
+  /**
+   * verify an api call, checks all the parameters
+   */
+  public function verify_request(&$request) {
+    global $OAuth_last_computed_signature;
+    $OAuth_last_computed_signature = false;
+    $this->get_version($request);
+    $consumer = $this->get_consumer($request);
+    $token = $this->get_token($request, $consumer, "access");
+    $this->check_signature($request, $consumer, $token);
+    return array($consumer, $token);
+  }
+
+  // Internals from here
+  /**
+   * version 1
+   */
+  private function get_version(&$request) {
+    $version = $request->get_parameter("oauth_version");
+    if (!$version) {
+      $version = 1.0;
+    }
+    if ($version && $version != $this->version) {
+      throw new OAuthException("OAuth version '$version' not supported");
+    }
+    return $version;
+  }
+
+  /**
+   * figure out the signature with some defaults
+   */
+  private function get_signature_method(&$request) {
+    $signature_method =
+        @$request->get_parameter("oauth_signature_method");
+    if (!$signature_method) {
+      $signature_method = "PLAINTEXT";
+    }
+    if (!in_array($signature_method,
+                  array_keys($this->signature_methods))) {
+      throw new OAuthException(
+        "Signature method '$signature_method' not supported " .
+        "try one of the following: " .
+        implode(", ", array_keys($this->signature_methods))
+      );
+    }
+    return $this->signature_methods[$signature_method];
+  }
+
+  /**
+   * try to find the consumer for the provided request's consumer key
+   */
+  private function get_consumer(&$request) {
+    $consumer_key = @$request->get_parameter("oauth_consumer_key");
+    if (!$consumer_key) {
+      throw new OAuthException("Invalid consumer key");
+    }
+
+    $consumer = $this->data_store->lookup_consumer($consumer_key);
+    if (!$consumer) {
+      throw new OAuthException("Invalid consumer");
+    }
+
+    return $consumer;
+  }
+
+  /**
+   * try to find the token for the provided request's token key
+   */
+  private function get_token(&$request, $consumer, $token_type="access") {
+    $token_field = @$request->get_parameter('oauth_token');
+    if ( !$token_field) return false;
+    $token = $this->data_store->lookup_token(
+      $consumer, $token_type, $token_field
+    );
+    if (!$token) {
+      throw new OAuthException("Invalid $token_type token: $token_field");
+    }
+    return $token;
+  }
+
+  /**
+   * all-in-one function to check the signature on a request
+   * should guess the signature method appropriately
+   */
+  private function check_signature(&$request, $consumer, $token) {
+    // this should probably be in a different method
+    global $OAuth_last_computed_signature;
+    $OAuth_last_computed_signature = false;
+
+    $timestamp = @$request->get_parameter('oauth_timestamp');
+    $nonce = @$request->get_parameter('oauth_nonce');
+
+    $this->check_timestamp($timestamp);
+    $this->check_nonce($consumer, $token, $nonce, $timestamp);
+
+    $signature_method = $this->get_signature_method($request);
+
+    $signature = $request->get_parameter('oauth_signature');
+    $valid_sig = $signature_method->check_signature(
+      $request,
+      $consumer,
+      $token,
+      $signature
+    );
+
+    if (!$valid_sig) {
+      $ex_text = "Invalid signature";
+      if ( $OAuth_last_computed_signature ) {
+          $ex_text = $ex_text . " ours= $OAuth_last_computed_signature yours=$signature";
+      }
+      throw new OAuthException($ex_text);
+    }
+  }
+
+  /**
+   * check that the timestamp is new enough
+   */
+  private function check_timestamp($timestamp) {
+    // verify that timestamp is recentish
+    $now = time();
+    if ($now - $timestamp > $this->timestamp_threshold) {
+      throw new OAuthException(
+        "Expired timestamp, yours $timestamp, ours $now"
+      );
+    }
+  }
+
+  /**
+   * check that the nonce is not repeated
+   */
+  private function check_nonce($consumer, $token, $nonce, $timestamp) {
+    // verify that the nonce is uniqueish
+    $found = $this->data_store->lookup_nonce(
+      $consumer,
+      $token,
+      $nonce,
+      $timestamp
+    );
+    if ($found) {
+      throw new OAuthException("Nonce already used: $nonce");
+    }
+  }
+
+}
+
+class OAuthDataStore {
+  function lookup_consumer($consumer_key) {
+    // implement me
+  }
+
+  function lookup_token($consumer, $token_type, $token) {
+    // implement me
+  }
+
+  function lookup_nonce($consumer, $token, $nonce, $timestamp) {
+    // implement me
+  }
+
+  function new_request_token($consumer) {
+    // return a new token attached to this consumer
+  }
+
+  function new_access_token($token, $consumer) {
+    // return a new access token attached to this consumer
+    // for the user associated with this token if the request token
+    // is authorized
+    // should also invalidate the request token
+  }
+
+}
+
+class OAuthUtil {
+  public static function urlencode_rfc3986($input) {
+  if (is_array($input)) {
+    return array_map(array('OAuthUtil', 'urlencode_rfc3986'), $input);
+  } else if (is_scalar($input)) {
+    return str_replace(
+      '+',
+      ' ',
+      str_replace('%7E', '~', rawurlencode($input))
+    );
+  } else {
+    return '';
+  }
+}
+
+
+  // This decode function isn't taking into consideration the above
+  // modifications to the encoding process. However, this method doesn't
+  // seem to be used anywhere so leaving it as is.
+  public static function urldecode_rfc3986($string) {
+    return urldecode($string);
+  }
+
+  // Utility function for turning the Authorization: header into
+  // parameters, has to do some unescaping
+  // Can filter out any non-oauth parameters if needed (default behaviour)
+  public static function split_header($header, $only_allow_oauth_parameters = true) {
+    $pattern = '/(([-_a-z]*)=("([^"]*)"|([^,]*)),?)/';
+    $offset = 0;
+    $params = array();
+    while (preg_match($pattern, $header, $matches, PREG_OFFSET_CAPTURE, $offset) > 0) {
+      $match = $matches[0];
+      $header_name = $matches[2][0];
+      $header_content = (isset($matches[5])) ? $matches[5][0] : $matches[4][0];
+      if (preg_match('/^oauth_/', $header_name) || !$only_allow_oauth_parameters) {
+        $params[$header_name] = OAuthUtil::urldecode_rfc3986($header_content);
+      }
+      $offset = $match[1] + strlen($match[0]);
+    }
+
+    if (isset($params['realm'])) {
+      unset($params['realm']);
+    }
+
+    return $params;
+  }
+
+  // helper to try to sort out headers for people who aren't running apache
+  public static function get_headers() {
+    if (function_exists('apache_request_headers')) {
+      // we need this to get the actual Authorization: header
+      // because apache tends to tell us it doesn't exist
+      return apache_request_headers();
+    }
+    // otherwise we don't have apache and are just going to have to hope
+    // that $_SERVER actually contains what we need
+    $out = array();
+    foreach ($_SERVER as $key => $value) {
+      if (substr($key, 0, 5) == "HTTP_") {
+        // this is chaos, basically it is just there to capitalize the first
+        // letter of every word that is not an initial HTTP and strip HTTP
+        // code from przemek
+        $key = str_replace(
+          " ",
+          "-",
+          ucwords(strtolower(str_replace("_", " ", substr($key, 5))))
+        );
+        $out[$key] = $value;
+      }
+    }
+    return $out;
+  }
+
+  // This function takes a input like a=b&a=c&d=e and returns the parsed
+  // parameters like this
+  // array('a' => array('b','c'), 'd' => 'e')
+  public static function parse_parameters( $input ) {
+    if (!isset($input) || !$input) return array();
+
+    $pairs = explode('&', $input);
+
+    $parsed_parameters = array();
+    foreach ($pairs as $pair) {
+      $split = explode('=', $pair, 2);
+      $parameter = OAuthUtil::urldecode_rfc3986($split[0]);
+      $value = isset($split[1]) ? OAuthUtil::urldecode_rfc3986($split[1]) : '';
+
+      if (isset($parsed_parameters[$parameter])) {
+        // We have already recieved parameter(s) with this name, so add to the list
+        // of parameters with this name
+
+        if (is_scalar($parsed_parameters[$parameter])) {
+          // This is the first duplicate, so transform scalar (string) into an array
+          // so we can add the duplicates
+          $parsed_parameters[$parameter] = array($parsed_parameters[$parameter]);
+        }
+
+        $parsed_parameters[$parameter][] = $value;
+      } else {
+        $parsed_parameters[$parameter] = $value;
+      }
+    }
+    return $parsed_parameters;
+  }
+
+  public static function build_http_query($params) {
+    if (!$params) return '';
+
+    // Urlencode both keys and values
+    $keys = OAuthUtil::urlencode_rfc3986(array_keys($params));
+    $values = OAuthUtil::urlencode_rfc3986(array_values($params));
+    $params = array_combine($keys, $values);
+
+    // Parameters are sorted by name, using lexicographical byte value ordering.
+    // Ref: Spec: 9.1.1 (1)
+    uksort($params, 'strcmp');
+
+    $pairs = array();
+    foreach ($params as $parameter => $value) {
+      if (is_array($value)) {
+        // If two or more parameters share the same name, they are sorted by their value
+        // Ref: Spec: 9.1.1 (1)
+        natsort($value);
+        foreach ($value as $duplicate_value) {
+          $pairs[] = $parameter . '=' . $duplicate_value;
+        }
+      } else {
+        $pairs[] = $parameter . '=' . $value;
+      }
+    }
+    // For each parameter, the name is separated from the corresponding value by an '=' character (ASCII code 61)
+    // Each name-value pair is separated by an '&' character (ASCII code 38)
+    return implode('&', $pairs);
+  }
+}
+
+?>
diff --git a/enrol/lti/ims-blti/OAuthBody.php b/enrol/lti/ims-blti/OAuthBody.php
new file mode 100644 (file)
index 0000000..be7b7f9
--- /dev/null
@@ -0,0 +1,146 @@
+<?php
+
+require_once("OAuth.php");
+require_once("TrivialOAuthDataStore.php");
+
+function getLastOAuthBodyBaseString() {
+    global $LastOAuthBodyBaseString;
+    return $LastOAuthBodyBaseString;
+}
+
+function handleOAuthBodyPOST($oauth_consumer_key, $oauth_consumer_secret)
+{
+    $request_headers = OAuthUtil::get_headers();
+    // print_r($request_headers);
+
+    // Must reject application/x-www-form-urlencoded
+    if ($request_headers['Content-type'] == 'application/x-www-form-urlencoded' ) {
+        throw new Exception("OAuth request body signing must not use application/x-www-form-urlencoded");
+    }
+
+    if (@substr($request_headers['Authorization'], 0, 6) == "OAuth ") {
+        $header_parameters = OAuthUtil::split_header($request_headers['Authorization']);
+
+        // echo("HEADER PARMS=\n");
+        // print_r($header_parameters);
+        $oauth_body_hash = $header_parameters['oauth_body_hash'];
+        // echo("OBH=".$oauth_body_hash."\n");
+    }
+
+    if ( ! isset($oauth_body_hash)  ) {
+        throw new Exception("OAuth request body signing requires oauth_body_hash body");
+    }
+
+    // Verify the message signature
+    $store = new TrivialOAuthDataStore();
+    $store->add_consumer($oauth_consumer_key, $oauth_consumer_secret);
+
+    $server = new OAuthServer($store);
+
+    $method = new OAuthSignatureMethod_HMAC_SHA1();
+    $server->add_signature_method($method);
+    $request = OAuthRequest::from_request();
+
+    global $LastOAuthBodyBaseString;
+    $LastOAuthBodyBaseString = $request->get_signature_base_string();
+    // echo($LastOAuthBodyBaseString."\n");
+
+    try {
+        $server->verify_request($request);
+    } catch (Exception $e) {
+        $message = $e->getMessage();
+        throw new Exception("OAuth signature failed: " . $message);
+    }
+
+    $postdata = file_get_contents('php://input');
+    // echo($postdata);
+
+    $hash = base64_encode(sha1($postdata, TRUE));
+
+    if ( $hash != $oauth_body_hash ) {
+        throw new Exception("OAuth oauth_body_hash mismatch");
+    }
+
+    return $postdata;
+}
+
+function sendOAuthBodyPOST($method, $endpoint, $oauth_consumer_key, $oauth_consumer_secret, $content_type, $body)
+{
+    global $CFG;
+
+    require_once($CFG->dirroot . '/lib/filelib.php');
+
+    $hash = base64_encode(sha1($body, TRUE));
+
+    $parms = array('oauth_body_hash' => $hash);
+
+    $test_token = '';
+    $hmac_method = new OAuthSignatureMethod_HMAC_SHA1();
+    $test_consumer = new OAuthConsumer($oauth_consumer_key, $oauth_consumer_secret, NULL);
+
+    $acc_req = OAuthRequest::from_consumer_and_token($test_consumer, $test_token, $method, $endpoint, $parms);
+    $acc_req->sign_request($hmac_method, $test_consumer, $test_token);
+
+    // Pass this back up "out of band" for debugging
+    global $LastOAuthBodyBaseString;
+    $LastOAuthBodyBaseString = $acc_req->get_signature_base_string();
+    // echo($LastOAuthBodyBaseString."\m");
+
+    $headers = array();
+    $headers[] = $acc_req->to_header();
+    $headers[] = "Content-type: " . $content_type;
+
+    $curl = new curl();
+    $curl->setHeader($headers);
+    $response =  $curl->post($endpoint, $body);
+
+    return $response;
+}
+
+function sendOAuthParamsPOST($method, $endpoint, $oauth_consumer_key, $oauth_consumer_secret, $content_type, $params)
+{
+
+    if (is_array($params)) {
+        $body = http_build_query($params, '', '&');
+    } else {
+        $body = $params;
+    }
+
+    $hash = base64_encode(sha1($body, TRUE));
+
+    $parms = $params;
+    $parms['oauth_body_hash'] = $hash;
+
+    $test_token = '';
+    $hmac_method = new OAuthSignatureMethod_HMAC_SHA1();
+    $test_consumer = new OAuthConsumer($oauth_consumer_key, $oauth_consumer_secret, NULL);
+
+    $acc_req = OAuthRequest::from_consumer_and_token($test_consumer, $test_token, $method, $endpoint, $parms);
+    $acc_req->sign_request($hmac_method, $test_consumer, $test_token);
+
+    // Pass this back up "out of band" for debugging
+    global $LastOAuthBodyBaseString;
+    $LastOAuthBodyBaseString = $acc_req->get_signature_base_string();
+    // echo($LastOAuthBodyBaseString."\m");
+
+    $header = $acc_req->to_header();
+    $header = $header . "\r\nContent-type: " . $content_type . "\r\n";
+
+    $params = array('http' => array(
+        'method' => 'POST',
+        'content' => $body,
+    'header' => $header
+        ));
+    $ctx = stream_context_create($params);
+    $fp = @fopen($endpoint, 'rb', false, $ctx);
+    if (!$fp) {
+        throw new \Exception("Problem with $endpoint, $php_errormsg");
+    }
+    $response = @stream_get_contents($fp);
+    if ($response === false) {
+        throw new \Exception("Problem reading data from $endpoint, $php_errormsg");
+    }
+    return $response;
+}
+
+?>
diff --git a/enrol/lti/ims-blti/TrivialOAuthDataStore.php b/enrol/lti/ims-blti/TrivialOAuthDataStore.php
new file mode 100644 (file)
index 0000000..e283eed
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+
+require_once($CFG->dirroot . '/enrol/lti/ims-blti/OAuth.php');
+
+/**
+ * A Trivial memory-based store - no support for tokens
+ */
+class TrivialOAuthDataStore extends OAuthDataStore {
+    private $consumers = array();
+
+    function add_consumer($consumer_key, $consumer_secret) {
+        $this->consumers[$consumer_key] = $consumer_secret;
+    }
+
+    function lookup_consumer($consumer_key) {
+        if ( strpos($consumer_key, "http://" ) === 0 ) {
+            $consumer = new OAuthConsumer($consumer_key,"secret", NULL);
+            return $consumer;
+        }
+        if ( $this->consumers[$consumer_key] ) {
+            $consumer = new OAuthConsumer($consumer_key,$this->consumers[$consumer_key], NULL);
+            return $consumer;
+        }
+        return NULL;
+    }
+
+    function lookup_token($consumer, $token_type, $token) {
+        return new OAuthToken($consumer, "");
+    }
+
+    // Return NULL if the nonce has not been used
+    // Return $nonce if the nonce was previously used
+    function lookup_nonce($consumer, $token, $nonce, $timestamp) {
+        // Should add some clever logic to keep nonces from
+        // being reused - for no we are really trusting
+    // that the timestamp will save us
+        return NULL;
+    }
+
+    function new_request_token($consumer) {
+        return NULL;
+    }
+
+    function new_access_token($token, $consumer) {
+        return NULL;
+    }
+}
+?>
\ No newline at end of file
diff --git a/enrol/lti/ims-blti/blti.php b/enrol/lti/ims-blti/blti.php
new file mode 100644 (file)
index 0000000..5050183
--- /dev/null
@@ -0,0 +1,277 @@
+<?php
+
+require_once($CFG->dirroot . '/enrol/lti/ims-blti/OAuth.php');
+require_once($CFG->dirroot . '/enrol/lti/ims-blti/TrivialOAuthDataStore.php');
+
+// Returns true if this is a Basic LTI message
+// with minimum values to meet the protocol
+function is_basic_lti_request() {
+   $good_message_type = $_REQUEST["lti_message_type"] == "basic-lti-launch-request";
+   $good_lti_version = ($_REQUEST["lti_version"] == "LTI-1p0" or $_REQUEST["lti_version"] == "LTI-1.0");
+   $resource_link_id = $_REQUEST["resource_link_id"];
+   if ($good_message_type and $good_lti_version and isset($resource_link_id) ) return(true);
+   return false;
+}
+
+// Basic LTI Class that does the setup and provides utility
+// functions
+class BLTI {
+
+    public $valid = false;
+    public $complete = false;
+    public $message = false;
+    public $basestring = false;
+    public $info = false;
+    public $row = false;
+    public $context_id = false;  // Override context_id
+
+    function __construct($parm=false, $usesession=true, $doredirect=true) {
+
+
+        // If this request is not an LTI Launch, either
+        // give up or try to retrieve the context from session
+        if ( ! is_basic_lti_request() ) {
+
+            if ( $usesession === false ) return;
+
+            if ( strlen(session_id()) > 0 ) {
+                $row = $_SESSION['_basiclti_lti_row'];
+                if ( isset($row) ) $this->row = $row;
+                $context_id = $_SESSION['_basiclti_lti_context_id'];
+                if ( isset($context_id) ) $this->context_id = $context_id;
+                $info = $_SESSION['_basic_lti_context'];
+                if ( isset($info) ) {
+                    $this->info = $info;
+                    $this->valid = true;
+                    return;
+                }
+                $this->message = "Could not find context in session";
+                return;
+            }
+            $this->message = "Session not available";
+            return;
+        }
+        // Insure we have a valid launch
+        if ( empty($_REQUEST["oauth_consumer_key"]) ) {
+            $this->message = "Missing oauth_consumer_key in request";
+            return;
+        }
+        $oauth_consumer_key = $_REQUEST["oauth_consumer_key"];
+
+        // Find the secret - either form the parameter as a string or
+        // look it up in a database from parameters we are given
+        $secret = false;
+        $row = false;
+        if ( is_string($parm) ) {
+            $secret = $parm;
+        } else if ( ! is_array($parm) ) {
+            $this->message = "Constructor requires a secret or database information.";
+            return;
+        }
+
+        // Verify the message signature
+        $store = new TrivialOAuthDataStore();
+        $store->add_consumer($oauth_consumer_key, $secret);
+
+        $server = new OAuthServer($store);
+
+        $method = new OAuthSignatureMethod_HMAC_SHA1();
+        $server->add_signature_method($method);
+        $request = OAuthRequest::from_request();
+
+        $this->basestring = $request->get_signature_base_string();
+        try {
+            $server->verify_request($request);
+            $this->valid = true;
+        } catch (Exception $e) {
+            $this->message = $e->getMessage();
+            return;
+        }
+        // Store the launch information in the session for later
+        $newinfo = array();
+        foreach($_POST as $key => $value ) {
+            if ( $key == "basiclti_submit" ) continue;
+            if ( strpos($key, "oauth_") === false ) {
+                $newinfo[$key] = $value;
+                continue;
+            }
+            if ( $key == "oauth_consumer_key" ) {
+                $newinfo[$key] = $value;
+                continue;
+            }
+        }
+        //Added abertranb to decode base 64 20120801
+        if (isset($newinfo['custom_lti_message_encoded_base64']) && $newinfo['custom_lti_message_encoded_base64']==1){
+            $newinfo = $this->decodeBase64($newinfo);
+        }
+
+        $this->info = $newinfo;
+
+        if ( $usesession == true and strlen(session_id()) > 0 ) {
+             $_SESSION['_basic_lti_context'] = $this->info;
+             unset($_SESSION['_basiclti_lti_row']);
+             unset($_SESSION['_basiclti_lti_context_id']);
+             if ( $this->row ) $_SESSION['_basiclti_lti_row'] = $this->row;
+             if ( $this->context_id ) $_SESSION['_basiclti_lti_context_id'] = $this->context_id;
+        }
+
+        if ( $this->valid && $doredirect ) {
+            $this->redirect();
+            $this->complete = true;
+        }
+    }
+
+    function addSession($location) {
+        if ( ini_get('session.use_cookies') == 0 ) {
+            if ( strpos($location,'?') > 0 ) {
+               $location = $location . '&';
+            } else {
+               $location = $location . '?';
+            }
+            $location = $location . session_name() . '=' . session_id();
+        }
+        return $location;
+    }
+
+    function isInstructor() {
+        $roles = $this->info['roles'];
+        $roles = strtolower($roles);
+        if ( ! ( strpos($roles,"instructor") === false ) ) return true;
+        if ( ! ( strpos($roles,"administrator") === false ) ) return true;
+        return false;
+    }
+
+    function getUserEmail() {
+        # set default email in the event privacy settings don't pass in email.
+        $email = $this->info['user_id'] . "@ltiuser.com";
+        if ( isset($this->info['lis_person_contact_email_primary']) ) $email = $this->info['lis_person_contact_email_primary'];
+        # Sakai Hack
+        if ( isset($this->info['lis_person_contact_emailprimary']) ) $email = $this->info['lis_person_contact_emailprimary'];
+        return $email;
+    }
+
+    function getUserShortName() {
+        $email = $this->getUserEmail();
+        $givenname = $this->info['lis_person_name_given'];
+        $familyname = $this->info['lis_person_name_family'];
+        $fullname = $this->info['lis_person_name_full'];
+        if ( strlen($email) > 0 ) return $email;
+        if ( strlen($givenname) > 0 ) return $givenname;
+        if ( strlen($familyname) > 0 ) return $familyname;
+        return $this->getUserName();
+    }
+
+    function getUserName() {
+        $givenname = $this->info['lis_person_name_given'];
+        $familyname = $this->info['lis_person_name_family'];
+        $fullname = $this->info['lis_person_name_full'];
+        if ( strlen($fullname) > 0 ) return $fullname;
+        if ( strlen($familyname) > 0 and strlen($givenname) > 0 ) return $givenname + $familyname;
+        if ( strlen($givenname) > 0 ) return $givenname;
+        if ( strlen($familyname) > 0 ) return $familyname;
+        return $this->getUserEmail();
+    }
+
+    function getUserKey() {
+        $oauth = $this->info['oauth_consumer_key'];
+        $id = $this->info['user_id'];
+        if ( strlen($id) > 0 and strlen($oauth) > 0 ) return $oauth . ':' . $id;
+        return false;
+    }
+
+    function getUserImage() {
+        $image = $this->info['user_image'];
+        if ( strlen($image) > 0 ) return $image;
+        $email = $this->getUserEmail();
+        if ( $email === false ) return false;
+        $size = 40;
+        $grav_url = $_SERVER['HTTPS'] ? 'https://' : 'http://';
+        $grav_url = $grav_url . "www.gravatar.com/avatar.php?gravatar_id=".md5( strtolower($email) )."&size=".$size;
+        return $grav_url;
+    }
+
+    function getResourceKey() {
+        $oauth = $this->info['oauth_consumer_key'];
+        $id = $this->info['resource_link_id'];
+        if ( strlen($id) > 0 and strlen($oauth) > 0 ) return $oauth . ':' . $id;
+        return false;
+    }
+
+    function getResourceTitle() {
+        $title = $this->info['resource_link_title'];
+        if ( strlen($title) > 0 ) return $title;
+        return false;
+    }
+
+    function getConsumerKey() {
+        $oauth = $this->info['oauth_consumer_key'];
+        return $oauth;
+    }
+
+    function getCourseKey() {
+        if ( $this->context_id ) return $this->context_id;
+        $oauth = $this->info['oauth_consumer_key'];
+        $id = $this->info['context_id'];
+        if ( strlen($id) > 0 and strlen($oauth) > 0 ) return $oauth . ':' . $id;
+        return false;
+    }
+
+    function getCourseName() {
+        $label = $this->info['context_label'];
+        $title = $this->info['context_title'];
+        $id = $this->info['context_id'];
+        if ( strlen($label) > 0 ) return $label;
+        if ( strlen($title) > 0 ) return $title;
+        if ( strlen($id) > 0 ) return $id;
+        return false;
+    }
+
+    // TODO: Add javasript version if headers are already sent
+    function redirect() {
+            $host = $_SERVER['HTTP_HOST'];
+            $uri = $_SERVER['PHP_SELF'];
+            $location = $_SERVER['HTTPS'] ? 'https://' : 'http://';
+            $location = $location . $host . $uri;
+            $location = $this->addSession($location);
+            header("Location: $location");
+    }
+
+    function dump() {
+        if ( ! $this->valid or $this->info == false ) return "Context not valid\n";
+        $ret = "";
+        if ( $this->isInstructor() ) {
+            $ret .= "isInstructor() = true\n";
+        } else {
+            $ret .= "isInstructor() = false\n";
+        }
+        $ret .= "getUserKey() = ".$this->getUserKey()."\n";
+        $ret .= "getUserEmail() = ".$this->getUserEmail()."\n";
+        $ret .= "getUserShortName() = ".$this->getUserShortName()."\n";
+        $ret .= "getUserName() = ".$this->getUserName()."\n";
+        $ret .= "getUserImage() = ".$this->getUserImage()."\n";
+        $ret .= "getResourceKey() = ".$this->getResourceKey()."\n";
+        $ret .= "getResourceTitle() = ".$this->getResourceTitle()."\n";
+        $ret .= "getCourseName() = ".$this->getCourseName()."\n";
+        $ret .= "getCourseKey() = ".$this->getCourseKey()."\n";
+        $ret .= "getConsumerKey() = ".$this->getConsumerKey()."\n";
+        return $ret;
+    }
+
+    /**
+     * Data submitter are in base64 then we have to decode
+     * @author Antoni Bertran (antoni@tresipunt.com)
+     * @param $info array
+     * @date 20120801
+     */
+     function decodeBase64($info) {
+         $keysNoEncode = array("lti_version", "lti_message_type", "tool_consumer_instance_description", "tool_consumer_instance_guid", "oauth_consumer_key", "custom_lti_message_encoded_base64", "oauth_nonce", "oauth_version", "oauth_callback", "oauth_timestamp", "basiclti_submit", "oauth_signature_method", "ext_ims_lis_memberships_id", "ext_ims_lis_memberships_url");
+         foreach ($info as $key => $item){
+             if (!in_array($key, $keysNoEncode))
+                $info[$key] = base64_decode($item);
+         }
+        return $info;
+     }
+
+}
+
+?>
diff --git a/enrol/lti/ims-blti/blti_util.php b/enrol/lti/ims-blti/blti_util.php
new file mode 100644 (file)
index 0000000..850131f
--- /dev/null
@@ -0,0 +1,219 @@
+<?php
+
+require_once 'OAuth.php';
+
+  // Replace this with some real function that pulls from the LMS.
+  function getLMSDummyData() {
+    $parms = array(
+      "resource_link_id" => "120988f929-274612",
+      "resource_link_title" => "Weekly Blog",
+      "resource_link_description" => "Each student needs to reflect on the weekly reading.  These should be one paragraph long.",
+      "user_id" => "292832126",
+      "roles" => "Instructor",  // or Learner
+      "lis_person_name_full" => 'Jane Q. Public',
+      "lis_person_contact_email_primary" => "user@school.edu",
+      "lis_person_sourcedid" => "school.edu:user",
+      "context_id" => "456434513",
+      "context_title" => "Design of Personal Environments",
+      "context_label" => "SI182",
+      );
+
+    return $parms;
+  }
+
+  function validateDescriptor($descriptor)
+  {
+    $xml = new SimpleXMLElement($xmldata);
+    if ( ! $xml ) {
+       echo("Error parsing Descriptor XML\n");
+       return;
+    }
+    $launch_url = $xml->secure_launch_url[0];
+    if ( ! $launch_url ) $launch_url = $xml->launch_url[0];
+    if ( $launch_url ) $launch_url = (string) $launch_url;
+    return $launch_url;
+  }
+
+  // Parse a descriptor
+  function launchInfo($xmldata) {
+    $xml = new SimpleXMLElement($xmldata);
+    if ( ! $xml ) {
+       echo("Error parsing Descriptor XML\n");
+       return;
+    }
+    $launch_url = $xml->secure_launch_url[0];
+    if ( ! $launch_url ) $launch_url = $xml->launch_url[0];
+    if ( $launch_url ) $launch_url = (string) $launch_url;
+    $custom = array();
+    if ( $xml->custom[0]->parameter )
+    foreach ( $xml->custom[0]->parameter as $resource) {
+      $key = (string) $resource['key'];
+      $key = strtolower($key);
+      $nk = "";
+      for($i=0; $i < strlen($key); $i++) {
+        $ch = substr($key,$i,1);
+        if ( $ch >= "a" && $ch <= "z" ) $nk .= $ch;
+        else if ( $ch >= "0" && $ch <= "9" ) $nk .= $ch;
+        else $nk .= "_";
+      }
+      $value = (string) $resource;
+      $custom["custom_".$nk] = $value;
+    }
+    return array("launch_url" => $launch_url, "custom" => $custom ) ;
+  }
+
+function signParameters($oldparms, $endpoint, $method, $oauth_consumer_key, $oauth_consumer_secret,
+    $submit_text = false, $org_id = false, $org_desc = false)
+{
+    global $last_base_string;
+    $parms = $oldparms;
+    if ( ! isset($parms["lti_version"]) ) $parms["lti_version"] = "LTI-1p0";
+    if ( ! isset($parms["lti_message_type"]) ) $parms["lti_message_type"] = "basic-lti-launch-request";
+    if ( ! isset($parms["oauth_callback"]) ) $parms["oauth_callback"] = "about:blank";
+    if ( $org_id ) $parms["tool_consumer_instance_guid"] = $org_id;
+    if ( $org_desc ) $parms["tool_consumer_instance_description"] = $org_desc;
+    if ( $submit_text ) $parms["ext_submit"] = $submit_text;
+
+    $test_token = '';
+
+    $hmac_method = new OAuthSignatureMethod_HMAC_SHA1();
+    $test_consumer = new OAuthConsumer($oauth_consumer_key, $oauth_consumer_secret, NULL);
+
+    $acc_req = OAuthRequest::from_consumer_and_token($test_consumer, $test_token, $method, $endpoint, $parms);
+
+    $acc_req->sign_request($hmac_method, $test_consumer, $test_token);
+
+    // Pass this back up "out of band" for debugging
+    $last_base_string = $acc_req->get_signature_base_string();
+
+    $newparms = $acc_req->get_parameters();
+
+    return $newparms;
+}
+
+function signOnly($oldparms, $endpoint, $method, $oauth_consumer_key, $oauth_consumer_secret)
+{
+    global $last_base_string;
+    $parms = $oldparms;
+
+    $test_token = '';
+
+    $hmac_method = new OAuthSignatureMethod_HMAC_SHA1();
+    $test_consumer = new OAuthConsumer($oauth_consumer_key, $oauth_consumer_secret, NULL);
+
+    $acc_req = OAuthRequest::from_consumer_and_token($test_consumer, $test_token, $method, $endpoint, $parms);
+    $acc_req->sign_request($hmac_method, $test_consumer, $test_token);
+
+    // Pass this back up "out of band" for debugging
+    $last_base_string = $acc_req->get_signature_base_string();
+
+    $newparms = $acc_req->get_parameters();
+
+    return $newparms;
+}
+
+function postLaunchHTML($newparms, $endpoint, $debug=false, $iframeattr=false) {
+    global $last_base_string;
+    $r = "<div id=\"ltiLaunchFormSubmitArea\">\n";
+    if ( $iframeattr ) {
+        $r = "<form action=\"".$endpoint."\" name=\"ltiLaunchForm\" id=\"ltiLaunchForm\" method=\"post\" target=\"basicltiLaunchFrame\" encType=\"application/x-www-form-urlencoded\">\n" ;
+    } else {
+        $r = "<form action=\"".$endpoint."\" name=\"ltiLaunchForm\" id=\"ltiLaunchForm\" method=\"post\" encType=\"application/x-www-form-urlencoded\">\n" ;
+    }
+    $submit_text = $newparms['ext_submit'];
+    foreach($newparms as $key => $value ) {
+        $key = htmlspecialchars($key);
+        $value = htmlspecialchars($value);
+        if ( $key == "ext_submit" ) {
+            $r .= "<input type=\"submit\" name=\"";
+        } else {
+            $r .= "<input type=\"hidden\" name=\"";
+        }
+        $r .= $key;
+        $r .= "\" value=\"";
+        $r .= $value;
+        $r .= "\"/>\n";
+    }
+    if ( $debug ) {
+        $r .= "<script language=\"javascript\"> \n";
+        $r .= "  //<![CDATA[ \n" ;
+        $r .= "function basicltiDebugToggle() {\n";
+        $r .= "    var ele = document.getElementById(\"basicltiDebug\");\n";
+        $r .= "    if(ele.style.display == \"block\") {\n";
+        $r .= "        ele.style.display = \"none\";\n";
+        $r .= "    }\n";
+        $r .= "    else {\n";
+        $r .= "        ele.style.display = \"block\";\n";
+        $r .= "    }\n";
+        $r .= "} \n";
+        $r .= "  //]]> \n" ;
+        $r .= "</script>\n";
+        $r .= "<a id=\"displayText\" href=\"javascript:basicltiDebugToggle();\">";
+        $r .= get_stringIMS("toggle_debug_data","basiclti")."</a>\n";
+        $r .= "<div id=\"basicltiDebug\" style=\"display:none\">\n";
+        $r .=  "<b>".get_stringIMS("basiclti_endpoint","basiclti")."</b><br/>\n";
+        $r .= $endpoint . "<br/>\n&nbsp;<br/>\n";
+        $r .=  "<b>".get_stringIMS("basiclti_parameters","basiclti")."</b><br/>\n";
+        foreach($newparms as $key => $value ) {
+            $key = htmlspecialchars($key);
+            $value = htmlspecialchars($value);
+            $r .= "$key = $value<br/>\n";
+        }
+        $r .= "&nbsp;<br/>\n";
+        $r .= "<p><b>".get_stringIMS("basiclti_base_string","basiclti")."</b><br/>\n".$last_base_string."</p>\n";
+        $r .= "</div>\n";
+    }
+    $r .= "</form>\n";
+    if ( $iframeattr ) {
+        $r .= "<iframe name=\"basicltiLaunchFrame\"  id=\"basicltiLaunchFrame\" src=\"\"\n";
+        $r .= $iframeattr . ">\n<p>".get_stringIMS("frames_required","basiclti")."</p>\n</iframe>\n";
+    }
+    if ( ! $debug ) {
+        $ext_submit = "ext_submit";
+        $ext_submit_text = $submit_text;
+        $r .= " <script type=\"text/javascript\"> \n" .
+            "  //<![CDATA[ \n" .
+            "    document.getElementById(\"ltiLaunchForm\").style.display = \"none\";\n" .
+            "    nei = document.createElement('input');\n" .
+            "    nei.setAttribute('type', 'hidden');\n" .
+            "    nei.setAttribute('name', '".$ext_submit."');\n" .
+            "    nei.setAttribute('value', '".$ext_submit_text."');\n" .
+            "    document.getElementById(\"ltiLaunchForm\").appendChild(nei);\n" .
+            "    document.ltiLaunchForm.submit(); \n" .
+            "  //]]> \n" .
+            " </script> \n";
+    }
+    $r .= "</div>\n";
+    return $r;
+}
+
+/* This is a bit of homage to Moodle's pattern of internationalisation */
+function get_stringIMS($key,$bundle) {
+    return $key;
+}
+
+function do_post_request($url, $data, $optional_headers = null)
+{
+  $params = array('http' => array(
+              'method' => 'POST',
+              'content' => $data
+            ));
+
+  if ($optional_headers !== null) {
+     $header = $optional_headers . "\r\n";
+  }
+  // $header = $header . "Content-type: application/x-www-form-urlencoded\r\n";
+  $params['http']['header'] = $header;
+  $ctx = stream_context_create($params);
+  $fp = @fopen($url, 'rb', false, $ctx);
+  if (!$fp) {
+    echo @stream_get_contents($fp);
+    throw new Exception("Problem with $url, $php_errormsg");
+  }
+  $response = @stream_get_contents($fp);
+  if ($response === false) {
+    throw new Exception("Problem reading data from $url, $php_errormsg");
+  }
+  return $response;
+}
+
diff --git a/enrol/lti/ims-blti/moodle_readme.txt b/enrol/lti/ims-blti/moodle_readme.txt
new file mode 100644 (file)
index 0000000..eb7c4f2
--- /dev/null
@@ -0,0 +1,4 @@
+This library was originally published by the IMS at https://code.google.com/p/ims-dev/ which no longer exists. The
+current code was taken from https://github.com/jfederico/ims-dev/tree/master/basiclti/php-simple/ims-blti - with
+several changes to the code (including bug fixes). As the library is no longer supported upgrades are not possible.
+In future releases we should look into using a supported library.
diff --git a/enrol/lti/index.php b/enrol/lti/index.php
new file mode 100644 (file)
index 0000000..3640f86
--- /dev/null
@@ -0,0 +1,123 @@
+<?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 the tool provided in a course
+ *
+ * @package    enrol_lti
+ * @copyright  2016 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(dirname(__FILE__) . '/../../config.php');
+require_once($CFG->dirroot.'/enrol/lti/lib.php');
+
+$courseid = required_param('courseid', PARAM_INT);
+$action = optional_param('action', '', PARAM_ALPHA);
+if ($action) {
+    require_sesskey();
+    $instanceid = required_param('instanceid', PARAM_INT);
+    $instance = $DB->get_record('enrol', array('id' => $instanceid), '*', MUST_EXIST);
+}
+$confirm = optional_param('confirm', 0, PARAM_INT);
+
+$course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
+
+$context = context_course::instance($course->id);
+
+require_login($course);
+require_capability('moodle/course:enrolreview', $context);
+
+$ltiplugin = enrol_get_plugin('lti');
+$canconfig = has_capability('moodle/course:enrolconfig', $context);
+$pageurl = new moodle_url('/enrol/lti/index.php', array('courseid' => $courseid));
+
+$PAGE->set_url($pageurl);
+$PAGE->set_title(get_string('course') . ': ' . $course->fullname);
+$PAGE->set_pagelayout('admin');
+
+// Check if we want to perform any actions.
+if ($action) {
+    if ($action === 'delete') {
+        if ($ltiplugin->can_delete_instance($instance)) {
+            if ($confirm) {
+                $ltiplugin->delete_instance($instance);
+                redirect($PAGE->url);
+            }
+
+            $yesurl = new moodle_url('/enrol/lti/index.php',
+                array('courseid' => $course->id,
+                    'action' => 'delete',
+                    'instanceid' => $instance->id,
+                    'confirm' => 1,
+                    'sesskey' => sesskey())
+                );
+            $displayname = $ltiplugin->get_instance_name($instance);
+            $users = $DB->count_records('user_enrolments', array('enrolid' => $instance->id));
+            if ($users) {
+                $message = markdown_to_html(get_string('deleteinstanceconfirm', 'enrol',
+                    array('name' => $displayname,
+                          'users' => $users)));
+            } else {
+                $message = markdown_to_html(get_string('deleteinstancenousersconfirm', 'enrol',
+                    array('name' => $displayname)));
+            }
+            echo $OUTPUT->header();
+            echo $OUTPUT->confirm($message, $yesurl, $PAGE->url);
+            echo $OUTPUT->footer();
+            die();
+        }
+    } else if ($action === 'disable') {
+        if ($ltiplugin->can_hide_show_instance($instance)) {
+            if ($instance->status != ENROL_INSTANCE_DISABLED) {
+                $ltiplugin->update_status($instance, ENROL_INSTANCE_DISABLED);
+                redirect($PAGE->url);
+            }
+        }
+    } else if ($action === 'enable') {
+        if ($ltiplugin->can_hide_show_instance($instance)) {
+            if ($instance->status != ENROL_INSTANCE_ENABLED) {
+                $ltiplugin->update_status($instance, ENROL_INSTANCE_ENABLED);
+                redirect($PAGE->url);
+            }
+        }
+    }
+}
+
+echo $OUTPUT->header();
+echo $OUTPUT->heading(get_string('toolsprovided', 'enrol_lti'));
+
+if (\enrol_lti\helper::count_lti_tools(array('courseid' => $courseid)) > 0) {
+    $table = new \enrol_lti\manage_table($courseid);
+    $table->define_baseurl($pageurl);
+    $table->out(50, false);
+} else {
+    $notify = new \core\output\notification(get_string('notoolsprovided', 'enrol_lti'),
+        \core\output\notification::NOTIFY_WARNING);
+    echo $OUTPUT->render($notify);
+}
+
+if ($ltiplugin->can_add_instance($course->id)) {
+    echo $OUTPUT->single_button(new moodle_url('/enrol/editinstance.php',
+        array(
+            'type' => 'lti',
+            'courseid' => $course->id,
+            'returnurl' => new moodle_url('/enrol/lti/index.php', array('courseid' => $course->id)))
+        ),
+        get_string('add'));
+}
+
+echo $OUTPUT->footer();
diff --git a/enrol/lti/lang/en/enrol_lti.php b/enrol/lti/lang/en/enrol_lti.php
new file mode 100644 (file)
index 0000000..efe55ff
--- /dev/null
@@ -0,0 +1,68 @@
+<?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/>.
+
+/**
+ * LTI enrolment plugin version information
+ *
+ * @package enrol_lti
+ * @copyright 2016 Mark Nelson <markn@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['enrolenddate'] = 'End date';
+$string['enrolenddate_help'] = 'If enabled, users can access until this date only.';
+$string['enrolenddateerror'] = 'Enrolment end date cannot be earlier than start date';
+$string['enrolisdisabled'] = 'The LTI enrolment plugin is disabled.';
+$string['enrolperiod'] = 'Enrolment duration';
+$string['enrolperiod_help'] = 'Length of time that the enrolment is valid, starting with the moment the user enrols themselves from the remote system. If disabled, the enrolment duration will be unlimited.';
+$string['enrolmentfinished'] = 'Enrolment finished.';
+$string['enrolmentnotstarted'] = 'Enrolment has not started.';
+$string['enrolstartdate'] = 'Start date';
+$string['enrolstartdate_help'] = 'If enabled, users can access from this date onward only.';
+$string['globalsharedsecret'] = 'Global shared secret';
+$string['gradesync'] = 'Grade synchronisation';
+$string['gradesync_help'] = 'This determines if we want grade synchronisation to occur.';
+$string['maxenrolled'] = 'Maximum enrolled';
+$string['maxenrolled_help'] = 'Specifies the maximum number of users that can access from the remote system. The value \'0\' means there is no limit.';
+$string['maxenrolledreached'] = 'Maximum number of users allowed to access was already reached.';
+$string['membersync'] = 'Member synchronisation';
+$string['membersync_help'] = 'This determines if we want member synchronisation to occur.';
+$string['membersyncmode'] = 'Members synchronisation mode';
+$string['membersyncmode_help'] = 'This setting determines what we should do when synchronising members.';
+$string['membersyncmodeenrolandunenrol'] = 'Enrol new and unenrol missing members';
+$string['membersyncmodeenrolnew'] = 'Enrol new members';
+$string['membersyncmodeunenrolmissing'] = 'Unenrol missing members';
+$string['notoolsprovided'] = 'No tools provided';
+$string['lti:config'] = 'Configure LTI enrol instances';
+$string['lti:unenrol'] = 'Unenrol users from the course';
+$string['pluginname'] = 'Shared external tool';
+$string['pluginname_desc'] = 'The shared external tool plugin allows externals users to access a course or an activity via a unique link - this requires the LTI authentication plugin to be enabled.';
+$string['remotesystem'] = 'Remote system';
+$string['requirecompletion'] = 'Require the course or activity to be completed before sending the grades';
+$string['roleinstructor'] = 'Role for instructor';
+$string['roleinstructor_help'] = 'This is the role that will be assigned at the context of the tool specificed to LTI consumer instructor.';
+$string['rolelearner'] = 'Role for learner';
+$string['rolelearner_help'] = 'This is the role that will be assigned at the context of the tool specificed to the LTI consumer student.';
+$string['secret'] = 'Secret';
+$string['secret_help'] = 'This is the secret that is shared with the LTI consumer in order for them to access this tool';
+$string['sharedexternaltools'] = 'Shared external tools';
+$string['syncsettings'] = 'Synchronisation settings';
+$string['tooldoesnotexist'] = 'The requested tool does not exist.';
+$string['tasksyncgrades'] = 'Handles syncing grades with the consumer';
+$string['tasksyncmembers'] = 'handles syncing members with the consumer';
+$string['toolsprovided'] = 'Tools provided';
+$string['tooltobeprovided'] = 'Tool to be provided';
+$string['userdefaultvalues'] = 'User default values';
diff --git a/enrol/lti/lib.php b/enrol/lti/lib.php
new file mode 100644 (file)
index 0000000..372e287
--- /dev/null
@@ -0,0 +1,423 @@
+<?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/>.
+
+/**
+ * LTI enrolment plugin main library file.
+ *
+ * @package enrol_lti
+ * @copyright 2016 Mark Nelson <markn@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * LTI enrolment plugin class.
+ *
+ * @package enrol_lti
+ * @copyright 2016 Mark Nelson <markn@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class enrol_lti_plugin extends enrol_plugin {
+
+    /**
+     * Return true if we can add a new instance to this course.
+     *
+     * @param int $courseid
+     * @return boolean
+     */
+    public function can_add_instance($courseid) {
+        $context = context_course::instance($courseid, MUST_EXIST);
+        return has_capability('moodle/course:enrolconfig', $context) && has_capability('enrol/lti:config', $context);
+    }
+
+    /**
+     * Is it possible to delete