Merge branch 'MDL-61846-master' of git://github.com/mihailges/moodle
authorDavid Monllao <davidm@moodle.com>
Tue, 10 Apr 2018 06:59:25 +0000 (08:59 +0200)
committerDavid Monllao <davidm@moodle.com>
Tue, 10 Apr 2018 06:59:25 +0000 (08:59 +0200)
108 files changed:
admin/cli/install.php
admin/settings/appearance.php
admin/tool/log/store/database/classes/helper.php
admin/tool/mobile/classes/api.php
admin/tool/mobile/classes/external.php
admin/tool/mobile/db/services.php
admin/tool/mobile/tests/externallib_test.php
admin/tool/mobile/tests/fixtures/output/mobile.php [new file with mode: 0644]
admin/tool/mobile/upgrade.txt
admin/tool/mobile/version.php
auth/db/tests/db_test.php
auth/test_settings.php
backup/moodle2/restore_subplugin.class.php
blocks/community/classes/privacy/provider.php [new file with mode: 0644]
blocks/community/lang/en/block_community.php
blocks/community/tests/privacy_test.php [new file with mode: 0644]
blocks/rss_client/classes/privacy/provider.php [new file with mode: 0644]
blocks/rss_client/lang/en/block_rss_client.php
blocks/rss_client/tests/privacy_test.php [new file with mode: 0644]
cohort/classes/external/cohort_summary_exporter.php
cohort/edit_form.php
cohort/externallib.php
cohort/lib.php
cohort/tests/behat/upload_cohorts.feature
cohort/tests/cohortlib_test.php
cohort/tests/externallib_test.php
cohort/tests/fixtures/uploadcohorts4.csv [new file with mode: 0644]
cohort/upload_form.php
config-dist.php
course/classes/output/modchooser_item.php
enrol/database/tests/sync_test.php
enrol/test_settings.php
install.php
lang/en/admin.php
lang/en/cohort.php
lang/en/install.php
lang/en/tag.php
lib/classes/plugin_manager.php
lib/db/install.xml
lib/db/upgrade.php
lib/dml/mssql_native_moodle_database.php [deleted file]
lib/dml/mssql_native_moodle_recordset.php [deleted file]
lib/dml/mssql_native_moodle_temptables.php [deleted file]
lib/dml/sqlsrv_native_moodle_temptables.php
lib/dmllib.php
lib/moodlelib.php
lib/outputrenderers.php
lib/pagelib.php
lib/tests/moodle_page_test.php
lib/upgradelib.php
media/player/videojs/classes/plugin.php
media/player/videojs/lang/en/media_videojs.php
media/player/videojs/settings.php
media/player/videojs/tests/player_test.php
mod/glossary/import.php
mod/glossary/lib.php
mod/glossary/tests/behat/import_entries.feature
mod/glossary/tests/fixtures/musicians.xml [new file with mode: 0644]
mod/lti/classes/local/ltiservice/resource_base.php
mod/lti/classes/local/ltiservice/response.php
mod/lti/classes/local/ltiservice/service_base.php
mod/lti/edit_form.php
mod/lti/lang/en/lti.php
mod/lti/locallib.php
mod/lti/service/gradebookservices/backup/moodle2/backup_ltiservice_gradebookservices_subplugin.class.php [new file with mode: 0644]
mod/lti/service/gradebookservices/backup/moodle2/restore_ltiservice_gradebookservices_subplugin.class.php [new file with mode: 0644]
mod/lti/service/gradebookservices/classes/local/resources/lineitem.php [new file with mode: 0644]
mod/lti/service/gradebookservices/classes/local/resources/lineitems.php [new file with mode: 0644]
mod/lti/service/gradebookservices/classes/local/resources/results.php [new file with mode: 0644]
mod/lti/service/gradebookservices/classes/local/resources/scores.php [new file with mode: 0644]
mod/lti/service/gradebookservices/classes/local/service/gradebookservices.php [new file with mode: 0644]
mod/lti/service/gradebookservices/classes/task/cleanup_task.php [new file with mode: 0644]
mod/lti/service/gradebookservices/db/install.xml [new file with mode: 0644]
mod/lti/service/gradebookservices/db/tasks.php [new file with mode: 0644]
mod/lti/service/gradebookservices/lang/en/ltiservice_gradebookservices.php [new file with mode: 0644]
mod/lti/service/gradebookservices/tests/task_cleanup_test.php [new file with mode: 0644]
mod/lti/service/gradebookservices/version.php [new file with mode: 0644]
mod/lti/service/memberships/classes/local/resources/contextmemberships.php
mod/lti/service/memberships/classes/local/resources/linkmemberships.php
mod/lti/service/memberships/classes/local/service/memberships.php
mod/lti/service/memberships/lang/en/ltiservice_memberships.php
mod/lti/service/profile/classes/local/resources/profile.php
mod/lti/service/toolsettings/classes/local/resources/contextsettings.php
mod/lti/service/toolsettings/classes/local/resources/linksettings.php
mod/lti/service/toolsettings/classes/local/resources/systemsettings.php
mod/quiz/attempt.php
mod/quiz/attemptlib.php
mod/quiz/autosave.ajax.php
mod/quiz/comment.php
mod/quiz/lang/en/quiz.php
mod/quiz/locallib.php
mod/quiz/processattempt.php
mod/quiz/renderer.php
mod/quiz/review.php
mod/quiz/reviewquestion.php
mod/quiz/summary.php
mod/quiz/tests/attempt_test.php
mod/quiz/tests/attempts_test.php
mod/survey/lib.php
privacy/tests/provider_test.php [new file with mode: 0644]
question/classes/external.php
question/lib.php
question/type/edit_question_form.php
question/type/tags_form.php
theme/boost/templates/flat_navigation.mustache
user/classes/participants_table.php
user/tests/behat/view_participants.feature
version.php

index 240a819..1ff43fe 100644 (file)
@@ -219,7 +219,6 @@ $databases = array('mysqli' => moodle_database::get_driver_instance('mysqli', 'n
                    'pgsql'  => moodle_database::get_driver_instance('pgsql',  'native'),
                    'oci'    => moodle_database::get_driver_instance('oci',    'native'),
                    'sqlsrv' => moodle_database::get_driver_instance('sqlsrv', 'native'), // MS SQL*Server PHP driver
-                   'mssql'  => moodle_database::get_driver_instance('mssql',  'native'), // FreeTDS driver
                   );
 foreach ($databases as $type=>$database) {
     if ($database->driver_installed() !== true) {
index 7486db8..db05c1d 100644 (file)
@@ -22,6 +22,7 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) { // sp
     $temp->add(new admin_setting_configcheckbox('allowuserthemes', new lang_string('allowuserthemes', 'admin'), new lang_string('configallowuserthemes', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('allowcoursethemes', new lang_string('allowcoursethemes', 'admin'), new lang_string('configallowcoursethemes', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('allowcategorythemes',  new lang_string('allowcategorythemes', 'admin'), new lang_string('configallowcategorythemes', 'admin'), 0));
+    $temp->add(new admin_setting_configcheckbox('allowcohortthemes',  new lang_string('allowcohortthemes', 'admin'), new lang_string('configallowcohortthemes', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('allowthemechangeonurl',  new lang_string('allowthemechangeonurl', 'admin'), new lang_string('configallowthemechangeonurl', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('allowuserblockhiding', new lang_string('allowuserblockhiding', 'admin'), new lang_string('configallowuserblockhiding', 'admin'), 1));
     $temp->add(new admin_setting_configcheckbox('allowblockstodock', new lang_string('allowblockstodock', 'admin'), new lang_string('configallowblockstodock', 'admin'), 1));
index 1acb99c..bad2c1b 100644 (file)
@@ -44,8 +44,7 @@ class helper {
             'native/mariadb' => \moodle_database::get_driver_instance('mariadb', 'native')->get_name(),
             'native/pgsql'   => \moodle_database::get_driver_instance('pgsql', 'native')->get_name(),
             'native/oci'     => \moodle_database::get_driver_instance('oci', 'native')->get_name(),
-            'native/sqlsrv'  => \moodle_database::get_driver_instance('sqlsrv', 'native')->get_name(),
-            'native/mssql'   => \moodle_database::get_driver_instance('mssql', 'native')->get_name()
+            'native/sqlsrv'  => \moodle_database::get_driver_instance('sqlsrv', 'native')->get_name()
         );
     }
 
index 160c4f0..189f99e 100644 (file)
@@ -90,6 +90,25 @@ class api {
 
                 require("$path/db/mobile.php");
                 foreach ($addons as $addonname => $addoninfo) {
+
+                    // Add handlers (for site add-ons).
+                    $handlers = !empty($addoninfo['handlers']) ? $addoninfo['handlers'] : array();
+                    $handlers = json_encode($handlers); // JSON formatted, since it is a complex structure that may vary over time.
+
+                    // Now language strings used by the app.
+                    $lang = array();
+                    if (!empty($addoninfo['lang'])) {
+                        $stringmanager = get_string_manager();
+                        $langs = $stringmanager->get_list_of_translations();
+                        foreach ($langs as $langid => $langname) {
+                            foreach ($addoninfo['lang'] as $stringinfo) {
+                                $lang[$langid][$stringinfo[0]] =
+                                    $stringmanager->get_string($stringinfo[0], $stringinfo[1], null, $langid);
+                            }
+                        }
+                    }
+                    $lang = json_encode($lang);
+
                     $plugininfo = array(
                         'component' => $component,
                         'version' => $version,
@@ -97,7 +116,9 @@ class api {
                         'dependencies' => !empty($addoninfo['dependencies']) ? $addoninfo['dependencies'] : array(),
                         'fileurl' => '',
                         'filehash' => '',
-                        'filesize' => 0
+                        'filesize' => 0,
+                        'handlers' => $handlers,
+                        'lang' => $lang,
                     );
 
                     // All the mobile packages must be under the plugin mobile directory.
index 7df769b..6a46264 100644 (file)
@@ -28,6 +28,7 @@ defined('MOODLE_INTERNAL') || die();
 require_once("$CFG->libdir/externallib.php");
 
 use external_api;
+use external_files;
 use external_function_parameters;
 use external_value;
 use external_single_structure;
@@ -37,6 +38,7 @@ use context_system;
 use moodle_exception;
 use moodle_url;
 use core_text;
+use coding_exception;
 
 /**
  * This is the external API for this tool.
@@ -91,7 +93,9 @@ class external extends external_api {
                             'fileurl' => new external_value(PARAM_URL, 'The addon package url for download
                                                             or empty if it doesn\'t exist.'),
                             'filehash' => new external_value(PARAM_RAW, 'The addon package hash or empty if it doesn\'t exist.'),
-                            'filesize' => new external_value(PARAM_INT, 'The addon package size or empty if it doesn\'t exist.')
+                            'filesize' => new external_value(PARAM_INT, 'The addon package size or empty if it doesn\'t exist.'),
+                            'handlers' => new external_value(PARAM_RAW, 'Handlers definition (JSON)', VALUE_OPTIONAL),
+                            'lang' => new external_value(PARAM_RAW, 'Language strings used by the handlers (JSON)', VALUE_OPTIONAL),
                         )
                     )
                 ),
@@ -330,4 +334,130 @@ class external extends external_api {
             )
         );
     }
+
+    /**
+     * Returns description of get_content() parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.5
+     */
+    public static function get_content_parameters() {
+        return new external_function_parameters(
+            array(
+                'component' => new external_value(PARAM_COMPONENT, 'Component where the class is e.g. mod_assign.'),
+                'method' => new external_value(PARAM_ALPHANUMEXT, 'Method to execute in class \$component\output\mobile.'),
+                'args' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'name' => new external_value(PARAM_ALPHANUMEXT, 'Param name.'),
+                            'value' => new external_value(PARAM_RAW, 'Param value.')
+                        )
+                    ), 'Args for the method are optional.', VALUE_OPTIONAL
+                )
+            )
+        );
+    }
+
+    /**
+     * Returns a piece of content to be displayed in the Mobile app, it usually returns a template, javascript and
+     * other structured data that will be used to render a view in the Mobile app..
+     *
+     * Callbacks (placed in \$component\output\mobile) that are called by this web service are responsible for doing the
+     * appropriate security checks to access the information to be returned.
+     *
+     * @param string $component fame of the component.
+     * @param string $method function method name in class \$component\output\mobile.
+     * @param array $args optional arguments for the method.
+     * @return array HTML, JavaScript and other required data and information to create a view in the app.
+     * @since Moodle 3.5
+     * @throws coding_exception
+     */
+    public static function get_content($component, $method, $args = array()) {
+        global $OUTPUT, $PAGE, $USER;
+
+        $params = self::validate_parameters(self::get_content_parameters(),
+            array(
+                'component' => $component,
+                'method' => $method,
+                'args' => $args
+            )
+        );
+
+        // Reformat arguments into something less unwieldy.
+        $arguments = array();
+        foreach ($params['args'] as $paramargument) {
+            $arguments[$paramargument['name']] = $paramargument['value'];
+        }
+
+        // The component was validated via the PARAM_COMPONENT parameter type.
+        $classname = '\\' . $params['component'] .'\output\mobile';
+        if (!method_exists($classname, $params['method'])) {
+            throw new coding_exception("Missing method in $classname");
+        }
+        $result = call_user_func_array(array($classname, $params['method']), array($arguments));
+
+        // Populate otherdata.
+        $otherdata = array();
+        if (!empty($result['otherdata'])) {
+            $result['otherdata'] = (array) $result['otherdata'];
+            foreach ($result['otherdata'] as $name => $value) {
+                $otherdata[] = array(
+                    'name' => $name,
+                    'value' => $value
+                );
+            }
+        }
+
+        return array(
+            'templates'  => !empty($result['templates']) ? $result['templates'] : array(),
+            'javascript' => !empty($result['javascript']) ? $result['javascript'] : '',
+            'otherdata'  => $otherdata,
+            'files'      => !empty($result['files']) ? $result['files'] : array(),
+            'restrict'   => !empty($result['restrict']) ? $result['restrict'] : array(),
+        );
+    }
+
+    /**
+     * Returns description of get_content() result value
+     *
+     * @return array
+     * @since Moodle 3.5
+     */
+    public static function get_content_returns() {
+        return new external_single_structure(
+            array(
+                'templates' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'id' => new external_value(PARAM_TEXT, 'ID of the template.'),
+                            'html' => new external_value(PARAM_RAW, 'HTML code.'),
+                        )
+                    ),
+                    'Templates required by the generated content.'
+                ),
+                'javascript' => new external_value(PARAM_RAW, 'JavaScript code.'),
+                'otherdata' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'name' => new external_value(PARAM_RAW, 'Field name.'),
+                            'value' => new external_value(PARAM_RAW, 'Field value.')
+                        )
+                    ),
+                    'Other data that can be used or manipulated by the template via 2-way data-binding.'
+                ),
+                'files' => new external_files('Files in the content.'),
+                'restrict' => new external_single_structure(
+                    array(
+                        'users' => new external_multiple_structure(
+                            new external_value(PARAM_INT, 'user id'), 'List of allowed users.', VALUE_OPTIONAL
+                        ),
+                        'courses' => new external_multiple_structure(
+                            new external_value(PARAM_INT, 'course id'), 'List of allowed courses.', VALUE_OPTIONAL
+                        ),
+                    ),
+                    'Restrict this content to certain users or courses.'
+                )
+            )
+        );
+    }
 }
index aa64cc7..5e329c6 100644 (file)
@@ -60,6 +60,13 @@ $functions = array(
                             Is created only in https sites and is restricted by time and ip address.',
         'type'        => 'write',
         'services'    => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
-    )
+    ),
+    'tool_mobile_get_content' => array(
+        'classname'   => 'tool_mobile\external',
+        'methodname'  => 'get_content',
+        'description' => 'Returns a piece of content to be displayed in the Mobile app.',
+        'type'        => 'read',
+        'services'    => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
 );
 
index e61657c..0b8873b 100644 (file)
@@ -29,6 +29,7 @@ defined('MOODLE_INTERNAL') || die();
 global $CFG;
 
 require_once($CFG->dirroot . '/webservice/tests/helpers.php');
+require_once($CFG->dirroot . '/admin/tool/mobile/tests/fixtures/output/mobile.php');
 
 use tool_mobile\external;
 use tool_mobile\api;
@@ -305,4 +306,53 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
         $this->expectExceptionMessage(get_string('autologinkeygenerationlockout', 'tool_mobile'));
         $result = external::get_autologin_key($token->privatetoken);
     }
+
+    /**
+     * Test get_content.
+     */
+    public function test_get_content() {
+
+        $paramval = 16;
+        $result = external::get_content('tool_mobile', 'test_view', array(array('name' => 'param1', 'value' => $paramval)));
+        $result = external_api::clean_returnvalue(external::get_content_returns(), $result);
+        $this->assertCount(1, $result['templates']);
+        $this->assertCount(1, $result['otherdata']);
+        $this->assertCount(2, $result['restrict']['users']);
+        $this->assertCount(2, $result['restrict']['courses']);
+        $this->assertEquals('alert();', $result['javascript']);
+        $this->assertEquals('main', $result['templates'][0]['id']);
+        $this->assertEquals('The HTML code', $result['templates'][0]['html']);
+        $this->assertEquals('otherdata1', $result['otherdata'][0]['name']);
+        $this->assertEquals($paramval, $result['otherdata'][0]['value']);
+        $this->assertEquals(array(1, 2), $result['restrict']['users']);
+        $this->assertEquals(array(3, 4), $result['restrict']['courses']);
+        $this->assertEmpty($result['files']);
+    }
+
+    /**
+     * Test get_content non existent function in valid component.
+     */
+    public function test_get_content_non_existent_function() {
+
+        $this->expectException('coding_exception');
+        $result = external::get_content('tool_mobile', 'test_blahblah');
+    }
+
+    /**
+     * Test get_content incorrect component.
+     */
+    public function test_get_content_invalid_component() {
+
+        $this->expectException('moodle_exception');
+        $result = external::get_content('tool_mobile\hack', 'test_view');
+    }
+
+    /**
+     * Test get_content non existent component.
+     */
+    public function test_get_content_non_existent_component() {
+
+        $this->expectException('moodle_exception');
+        $result = external::get_content('tool_blahblahblah', 'test_view');
+    }
 }
diff --git a/admin/tool/mobile/tests/fixtures/output/mobile.php b/admin/tool/mobile/tests/fixtures/output/mobile.php
new file mode 100644 (file)
index 0000000..d803743
--- /dev/null
@@ -0,0 +1,60 @@
+<?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/>.
+
+/**
+ * Mock class for get_content.
+ *
+ * @package tool_mobile
+ * @copyright 2018 Juan Leyva
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_mobile\output;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Mock class for get_content.
+ *
+ * @package tool_mobile
+ * @copyright 2018 Juan Leyva
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mobile {
+
+    /**
+     * Returns a test view.
+     * @param  array $args Arguments from tool_mobile_get_content WS
+     *
+     * @return array       HTML, javascript and otherdata
+     */
+    public static function test_view($args) {
+        $args = (object) $args;
+
+        return array(
+            'templates' => array(
+                array(
+                    'id' => 'main',
+                    'html' => 'The HTML code',
+                ),
+            ),
+            'javascript' => 'alert();',
+            'otherdata' => array('otherdata1' => $args->param1),
+            'restrict' => array('users' => array(1, 2), 'courses' => array(3, 4)),
+            'files' => array()
+        );
+    }
+}
index 6c2c704..2149382 100644 (file)
@@ -1,6 +1,11 @@
 This files describes changes in tool_mobile code.
 Information provided here is intended especially for developers.
 
+=== 3.5 ===
+
+ * External function tool_mobile::tool_mobile_get_plugins_supporting_mobile now returns additional plugins information required by
+   Moodle Mobile 3.5.0.
+
 === 3.4 ===
 
  * External function tool_mobile::tool_mobile_get_plugins_supporting_mobile is now available via AJAX for not logged users.
index c09e6fd..438a6d5 100644 (file)
@@ -23,7 +23,7 @@
  */
 
 defined('MOODLE_INTERNAL') || die();
-$plugin->version   = 2017111300; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2017111301; // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2017110800; // Requires this Moodle version.
 $plugin->component = 'tool_mobile'; // Full name of the plugin (used for diagnostics).
 $plugin->dependencies = array(
index 2900719..fd6a62c 100644 (file)
@@ -97,11 +97,7 @@ class auth_db_testcase extends advanced_testcase {
                 break;
 
             case 'mssql':
-                if (get_class($DB) == 'mssql_native_moodle_database') {
-                    set_config('type', 'mssql_n', 'auth_db');
-                } else {
-                    set_config('type', 'mssqlnative', 'auth_db');
-                }
+                set_config('type', 'mssqlnative', 'auth_db');
                 set_config('sybasequoting', '1', 'auth_db');
                 break;
 
index a0ff227..c0eef4b 100644 (file)
@@ -22,7 +22,7 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-require(__DIR__.'../../config.php');
+require(__DIR__.'/../config.php');
 require_once("$CFG->libdir/adminlib.php");
 
 $auth = optional_param('auth', '', PARAM_RAW);
index 640968b..c848219 100644 (file)
@@ -87,6 +87,23 @@ abstract class restore_subplugin {
         }
     }
 
+    /**
+     * The after_restore dispatcher for any restore_subplugin class.
+     *
+     * This method will dispatch execution to the corresponding
+     * after_restore_xxx() method when available, with xxx
+     * being the connection point of the instance, so subplugin
+     * classes with multiple connection points will support
+     * multiple after_restore methods, one for each connection point.
+     */
+    public function launch_after_restore_methods() {
+        // Check if the after_restore method exists and launch it.
+        $afterestore = 'after_restore_' . basename($this->connectionpoint->get_path());
+        if (method_exists($this, $afterestore)) {
+            $this->$afterestore();
+        }
+    }
+
 // Protected API starts here
 
 // restore_step/structure_step/task wrappers
diff --git a/blocks/community/classes/privacy/provider.php b/blocks/community/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..5699066
--- /dev/null
@@ -0,0 +1,181 @@
+<?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/>.
+
+/**
+ * Privacy Subsystem implementation for block_community.
+ *
+ * @package    block_community
+ * @copyright  2018 Zig Tan <zig@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace block_community\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+use \core_privacy\local\request\approved_contextlist;
+use \core_privacy\local\request\contextlist;
+use \core_privacy\local\request\writer;
+use \core_privacy\local\request\deletion_criteria;
+use \core_privacy\local\metadata\collection;
+
+/**
+ * Privacy Subsystem implementation for block_community.
+ *
+ * @copyright  2018 Zig Tan <zig@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider {
+
+    /**
+     * Returns information about how block_community stores its data.
+     *
+     * @param   collection     $collection The initialised collection to add items to.
+     * @return  collection     A listing of user data stored through this system.
+     */
+    public static function get_metadata(collection $collection) : collection {
+        $collection->add_database_table(
+            'block_community',
+            [
+                'coursename' => 'privacy:metadata:block_community:coursename',
+                'coursedescription' => 'privacy:metadata:block_community:coursedescription',
+                'courseurl' => 'privacy:metadata:block_community:courseurl',
+                'imageurl' => 'privacy:metadata:block_community:imageurl',
+                'userid' => 'privacy:metadata:block_community:userid',
+            ],
+            'privacy:metadata:block_community'
+        );
+
+        return $collection;
+    }
+
+    /**
+     * Get the list of contexts that contain user information for the specified user.
+     *
+     * @param   int         $userid     The user to search.
+     * @return  contextlist   $contextlist  The contextlist containing the list of contexts used in this plugin.
+     */
+    public static function get_contexts_for_userid(int $userid) : contextlist {
+        $contextlist = new \core_privacy\local\request\contextlist();
+
+        // The block_community data is associated at the user context level, so retrieve the user's context id.
+        $sql = "SELECT c.id
+                  FROM {block_community} bc
+                  JOIN {context} c ON c.instanceid = bc.userid AND c.contextlevel = :contextuser
+                 WHERE bc.userid = :userid
+              GROUP BY c.id";
+
+        $params = [
+            'contextuser'   => CONTEXT_USER,
+            'userid'        => $userid
+        ];
+
+        $contextlist->add_from_sql($sql, $params);
+
+        return $contextlist;
+    }
+
+    /**
+     * Export all user data for the specified user using the User context level.
+     *
+     * @param   approved_contextlist    $contextlist    The approved contexts to export information for.
+     */
+    public static function export_user_data(approved_contextlist $contextlist) {
+        global $DB;
+
+        // If the user has block_community data, then only the User context should be present so get the first context.
+        $contexts = $contextlist->get_contexts();
+        if (count($contexts) == 0) {
+            return;
+        }
+        $context = reset($contexts);
+
+        // Sanity check that context is at the User context level, then get the userid.
+        if ($context->contextlevel !== CONTEXT_USER) {
+            return;
+        }
+        $userid = $context->instanceid;
+
+        // The block_community data export is organised in: {User Context}/Community Finder/My communities/data.json.
+        $subcontext = [
+            get_string('pluginname', 'block_community'),
+            get_string('mycommunities', 'block_community')
+        ];
+
+        $sql = "SELECT bc.id as id,
+                       bc.coursename as name,
+                       bc.coursedescription as description,
+                       bc.courseurl as url,
+                       bc.imageurl as imageurl
+                  FROM {block_community} bc
+                 WHERE bc.userid = :userid
+              ORDER BY bc.coursename";
+
+        $params = [
+            'userid' => $userid
+        ];
+
+        $communities = $DB->get_records_sql($sql, $params);
+
+        $data = (object) [
+            'communities' => $communities
+        ];
+
+        writer::with_context($context)->export_data($subcontext, $data);
+    }
+
+    /**
+     * Delete all data for all users in the specified context.
+     *
+     * @param   context $context   The specific context to delete data for.
+     */
+    public static function delete_data_for_all_users_in_context(\context $context) {
+        global $DB;
+
+        // Sanity check that context is at the User context level, then get the userid.
+        if ($context->contextlevel !== CONTEXT_USER) {
+            return;
+        }
+        $userid = $context->instanceid;
+
+        $DB->delete_records('block_community', ['userid' => $userid]);
+    }
+
+    /**
+     * Delete all user data for the specified user.
+     *
+     * @param   approved_contextlist $contextlist  The approved contexts and user information to delete information for.
+     */
+    public static function delete_data_for_user(approved_contextlist $contextlist) {
+        global $DB;
+
+        // If the user has block_community data, then only the User context should be present so get the first context.
+        $contexts = $contextlist->get_contexts();
+        if (count($contexts) == 0) {
+            return;
+        }
+        $context = reset($contexts);
+
+        // Sanity check that context is at the User context level, then get the userid.
+        if ($context->contextlevel !== CONTEXT_USER) {
+            return;
+        }
+        $userid = $context->instanceid;
+
+        $DB->delete_records('block_community', ['userid' => $userid]);
+    }
+
+}
index 1d8fa91..73e65d4 100644 (file)
@@ -95,6 +95,12 @@ $string['orderbypublisher'] = 'Publisher';
 $string['orderbyratingaverage'] = 'Rating';
 $string['outcomes'] = 'Outcomes: {$a}';
 $string['pluginname'] = 'Community finder';
+$string['privacy:metadata:block_community'] = 'The Community block stores links to shared community courses users can enrol in.';
+$string['privacy:metadata:block_community:coursename'] = 'The name of the linked community course.';
+$string['privacy:metadata:block_community:coursedescription'] = 'The description of the linked community course.';
+$string['privacy:metadata:block_community:courseurl'] = 'The course URL of the linked community course.';
+$string['privacy:metadata:block_community:imageurl'] = 'The image URL of the linked community course.';
+$string['privacy:metadata:block_community:userid'] = 'The ID of the user who created the linked community course.';
 $string['rateandcomment'] = 'Rate and comment';
 $string['rating'] = 'Rating';
 $string['removecommunitycourse'] = 'Remove community course';
diff --git a/blocks/community/tests/privacy_test.php b/blocks/community/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..48f672f
--- /dev/null
@@ -0,0 +1,267 @@
+<?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/>.
+
+/**
+ * Unit tests for the block_community implementation of the privacy API.
+ *
+ * @package    block_community
+ * @category   test
+ * @copyright  2018 Zig Tan <zig@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use \core_privacy\local\metadata\collection;
+use \core_privacy\local\request\writer;
+use \core_privacy\local\request\approved_contextlist;
+use \block_community\privacy\provider;
+
+/**
+ * Unit tests for the block_community implementation of the privacy API.
+ *
+ * @copyright  2018 Zig Tan <zig@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class block_community_privacy_testcase extends \core_privacy\tests\provider_testcase {
+
+    /**
+     * Overriding setUp() function to always reset after tests.
+     */
+    public function setUp() {
+        $this->resetAfterTest(true);
+    }
+
+    /**
+     * Test for provider::get_metadata().
+     */
+    public function test_get_metadata() {
+        $collection = new collection('block_community');
+        $newcollection = provider::get_metadata($collection);
+        $itemcollection = $newcollection->get_collection();
+        $this->assertCount(1, $itemcollection);
+
+        $table = reset($itemcollection);
+        $this->assertEquals('block_community', $table->get_name());
+
+        $privacyfields = $table->get_privacy_fields();
+        $this->assertArrayHasKey('userid', $privacyfields);
+        $this->assertArrayHasKey('coursename', $privacyfields);
+        $this->assertArrayHasKey('coursedescription', $privacyfields);
+        $this->assertArrayHasKey('courseurl', $privacyfields);
+        $this->assertArrayHasKey('imageurl', $privacyfields);
+
+        $this->assertEquals('privacy:metadata:block_community', $table->get_summary());
+    }
+
+    /**
+     * Test for provider::get_contexts_for_userid().
+     */
+    public function test_get_contexts_for_userid() {
+        global $DB;
+
+        // Test setup.
+        $teacher = $this->getDataGenerator()->create_user();
+        $this->setUser($teacher);
+
+        // Add two community links for the User.
+        $community = (object)[
+            'userid' => $teacher->id,
+            'coursename' => 'Dummy Community Course Name - 1',
+            'coursedescription' => 'Dummy Community Course Description - 1',
+            'courseurl' => 'https://moodle.org/community_courses/Dummy_Community_Course-1',
+            'imageurl' => ''
+        ];
+        $DB->insert_record('block_community', $community);
+
+        $community = (object)[
+            'userid' => $teacher->id,
+            'coursename' => 'Dummy Community Course Name - 2',
+            'coursedescription' => 'Dummy Community Course Description - 2',
+            'courseurl' => 'https://moodle.org/community_courses/Dummy_Community_Course-2',
+            'imageurl' => ''
+        ];
+        $DB->insert_record('block_community', $community);
+
+        // Test the User's retrieved contextlist contains only one context.
+        $contextlist = provider::get_contexts_for_userid($teacher->id);
+        $contexts = $contextlist->get_contexts();
+        $this->assertCount(1, $contexts);
+
+        // Test the User's contexts equal the User's own context.
+        $context = reset($contexts);
+        $this->assertEquals(CONTEXT_USER, $context->contextlevel);
+        $this->assertEquals($teacher->id, $context->instanceid);
+    }
+
+    /**
+     * Test for provider::export_user_data().
+     */
+    public function test_export_user_data() {
+        global $DB;
+
+        // Test setup.
+        $teacher = $this->getDataGenerator()->create_user();
+        $this->setUser($teacher);
+
+        // Add 3 community links for the User.
+        $nocommunities = 3;
+        for ($c = 0; $c < $nocommunities; $c++) {
+            $community = (object)[
+                'userid' => $teacher->id,
+                'coursename' => 'Dummy Community Course Name - ' . $c,
+                'coursedescription' => 'Dummy Community Course Description - ' . $c,
+                'courseurl' => 'https://moodle.org/community_courses/Dummy_Community_Course-' . $c,
+                'imageurl' => ''
+            ];
+            $DB->insert_record('block_community', $community);
+        }
+
+        // Test the created block_community records matches the test number of communities specified.
+        $communities = $DB->get_records('block_community', ['userid' => $teacher->id]);
+        $this->assertCount($nocommunities, $communities);
+
+        // Test the User's retrieved contextlist contains only one context.
+        $contextlist = provider::get_contexts_for_userid($teacher->id);
+        $contexts = $contextlist->get_contexts();
+        $this->assertCount(1, $contexts);
+
+        // Test the User's contexts equal the User's own context.
+        $context = reset($contexts);
+        $this->assertEquals(CONTEXT_USER, $context->contextlevel);
+        $this->assertEquals($teacher->id, $context->instanceid);
+
+        $approvedcontextlist = new approved_contextlist($teacher, 'block_community', $contextlist->get_contextids());
+
+        // Retrieve Calendar Event and Subscriptions data only for this user.
+        provider::export_user_data($approvedcontextlist);
+
+        // Test the block_community data is exported at the User context level.
+        $user = $approvedcontextlist->get_user();
+        $contextuser = context_user::instance($user->id);
+        $writer = writer::with_context($contextuser);
+        $this->assertTrue($writer->has_any_data());
+    }
+
+    /**
+     * Test for provider::delete_data_for_all_users_in_context().
+     */
+    public function test_delete_data_for_all_users_in_context() {
+        global $DB;
+
+        // Test setup.
+        $teacher = $this->getDataGenerator()->create_user();
+        $this->setUser($teacher);
+
+        // Add a community link for the User.
+        $community = (object)[
+            'userid' => $teacher->id,
+            'coursename' => 'Dummy Community Course Name',
+            'coursedescription' => 'Dummy Community Course Description',
+            'courseurl' => 'https://moodle.org/community_courses/Dummy_Community_Course',
+            'imageurl' => ''
+        ];
+        $DB->insert_record('block_community', $community);
+
+        // Test the User's retrieved contextlist contains only one context.
+        $contextlist = provider::get_contexts_for_userid($teacher->id);
+        $contexts = $contextlist->get_contexts();
+        $this->assertCount(1, $contexts);
+
+        // Test the User's contexts equal the User's own context.
+        $context = reset($contexts);
+        $this->assertEquals(CONTEXT_USER, $context->contextlevel);
+        $this->assertEquals($teacher->id, $context->instanceid);
+
+        // Test delete all users content by context.
+        provider::delete_data_for_all_users_in_context($context);
+        $blockcommunity = $DB->get_records('block_community', ['userid' => $teacher->id]);
+        $this->assertCount(0, $blockcommunity);
+    }
+
+    /**
+     * Test for provider::delete_data_for_user().
+     */
+    public function test_delete_data_for_user() {
+        global $DB;
+
+        // Test setup.
+        $teacher1 = $this->getDataGenerator()->create_user();
+        $teacher2 = $this->getDataGenerator()->create_user();
+        $this->setUser($teacher1);
+
+        // Add 3 community links for Teacher 1.
+        $nocommunities = 3;
+        for ($c = 0; $c < $nocommunities; $c++) {
+            $community = (object)[
+                'userid' => $teacher1->id,
+                'coursename' => 'Dummy Community Course Name - ' . $c,
+                'coursedescription' => 'Dummy Community Course Description - ' . $c,
+                'courseurl' => 'https://moodle.org/community_courses/Dummy_Community_Course-' . $c,
+                'imageurl' => ''
+            ];
+            $DB->insert_record('block_community', $community);
+        }
+
+        // Add 1 community link for Teacher 2.
+        $community = (object)[
+            'userid' => $teacher2->id,
+            'coursename' => 'Dummy Community Course Name - Blah',
+            'coursedescription' => 'Dummy Community Course Description - Blah',
+            'courseurl' => 'https://moodle.org/community_courses/Dummy_Community_Course-Blah',
+            'imageurl' => ''
+        ];
+        $DB->insert_record('block_community', $community);
+
+        // Test the created block_community records for Teacher 1 equals test number of communities specified.
+        $communities = $DB->get_records('block_community', ['userid' => $teacher1->id]);
+        $this->assertCount($nocommunities, $communities);
+
+        // Test the created block_community records for Teacher 2 equals 1.
+        $communities = $DB->get_records('block_community', ['userid' => $teacher2->id]);
+        $this->assertCount(1, $communities);
+
+        // Test the deletion of block_community records for Teacher 1 results in zero records.
+        $contextlist = provider::get_contexts_for_userid($teacher1->id);
+        $contexts = $contextlist->get_contexts();
+        $this->assertCount(1, $contexts);
+
+        // Test the User's contexts equal the User's own context.
+        $context = reset($contexts);
+        $this->assertEquals(CONTEXT_USER, $context->contextlevel);
+        $this->assertEquals($teacher1->id, $context->instanceid);
+
+        $approvedcontextlist = new approved_contextlist($teacher1, 'block_community', $contextlist->get_contextids());
+        provider::delete_data_for_user($approvedcontextlist);
+        $communities = $DB->get_records('block_community', ['userid' => $teacher1->id]);
+        $this->assertCount(0, $communities);
+
+
+        // Test that Teacher 2's single block_community record still exists.
+        $contextlist = provider::get_contexts_for_userid($teacher2->id);
+        $contexts = $contextlist->get_contexts();
+        $this->assertCount(1, $contexts);
+
+        // Test the User's contexts equal the User's own context.
+        $context = reset($contexts);
+        $this->assertEquals(CONTEXT_USER, $context->contextlevel);
+        $this->assertEquals($teacher2->id, $context->instanceid);
+
+        $communities = $DB->get_records('block_community', ['userid' => $teacher2->id]);
+        $this->assertCount(1, $communities);
+    }
+
+}
diff --git a/blocks/rss_client/classes/privacy/provider.php b/blocks/rss_client/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..d37f27f
--- /dev/null
@@ -0,0 +1,145 @@
+<?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/>.
+/**
+ * Privacy class for requesting user data.
+ *
+ * @package    block_rss_client
+ * @copyright  2018 Mihail Geshoski <mihail@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace block_rss_client\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+use \core_privacy\local\metadata\collection;
+use \core_privacy\local\request\contextlist;
+use \core_privacy\local\request\approved_contextlist;
+
+/**
+ * Privacy class for requesting user data.
+ *
+ * @package    block_rss_client
+ * @copyright  2018 Mihail Geshoski <mihail@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider {
+
+    /**
+     * Returns meta data about this system.
+     *
+     * @param   collection $collection The initialised collection to add items to.
+     * @return  collection A listing of user data stored through this system.
+     */
+    public static function get_metadata(collection $collection) : collection {
+        $collection->add_database_table('block_rss_client', [
+            'userid' => 'privacy:metadata:block_rss_client:userid',
+            'title' => 'privacy:metadata:block_rss_client:title',
+            'preferredtitle' => 'privacy:metadata:block_rss_client:preferredtitle',
+            'description' => 'privacy:metadata:block_rss_client:description',
+            'shared' => 'privacy:metadata:block_rss_client:shared',
+            'url' => 'privacy:metadata:block_rss_client:url',
+            'skiptime' => 'privacy:metadata:block_rss_client:skiptime',
+            'skipuntil' => 'privacy:metadata:block_rss_client:skipuntil',
+        ], 'privacy:metadata:block_rss_client:tableexplanation');
+        return $collection;
+    }
+
+    /**
+     * Get the list of contexts that contain user information for the specified user.
+     *
+     * @param   int         $userid     The user to search.
+     * @return  contextlist $contextlist  The contextlist containing the list of contexts used in this plugin.
+     */
+    public static function get_contexts_for_userid(int $userid) : contextlist {
+        $sql = "SELECT ctx.id
+                FROM {block_rss_client} brc
+                JOIN {user} u
+                    ON brc.userid = u.id
+                JOIN {context} ctx
+                    ON ctx.instanceid = u.id
+                        AND ctx.contextlevel = :contextlevel
+                WHERE brc.userid = :userid";
+
+        $params = ['userid' => $userid, 'contextlevel' => CONTEXT_USER];
+
+        $contextlist = new contextlist();
+        $contextlist->add_from_sql($sql, $params);
+        return $contextlist;
+    }
+
+    /**
+     * Export all user data for the specified user, in the specified contexts.
+     *
+     * @param approved_contextlist $contextlist The approved contexts to export information for.
+     */
+    public static function export_user_data(approved_contextlist $contextlist) {
+        $results = static::get_records($contextlist->get_user()->id);
+        foreach ($results as $result) {
+            $data = (object) [
+                'title' => $result->title,
+                'preferredtitle' => $result->preferredtitle,
+                'description' => $result->description,
+                'shared' => \core_privacy\local\request\transform::yesno($result->shared),
+                'url' => $result->url
+            ];
+
+            \core_privacy\local\request\writer::with_context($contextlist->current())->export_data([
+                    get_string('pluginname', 'block_rss_client')], $data);
+        }
+    }
+
+    /**
+     * Delete all use data which matches the specified deletion_criteria.
+     *
+     * @param   context $context A user context.
+     */
+    public static function delete_data_for_all_users_in_context(\context $context) {
+        static::delete_data($context->instanceid);
+    }
+
+    /**
+     * Delete all user data for the specified user, in the specified contexts.
+     *
+     * @param   approved_contextlist    $contextlist    The approved contexts and user information to delete information for.
+     */
+    public static function delete_data_for_user(approved_contextlist $contextlist) {
+        static::delete_data($contextlist->get_user()->id);
+    }
+
+    /**
+     * Delete data related to a userid.
+     *
+     * @param  int $userid The user ID
+     */
+    protected static function delete_data($userid) {
+        global $DB;
+
+        $DB->delete_records('block_rss_client', ['userid' => $userid]);
+    }
+
+    /**
+     * Get records related to this plugin and user.
+     *
+     * @param  int $userid The user ID
+     * @return array An array of records.
+     */
+    protected static function get_records($userid) {
+        global $DB;
+
+        return $DB->get_records('block_rss_client', ['userid' => $userid]);
+    }
+}
index e606979..3045074 100644 (file)
@@ -62,6 +62,15 @@ $string['nofeeds'] = 'There are no RSS feeds defined for this site.';
 $string['numentries'] = 'Entries per feed';
 $string['pickfeed'] = 'Pick a news feed';
 $string['pluginname'] = 'Remote RSS feeds';
+$string['privacy:metadata:block_rss_client:description'] = 'The description of the RSS feed.';
+$string['privacy:metadata:block_rss_client:preferredtitle'] = 'The preferred (custom) title of the RSS feed.';
+$string['privacy:metadata:block_rss_client:shared'] = 'If the RSS feed is available to all courses.';
+$string['privacy:metadata:block_rss_client:skiptime'] = 'The defined time in seconds that the cron will wait between attempts to retry failing RSS feeds.';
+$string['privacy:metadata:block_rss_client:skipuntil'] = 'The maximum defined time that the cron will attempt to open failing RSS feeds.';
+$string['privacy:metadata:block_rss_client:tableexplanation'] = 'RSS block information is stored here.';
+$string['privacy:metadata:block_rss_client:title'] = 'The title of the RSS feed.';
+$string['privacy:metadata:block_rss_client:url'] = 'The URL of the RSS feed.';
+$string['privacy:metadata:block_rss_client:userid'] = 'The ID of the user that added the RSS feed.';
 $string['remotenewsfeed'] = 'Remote news feed';
 $string['rss_client:addinstance'] = 'Add a new remote RSS feeds block';
 $string['rss_client:createprivatefeeds'] = 'Create private RSS feeds';
diff --git a/blocks/rss_client/tests/privacy_test.php b/blocks/rss_client/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..dfd37c1
--- /dev/null
@@ -0,0 +1,145 @@
+<?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/>.
+/**
+ * Base class for unit tests for block_rss_client.
+ *
+ * @package    block_rss_client
+ * @copyright  2018 Mihail Geshoski <mihail@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use \core_privacy\tests\provider_testcase;
+
+/**
+ * Unit tests for blocks\rss_client\classes\privacy\provider.php
+ *
+ * @copyright  2018 Mihail Geshoski <mihail@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class block_rss_client_testcase extends provider_testcase {
+
+    /**
+     * Basic setup for these tests.
+     */
+    public function setUp() {
+        $this->resetAfterTest(true);
+    }
+
+    /**
+     * Test getting the context for the user ID related to this plugin.
+     */
+    public function test_get_contexts_for_userid() {
+
+        $user = $this->getDataGenerator()->create_user();
+        $context = context_user::instance($user->id);
+
+        $this->add_rss_feed($user);
+
+        $contextlist = \block_rss_client\privacy\provider::get_contexts_for_userid($user->id);
+
+        $this->assertEquals($context, $contextlist->current());
+    }
+
+    /**
+     * Test that data is exported correctly for this plugin.
+     */
+    public function test_export_user_data() {
+
+        $user = $this->getDataGenerator()->create_user();
+        $context = context_user::instance($user->id);
+
+        $this->add_rss_feed($user);
+
+        $writer = \core_privacy\local\request\writer::with_context($context);
+        $this->assertFalse($writer->has_any_data());
+        $this->export_context_data_for_user($user->id, $context, 'block_rss_client');
+
+        $data = $writer->get_data([get_string('pluginname', 'block_rss_client')]);
+        $this->assertEquals('BBC News - World', $data->title);
+        $this->assertEquals('World News', $data->preferredtitle);
+        $this->assertEquals('Description: BBC News - World', $data->description);
+        $this->assertEquals(get_string('no'), $data->shared);
+        $this->assertEquals('http://feeds.bbci.co.uk/news/world/rss.xml?edition=uk', $data->url);
+    }
+
+    /**
+     * Test that user data is deleted using the context.
+     */
+    public function test_delete_data_for_all_users_in_context() {
+        global $DB;
+
+        $user = $this->getDataGenerator()->create_user();
+        $context = context_user::instance($user->id);
+
+        $this->add_rss_feed($user);
+
+        // Check that we have an entry.
+        $rssfeeds = $DB->get_records('block_rss_client', ['userid' => $user->id]);
+        $this->assertCount(1, $rssfeeds);
+
+        \block_rss_client\privacy\provider::delete_data_for_all_users_in_context($context);
+
+        // Check that it has now been deleted.
+        $rssfeeds = $DB->get_records('block_rss_client', ['userid' => $user->id]);
+        $this->assertCount(0, $rssfeeds);
+    }
+
+    /**
+     * Test that user data is deleted for this user.
+     */
+    public function test_delete_data_for_user() {
+        global $DB;
+
+        $user = $this->getDataGenerator()->create_user();
+        $context = context_user::instance($user->id);
+
+        $this->add_rss_feed($user);
+
+        // Check that we have an entry.
+        $rssfeeds = $DB->get_records('block_rss_client', ['userid' => $user->id]);
+        $this->assertCount(1, $rssfeeds);
+
+        $approvedlist = new \core_privacy\local\request\approved_contextlist($user, 'block_rss_feed',
+                [$context->id]);
+        \block_rss_client\privacy\provider::delete_data_for_user($approvedlist);
+
+        // Check that it has now been deleted.
+        $rssfeeds = $DB->get_records('block_rss_client', ['userid' => $user->id]);
+        $this->assertCount(0, $rssfeeds);
+    }
+
+    /**
+     * Add dummy rss feed.
+     *
+     * @param object $user User object
+     */
+    private function add_rss_feed($user) {
+        global $DB;
+
+        $rssfeeddata = array(
+            'userid' => $user->id,
+            'title' => 'BBC News - World',
+            'preferredtitle' => 'World News',
+            'description' => 'Description: BBC News - World',
+            'shared' => 0,
+            'url' => 'http://feeds.bbci.co.uk/news/world/rss.xml?edition=uk',
+        );
+
+        $DB->insert_record('block_rss_client', $rssfeeddata);
+    }
+}
index 55e7529..cbd2101 100644 (file)
@@ -64,6 +64,10 @@ class cohort_summary_exporter extends \core\external\exporter {
             ),
             'visible' => array(
                 'type' => PARAM_BOOL,
+            ),
+            'theme' => array(
+                'type' => PARAM_THEME,
+                'null' => NULL_ALLOWED
             )
         );
     }
index d4abb88..83df3a7 100644 (file)
@@ -32,6 +32,7 @@ class cohort_edit_form extends moodleform {
      * Define the cohort edit form
      */
     public function definition() {
+        global $CFG;
 
         $mform = $this->_form;
         $editoroptions = $this->_customdata['editoroptions'];
@@ -54,6 +55,11 @@ class cohort_edit_form extends moodleform {
         $mform->addElement('editor', 'description_editor', get_string('description', 'cohort'), null, $editoroptions);
         $mform->setType('description_editor', PARAM_RAW);
 
+        if (!empty($CFG->allowcohortthemes)) {
+            $themes = array_merge(array('' => get_string('forceno')), cohort_get_list_of_themes());
+            $mform->addElement('select', 'theme', get_string('forcetheme'), $themes);
+        }
+
         $mform->addElement('hidden', 'id');
         $mform->setType('id', PARAM_INT);
 
index 7c38a8f..08f62ee 100644 (file)
@@ -54,6 +54,10 @@ class core_cohort_external extends external_api {
                             'description' => new external_value(PARAM_RAW, 'cohort description', VALUE_OPTIONAL),
                             'descriptionformat' => new external_format_value('description', VALUE_DEFAULT),
                             'visible' => new external_value(PARAM_BOOL, 'cohort visible', VALUE_OPTIONAL, true),
+                            'theme' => new external_value(PARAM_THEME,
+                                'the cohort theme. The allowcohortthemes setting must be enabled on Moodle',
+                                VALUE_OPTIONAL
+                            ),
                         )
                     )
                 )
@@ -74,6 +78,8 @@ class core_cohort_external extends external_api {
 
         $params = self::validate_parameters(self::create_cohorts_parameters(), array('cohorts' => $cohorts));
 
+        $availablethemes = cohort_get_list_of_themes();
+
         $transaction = $DB->start_delegated_transaction();
 
         $syscontext = context_system::instance();
@@ -107,6 +113,15 @@ class core_cohort_external extends external_api {
             self::validate_context($context);
             require_capability('moodle/cohort:manage', $context);
 
+            // Make sure theme is valid.
+            if (isset($cohort->theme)) {
+                if (!empty($CFG->allowcohortthemes)) {
+                    if (empty($availablethemes[$cohort->theme])) {
+                        throw new moodle_exception('errorinvalidparam', 'webservice', '', 'theme');
+                    }
+                }
+            }
+
             // Validate format.
             $cohort->descriptionformat = external_validate_format($cohort->descriptionformat);
             $cohort->id = cohort_add_cohort($cohort);
@@ -137,6 +152,7 @@ class core_cohort_external extends external_api {
                     'description' => new external_value(PARAM_RAW, 'cohort description'),
                     'descriptionformat' => new external_format_value('description'),
                     'visible' => new external_value(PARAM_BOOL, 'cohort visible'),
+                    'theme' => new external_value(PARAM_THEME, 'cohort theme', VALUE_OPTIONAL),
                 )
             )
         );
@@ -223,7 +239,7 @@ class core_cohort_external extends external_api {
      * @since Moodle 2.5
      */
     public static function get_cohorts($cohortids = array()) {
-        global $DB;
+        global $DB, $CFG;
 
         $params = self::validate_parameters(self::get_cohorts_parameters(), array('cohortids' => $cohortids));
 
@@ -245,6 +261,11 @@ class core_cohort_external extends external_api {
                 throw new required_capability_exception($context, 'moodle/cohort:view', 'nopermissions', '');
             }
 
+            // Only return theme when $CFG->allowcohortthemes is enabled.
+            if (!empty($cohort->theme) && empty($CFG->allowcohortthemes)) {
+                $cohort->theme = null;
+            }
+
             list($cohort->description, $cohort->descriptionformat) =
                 external_format_text($cohort->description, $cohort->descriptionformat,
                         $context->id, 'cohort', 'description', $cohort->id);
@@ -271,6 +292,7 @@ class core_cohort_external extends external_api {
                     'description' => new external_value(PARAM_RAW, 'cohort description'),
                     'descriptionformat' => new external_format_value('description'),
                     'visible' => new external_value(PARAM_BOOL, 'cohort visible'),
+                    'theme' => new external_value(PARAM_THEME, 'cohort theme', VALUE_OPTIONAL),
                 )
             )
         );
@@ -367,6 +389,12 @@ class core_cohort_external extends external_api {
         $cohorts = array();
         foreach ($results as $key => $cohort) {
             $cohortcontext = context::instance_by_id($cohort->contextid);
+
+            // Only return theme when $CFG->allowcohortthemes is enabled.
+            if (!empty($cohort->theme) && empty($CFG->allowcohortthemes)) {
+                $cohort->theme = null;
+            }
+
             if (!isset($cohort->description)) {
                 $cohort->description = '';
             }
@@ -399,6 +427,7 @@ class core_cohort_external extends external_api {
                     'description' => new external_value(PARAM_RAW, 'cohort description'),
                     'descriptionformat' => new external_format_value('description'),
                     'visible' => new external_value(PARAM_BOOL, 'cohort visible'),
+                    'theme' => new external_value(PARAM_THEME, 'cohort theme', VALUE_OPTIONAL),
                 ))
             )
         ));
@@ -432,6 +461,10 @@ class core_cohort_external extends external_api {
                             'description' => new external_value(PARAM_RAW, 'cohort description', VALUE_OPTIONAL),
                             'descriptionformat' => new external_format_value('description', VALUE_DEFAULT),
                             'visible' => new external_value(PARAM_BOOL, 'cohort visible', VALUE_OPTIONAL),
+                            'theme' => new external_value(PARAM_THEME,
+                                'the cohort theme. The allowcohortthemes setting must be enabled on Moodle',
+                                VALUE_OPTIONAL
+                            ),
                         )
                     )
                 )
@@ -452,6 +485,8 @@ class core_cohort_external extends external_api {
 
         $params = self::validate_parameters(self::update_cohorts_parameters(), array('cohorts' => $cohorts));
 
+        $availablethemes = cohort_get_list_of_themes();
+
         $transaction = $DB->start_delegated_transaction();
         $syscontext = context_system::instance();
 
@@ -490,6 +525,14 @@ class core_cohort_external extends external_api {
                 require_capability('moodle/cohort:manage', $context);
             }
 
+            // Make sure theme is valid.
+            if (!empty($cohort->theme) && !empty($CFG->allowcohortthemes)) {
+                if (empty($availablethemes[$cohort->theme])) {
+                    $debuginfo = 'The following cohort theme is not installed on this site: '.$cohort->theme;
+                    throw new moodle_exception('errorinvalidparam', 'webservice', '', 'theme', $debuginfo);
+                }
+            }
+
             if (!empty($cohort->description)) {
                 $cohort->descriptionformat = external_validate_format($cohort->descriptionformat);
             }
index f3c3a16..09d17dc 100644 (file)
@@ -38,7 +38,7 @@ define('COHORT_WITH_NOTENROLLED_MEMBERS_ONLY', 23);
  * @return int new cohort id
  */
 function cohort_add_cohort($cohort) {
-    global $DB;
+    global $DB, $CFG;
 
     if (!isset($cohort->name)) {
         throw new coding_exception('Missing cohort name in cohort_add_cohort().');
@@ -58,6 +58,12 @@ function cohort_add_cohort($cohort) {
     if (empty($cohort->component)) {
         $cohort->component = '';
     }
+    if (empty($CFG->allowcohortthemes) && isset($cohort->theme)) {
+        unset($cohort->theme);
+    }
+    if (empty($cohort->theme) || empty($CFG->allowcohortthemes)) {
+        $cohort->theme = '';
+    }
     if (!isset($cohort->timecreated)) {
         $cohort->timecreated = time();
     }
@@ -83,11 +89,15 @@ function cohort_add_cohort($cohort) {
  * @return void
  */
 function cohort_update_cohort($cohort) {
-    global $DB;
+    global $DB, $CFG;
     if (property_exists($cohort, 'component') and empty($cohort->component)) {
         // prevent NULLs
         $cohort->component = '';
     }
+    // Only unset the cohort theme if allowcohortthemes is enabled to prevent the value from being overwritten.
+    if (empty($CFG->allowcohortthemes) && isset($cohort->theme)) {
+        unset($cohort->theme);
+    }
     $cohort->timemodified = time();
     $DB->update_record('cohort', $cohort);
 
@@ -478,6 +488,47 @@ function cohort_get_all_cohorts($page = 0, $perpage = 25, $search = '') {
     return array('totalcohorts' => $totalcohorts, 'cohorts' => $cohorts, 'allcohorts' => $allcohorts);
 }
 
+/**
+ * Get all the cohorts where the given user is member of.
+ *
+ * @param int $userid
+ * @return array Array
+ */
+function cohort_get_user_cohorts($userid) {
+    global $DB;
+
+    $sql = 'SELECT c.*
+              FROM {cohort} c
+              JOIN {cohort_members} cm ON c.id = cm.cohortid
+             WHERE cm.userid = ? AND c.visible = 1';
+    return $DB->get_records_sql($sql, array($userid));
+}
+
+/**
+ * Get the user cohort theme.
+ *
+ * If the user is member of one cohort, will return this cohort theme (if defined).
+ * If the user is member of 2 or more cohorts, will return the theme if all them have the same
+ * theme (null themes are ignored).
+ *
+ * @param int $userid
+ * @return string|null
+ */
+function cohort_get_user_cohort_theme($userid) {
+    $cohorts = cohort_get_user_cohorts($userid);
+    $theme = null;
+    foreach ($cohorts as $cohort) {
+        if (!empty($cohort->theme)) {
+            if (null === $theme) {
+                $theme = $cohort->theme;
+            } else if ($theme != $cohort->theme) {
+                return null;
+            }
+        }
+    }
+    return $theme;
+}
+
 /**
  * Returns list of contexts where cohorts are present but current user does not have capability to view/manage them.
  *
@@ -568,3 +619,19 @@ function core_cohort_inplace_editable($itemtype, $itemid, $newvalue) {
         return \core_cohort\output\cohortidnumber::update($itemid, $newvalue);
     }
 }
+
+/**
+ * Returns a list of valid themes which can be displayed in a selector.
+ *
+ * @return array as (string)themename => (string)get_string_theme
+ */
+function cohort_get_list_of_themes() {
+    $themes = array();
+    $allthemes = get_list_of_themes();
+    foreach ($allthemes as $key => $theme) {
+        if (empty($theme->hidefromselector)) {
+            $themes[$key] = get_string('pluginname', 'theme_'.$theme->name);
+        }
+    }
+    return $themes;
+}
\ No newline at end of file
index a400b01..2cc5074 100644 (file)
@@ -50,7 +50,7 @@ Feature: A privileged user can create cohorts using a CSV file
     And the "class" attribute of "cohort name 5" "table_row" should contain "dimmed_text"
     And ".dimmed_text" "css_element" should not exist in the "cohort name 6" "table_row"
 
-  @javascript
+  @javascript @_file_upload
   Scenario: Upload cohorts with default category context as admin
     When I log in as "admin"
     And I navigate to "Cohorts" node in "Site administration > Users > Accounts"
@@ -81,7 +81,7 @@ Feature: A privileged user can create cohorts using a CSV file
       | Cat 2         | cohort name 5 | cohortid5 |                   | 0           | Created manually |
       | Cat 3         | cohort name 6 | cohortid6 |                   | 0           | Created manually |
 
-  @javascript
+  @javascript @_file_upload
   Scenario: Upload cohorts with default category context as manager
     Given the following "users" exist:
       | username | firstname | lastname | email                  |
@@ -107,7 +107,7 @@ Feature: A privileged user can create cohorts using a CSV file
     And I press "Upload cohorts"
     And I should see "Uploaded 6 cohorts"
 
-  @javascript
+  @javascript @_file_upload
   Scenario: Upload cohorts with conflicting id number
     Given the following "cohorts" exist:
       | name   | idnumber  |
@@ -128,7 +128,7 @@ Feature: A privileged user can create cohorts using a CSV file
       | cohort name 6 | cohortid6 |  | Cat 3 |  |
     And "Upload cohorts" "button" should not exist
 
-  @javascript
+  @javascript @_file_upload
   Scenario: Upload cohorts with different ways of specifying context
     When I log in as "admin"
     And I navigate to "Cohorts" node in "Site administration > Users > Accounts"
@@ -161,3 +161,35 @@ Feature: A privileged user can create cohorts using a CSV file
     And I should not see "not found or you"
     And I press "Upload cohorts"
     And I should see "Uploaded 5 cohorts"
+
+  @javascript @_file_upload
+  Scenario: Upload cohorts with theme
+    When I log in as "admin"
+    And I navigate to "Cohorts" node in "Site administration > Users > Accounts"
+    And I follow "Upload cohorts"
+    And I upload "cohort/tests/fixtures/uploadcohorts4.csv" file to "File" filemanager
+    And I click on "Preview" "button"
+    Then the following should exist in the "previewuploadedcohorts" table:
+      | name          | idnumber  | description       | Context       | visible | theme  | Status |
+      | cohort name 1 | cohortid1 | first description | System        | 1       | boost  |        |
+      | cohort name 2 | cohortid2 |                   | System        | 1       |        |        |
+      | cohort name 3 | cohortid3 |                   | Miscellaneous | 0       | boost  |        |
+      | cohort name 4 | cohortid4 |                   | Cat 1         | 1       | clean  |        |
+      | cohort name 5 | cohortid5 |                   | Cat 2         | 0       |        |        |
+      | cohort name 6 | cohortid6 |                   | Cat 3         | 1       | clean  |        |
+    And I press "Upload cohorts"
+    And I should see "Uploaded 6 cohorts"
+    And I press "Continue"
+    And the following should exist in the "cohorts" table:
+      | Name          | Cohort ID | Description | Cohort size | Source           |
+      | cohort name 1 | cohortid1 | first description | 0           | Created manually |
+      | cohort name 2 | cohortid2 |             | 0           | Created manually |
+    And I follow "All cohorts"
+    And the following should exist in the "cohorts" table:
+      | Category      | Name          | Cohort ID | Description       | Cohort size | Source           |
+      | System        | cohort name 1 | cohortid1 | first description | 0           | Created manually |
+      | System        | cohort name 2 | cohortid2 |                   | 0           | Created manually |
+      | Miscellaneous | cohort name 3 | cohortid3 |                   | 0           | Created manually |
+      | Cat 1         | cohort name 4 | cohortid4 |                   | 0           | Created manually |
+      | Cat 2         | cohort name 5 | cohortid5 |                   | 0           | Created manually |
+      | Cat 3         | cohort name 6 | cohortid6 |                   | 0           | Created manually |
index 04b37b1..e92c264 100644 (file)
@@ -61,6 +61,7 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $this->assertEquals($cohort->descriptionformat, $newcohort->descriptionformat);
         $this->assertNotEmpty($newcohort->timecreated);
         $this->assertSame($newcohort->component, '');
+        $this->assertSame($newcohort->theme, '');
         $this->assertSame($newcohort->timecreated, $newcohort->timemodified);
     }
 
@@ -142,6 +143,7 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $this->assertSame($cohort->descriptionformat, $newcohort->descriptionformat);
         $this->assertSame($cohort->timecreated, $newcohort->timecreated);
         $this->assertSame($cohort->component, $newcohort->component);
+        $this->assertSame($newcohort->theme, '');
         $this->assertGreaterThan($newcohort->timecreated, $newcohort->timemodified);
         $this->assertLessThanOrEqual(time(), $newcohort->timemodified);
     }
@@ -158,6 +160,7 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $cohort->idnumber = 'testid';
         $cohort->description = 'test cohort desc';
         $cohort->descriptionformat = FORMAT_HTML;
+        $cohort->theme = '';
         $id = cohort_add_cohort($cohort);
         $this->assertNotEmpty($id);
 
@@ -168,6 +171,8 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
 
         // Peform the update.
         cohort_update_cohort($cohort);
+        // Add again theme property to the cohort object for comparing it to the event snapshop.
+        $cohort->theme = '';
 
         $events = $sink->get_events();
         $sink->close();
@@ -651,4 +656,87 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $result = cohort_get_available_cohorts($course1ctx, COHORT_ALL, 0, 0, '');
         $this->assertEquals(array($cohort1->id, $cohort2->id, $cohort4->id), array_keys($result));
     }
+
+    /**
+     * Create a cohort with allowcohortthemes enabled/disabled.
+     */
+    public function test_cohort_add_theme_cohort() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Theme is added when allowcohortthemes is enabled.
+        set_config('allowcohortthemes', 1);
+        set_config('theme', 'boost');
+
+        $systemctx = context_system::instance();
+        $cohort1 = $this->getDataGenerator()->create_cohort(array('contextid' => $systemctx->id, 'name' => 'test cohort 1',
+            'idnumber' => 'testid1', 'description' => 'test cohort desc', 'descriptionformat' => FORMAT_HTML, 'theme' => 'clean'));
+
+        $id = cohort_add_cohort($cohort1);
+        $this->assertNotEmpty($id);
+        $newcohort = $DB->get_record('cohort', array('id' => $id));
+        $this->assertEquals($cohort1->contextid, $newcohort->contextid);
+        $this->assertSame($cohort1->name, $newcohort->name);
+        $this->assertSame($cohort1->description, $newcohort->description);
+        $this->assertEquals($cohort1->descriptionformat, $newcohort->descriptionformat);
+        $this->assertNotEmpty($newcohort->theme);
+        $this->assertSame($cohort1->theme, $newcohort->theme);
+        $this->assertNotEmpty($newcohort->timecreated);
+        $this->assertSame($newcohort->component, '');
+        $this->assertSame($newcohort->timecreated, $newcohort->timemodified);
+
+        // Theme is not added when allowcohortthemes is disabled.
+        set_config('allowcohortthemes', 0);
+
+        $cohort2 = $this->getDataGenerator()->create_cohort(array('contextid' => $systemctx->id, 'name' => 'test cohort 2',
+            'idnumber' => 'testid2', 'description' => 'test cohort desc', 'descriptionformat' => FORMAT_HTML, 'theme' => 'clean'));
+
+        $id = cohort_add_cohort($cohort2);
+        $this->assertNotEmpty($id);
+        $newcohort = $DB->get_record('cohort', array('id' => $id));
+        $this->assertSame($cohort2->name, $newcohort->name);
+        $this->assertEmpty($newcohort->theme);
+    }
+
+    /**
+     * Update a cohort with allowcohortthemes enabled/disabled.
+     */
+    public function test_cohort_update_theme_cohort() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Enable cohort themes.
+        set_config('allowcohortthemes', 1);
+        set_config('theme', 'boost');
+
+        $systemctx = context_system::instance();
+        $cohort1 = $this->getDataGenerator()->create_cohort(array('contextid' => $systemctx->id, 'name' => 'test cohort 1',
+            'idnumber' => 'testid1', 'description' => 'test cohort desc', 'descriptionformat' => FORMAT_HTML, 'theme' => 'clean'));
+        $id = cohort_add_cohort($cohort1);
+        $this->assertNotEmpty($id);
+
+        // Theme is updated when allowcohortthemes is enabled.
+        $cohort1 = $DB->get_record('cohort', array('id' => $id));
+        $cohort1->name = 'test cohort 1 updated';
+        $cohort1->theme = 'more';
+        cohort_update_cohort($cohort1);
+        $updatedcohort = $DB->get_record('cohort', array('id' => $id));
+        $this->assertEquals($cohort1->contextid, $updatedcohort->contextid);
+        $this->assertSame($cohort1->name, $updatedcohort->name);
+        $this->assertSame($cohort1->description, $updatedcohort->description);
+        $this->assertNotEmpty($updatedcohort->theme);
+        $this->assertSame($cohort1->theme, $updatedcohort->theme);
+
+        // Theme is not updated neither overwritten when allowcohortthemes is disabled.
+        set_config('allowcohortthemes', 0);
+        $cohort2 = $DB->get_record('cohort', array('id' => $id));
+        $cohort2->theme = 'clean';
+        cohort_update_cohort($cohort2);
+        $updatedcohort = $DB->get_record('cohort', array('id' => $id));
+        $this->assertEquals($cohort2->contextid, $updatedcohort->contextid);
+        $this->assertNotEmpty($updatedcohort->theme);
+        $this->assertSame($cohort1->theme, $updatedcohort->theme);
+    }
 }
index 85b2f02..a6717b8 100644 (file)
@@ -42,6 +42,8 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
 
         $this->resetAfterTest(true);
 
+        set_config('allowcohortthemes', 1);
+
         $contextid = context_system::instance()->id;
         $category = $this->getDataGenerator()->create_category();
 
@@ -49,7 +51,8 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
             'categorytype' => array('type' => 'id', 'value' => $category->id),
             'name' => 'cohort test 1',
             'idnumber' => 'cohorttest1',
-            'description' => 'This is a description for cohorttest1'
+            'description' => 'This is a description for cohorttest1',
+            'theme' => 'clean'
             );
 
         $cohort2 = array(
@@ -68,6 +71,14 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
             );
         $roleid = $this->assignUserCapability('moodle/cohort:manage', $contextid);
 
+        $cohort4 = array(
+            'categorytype' => array('type' => 'id', 'value' => $category->id),
+            'name' => 'cohort test 4',
+            'idnumber' => 'cohorttest4',
+            'description' => 'This is a description for cohorttest4',
+            'theme' => 'clean'
+            );
+
         // Call the external function.
         $this->setCurrentTimeStart();
         $createdcohorts = core_cohort_external::create_cohorts(array($cohort1, $cohort2));
@@ -85,11 +96,15 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
                 $this->assertEquals($dbcohort->name, $cohort1['name']);
                 $this->assertEquals($dbcohort->description, $cohort1['description']);
                 $this->assertEquals($dbcohort->visible, 1); // Field was not specified, ensure it is visible by default.
+                // As $CFG->allowcohortthemes is enabled, theme must be initialised.
+                $this->assertEquals($dbcohort->theme, $cohort1['theme']);
             } else if ($createdcohort['idnumber'] == $cohort2['idnumber']) {
                 $this->assertEquals($dbcohort->contextid, context_system::instance()->id);
                 $this->assertEquals($dbcohort->name, $cohort2['name']);
                 $this->assertEquals($dbcohort->description, $cohort2['description']);
                 $this->assertEquals($dbcohort->visible, $cohort2['visible']);
+                // Although $CFG->allowcohortthemes is enabled, no theme is defined for this cohort.
+                $this->assertEquals($dbcohort->theme, '');
             } else {
                 $this->fail('Unrecognised cohort found');
             }
@@ -97,6 +112,23 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
             $this->assertTimeCurrent($dbcohort->timemodified);
         }
 
+        // Call when $CFG->allowcohortthemes is disabled.
+        set_config('allowcohortthemes', 0);
+        $createdcohorts = core_cohort_external::create_cohorts(array($cohort4));
+        $createdcohorts = external_api::clean_returnvalue(core_cohort_external::create_cohorts_returns(), $createdcohorts);
+        foreach ($createdcohorts as $createdcohort) {
+            $dbcohort = $DB->get_record('cohort', array('id' => $createdcohort['id']));
+            if ($createdcohort['idnumber'] == $cohort4['idnumber']) {
+                $conid = $DB->get_field('context', 'id', array('instanceid' => $cohort4['categorytype']['value'],
+                        'contextlevel' => CONTEXT_COURSECAT));
+                $this->assertEquals($dbcohort->contextid, $conid);
+                $this->assertEquals($dbcohort->name, $cohort4['name']);
+                $this->assertEquals($dbcohort->description, $cohort4['description']);
+                $this->assertEquals($dbcohort->visible, 1); // Field was not specified, ensure it is visible by default.
+                $this->assertEquals($dbcohort->theme, ''); // As $CFG->allowcohortthemes is disabled, theme must be empty.
+            }
+        }
+
         // Call without required capability.
         $this->unassignUserCapability('moodle/cohort:manage', $contextid, $roleid);
         $createdcohorts = core_cohort_external::create_cohorts(array($cohort3));
@@ -143,11 +175,14 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
 
         $this->resetAfterTest(true);
 
+        set_config('allowcohortthemes', 1);
+
         $cohort1 = array(
             'contextid' => 1,
             'name' => 'cohortnametest1',
             'idnumber' => 'idnumbertest1',
-            'description' => 'This is a description for cohort 1'
+            'description' => 'This is a description for cohort 1',
+            'theme' => 'clean'
             );
         $cohort1 = self::getDataGenerator()->create_cohort($cohort1);
         $cohort2 = self::getDataGenerator()->create_cohort();
@@ -168,6 +203,7 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
                 $this->assertEquals($cohort1->name, $enrolledcohort['name']);
                 $this->assertEquals($cohort1->description, $enrolledcohort['description']);
                 $this->assertEquals($cohort1->visible, $enrolledcohort['visible']);
+                $this->assertEquals($cohort1->theme, $enrolledcohort['theme']);
             }
         }
 
@@ -181,6 +217,17 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
 
         // Check we retrieve the good total number of enrolled cohorts + no error on capability.
         $this->assertEquals(2, count($returnedcohorts));
+
+        // Check when allowcohortstheme is disabled, theme is not returned.
+        set_config('allowcohortthemes', 0);
+        $returnedcohorts = core_cohort_external::get_cohorts(array(
+            $cohort1->id));
+        $returnedcohorts = external_api::clean_returnvalue(core_cohort_external::get_cohorts_returns(), $returnedcohorts);
+        foreach ($returnedcohorts as $enrolledcohort) {
+            if ($enrolledcohort['idnumber'] == $cohort1->idnumber) {
+                $this->assertNull($enrolledcohort['theme']);
+            }
+        }
     }
 
     /**
@@ -193,6 +240,8 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
 
         $this->resetAfterTest(true);
 
+        set_config('allowcohortthemes', 0);
+
         $cohort1 = self::getDataGenerator()->create_cohort(array('visible' => 0));
 
         $cohort1 = array(
@@ -200,7 +249,8 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
             'categorytype' => array('type' => 'id', 'value' => '1'),
             'name' => 'cohortnametest1',
             'idnumber' => 'idnumbertest1',
-            'description' => 'This is a description for cohort 1'
+            'description' => 'This is a description for cohort 1',
+            'theme' => 'clean'
             );
 
         $context = context_system::instance();
@@ -217,6 +267,7 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals($dbcohort->idnumber, $cohort1['idnumber']);
         $this->assertEquals($dbcohort->description, $cohort1['description']);
         $this->assertEquals($dbcohort->visible, 0);
+        $this->assertEmpty($dbcohort->theme);
 
         // Since field 'visible' was added in 2.8, make sure that update works correctly with and without this parameter.
         core_cohort_external::update_cohorts(array($cohort1 + array('visible' => 1)));
@@ -226,6 +277,18 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
         $dbcohort = $DB->get_record('cohort', array('id' => $cohort1['id']));
         $this->assertEquals(1, $dbcohort->visible);
 
+        // Call when $CFG->allowcohortthemes is enabled.
+        set_config('allowcohortthemes', 1);
+        core_cohort_external::update_cohorts(array($cohort1 + array('theme' => 'clean')));
+        $dbcohort = $DB->get_record('cohort', array('id' => $cohort1['id']));
+        $this->assertEquals('clean', $dbcohort->theme);
+
+        // Call when $CFG->allowcohortthemes is disabled.
+        set_config('allowcohortthemes', 0);
+        core_cohort_external::update_cohorts(array($cohort1 + array('theme' => 'more')));
+        $dbcohort = $DB->get_record('cohort', array('id' => $cohort1['id']));
+        $this->assertEquals('clean', $dbcohort->theme);
+
         // Call without required capability.
         $this->unassignUserCapability('moodle/cohort:manage', $context->id, $roleid);
         core_cohort_external::update_cohorts(array($cohort1));
diff --git a/cohort/tests/fixtures/uploadcohorts4.csv b/cohort/tests/fixtures/uploadcohorts4.csv
new file mode 100644 (file)
index 0000000..1aba4cc
--- /dev/null
@@ -0,0 +1,7 @@
+name,idnumber,description,category,visible,theme
+cohort name 1,cohortid1,first description,,,boost
+cohort name 2,cohortid2,,,,
+cohort name 3,cohortid3,,Miscellaneous,no,boost
+cohort name 4,cohortid4,,CAT1,yes,clean
+cohort name 5,cohortid5,,CAT2,0,
+cohort name 6,cohortid6,,CAT3,1,clean
index a7ee3a8..dddb26e 100644 (file)
@@ -359,7 +359,7 @@ class cohort_upload_form extends moodleform {
         $columns = $cir->get_columns();
 
         // Check that columns include 'name' and warn about extra columns.
-        $allowedcolumns = array('contextid', 'name', 'idnumber', 'description', 'descriptionformat', 'visible');
+        $allowedcolumns = array('contextid', 'name', 'idnumber', 'description', 'descriptionformat', 'visible', 'theme');
         $additionalcolumns = array('context', 'category', 'category_id', 'category_idnumber', 'category_path');
         $displaycolumns = array();
         $extracolumns = array();
@@ -424,6 +424,13 @@ class cohort_upload_form extends moodleform {
                 $cohorts[$rownum]['errors'][] = new lang_string('namefieldempty', 'cohort');
             }
 
+            if (!empty($hash['theme']) && !empty($CFG->allowcohortthemes)) {
+                $availablethemes = cohort_get_list_of_themes();
+                if (empty($availablethemes[$hash['theme']])) {
+                    $cohorts[$rownum]['errors'][] = new lang_string('invalidtheme', 'cohort');
+                }
+            }
+
             $cohorts[$rownum]['data'] = array_intersect_key($hash, $cohorts[0]['data']);
             $haserrors = $haserrors || !empty($cohorts[$rownum]['errors']);
             $haswarnings = $haswarnings || !empty($cohorts[$rownum]['warnings']);
@@ -466,6 +473,9 @@ class cohort_upload_form extends moodleform {
                         $hash[$key] = clean_param($value, PARAM_BOOL) ? 1 : 0;
                     }
                     break;
+                case 'theme':
+                    $hash[$key] = core_text::substr(clean_param($value, PARAM_TEXT), 0, 50);
+                    break;
             }
         }
     }
index 5308052..e3156b0 100644 (file)
@@ -38,7 +38,7 @@ $CFG = new stdClass();
 // will be stored.  This database must already have been created         //
 // and a username/password created to access it.                         //
 
-$CFG->dbtype    = 'pgsql';      // 'pgsql', 'mariadb', 'mysqli', 'mssql', 'sqlsrv' or 'oci'
+$CFG->dbtype    = 'pgsql';      // 'pgsql', 'mariadb', 'mysqli', 'sqlsrv' or 'oci'
 $CFG->dblibrary = 'native';     // 'native' only at the moment
 $CFG->dbhost    = 'localhost';  // eg 'localhost' or 'db.isp.com' or IP
 $CFG->dbname    = 'moodle';     // database name, eg moodle
@@ -418,8 +418,10 @@ $CFG->admin = 'admin';
 // example) in sites where the user theme should override all other theme
 // settings for accessibility reasons. You can also disable types of themes
 // (other than site)  by removing them from the array. The default setting is:
-//      $CFG->themeorder = array('course', 'category', 'session', 'user', 'site');
-// NOTE: course, category, session, user themes still require the
+//
+//     $CFG->themeorder = array('course', 'category', 'session', 'user', 'cohort', 'site');
+//
+// NOTE: course, category, session, user, cohort themes still require the
 // respective settings to be enabled
 //
 // It is possible to add extra themes directory stored outside of $CFG->dirroot.
index e38956d..553e534 100644 (file)
@@ -38,6 +38,9 @@ use pix_icon;
  */
 class modchooser_item extends \core\output\chooser_item {
 
+    /** @var string */
+    protected $customiconurl;
+
     /**
      * Constructor.
      *
@@ -50,10 +53,33 @@ class modchooser_item extends \core\output\chooser_item {
         if ($colon = strpos($modulename, ':')) {
             $modulename = substr($modulename, 0, $colon);
         }
+        if (preg_match('/src="([^"]*)"/i', $module->icon, $matches)) {
+            // Use the custom icon.
+            $this->customiconurl = str_replace('&amp;', '&', $matches[1]);
+        }
+
         $icon = new pix_icon('icon', '', $modulename, ['class' => 'icon']);
         $help = isset($module->help) ? $module->help : new lang_string('nohelpforactivityorresource', 'moodle');
 
         parent::__construct($module->name, $module->title, $module->link->out(false), $icon, $help, $context);
     }
 
+    /**
+     * Export for template.
+     *
+     * @param \renderer_base $output The renderer
+     * @return \stdClass $data
+     */
+    public function export_for_template(\renderer_base $output) {
+        $data = parent::export_for_template($output);
+        if ($this->customiconurl && !empty($data->icon['attributes'])) {
+            // Replace icon source with a module-provided icon.
+            foreach ($data->icon['attributes'] as &$attribute) {
+                if ($attribute['name'] === 'src') {
+                    $attribute['value'] = $this->customiconurl;
+                }
+            }
+        }
+        return $data;
+    }
 }
index d7eacac..0165ba1 100644 (file)
@@ -96,11 +96,7 @@ class enrol_database_testcase extends advanced_testcase {
                 break;
 
             case 'mssql':
-                if (get_class($DB) == 'mssql_native_moodle_database') {
-                    set_config('dbtype', 'mssql_n', 'enrol_database');
-                } else {
-                    set_config('dbtype', 'mssqlnative', 'enrol_database');
-                }
+                set_config('dbtype', 'mssqlnative', 'enrol_database');
                 set_config('dbsybasequoting', '1', 'enrol_database');
                 break;
 
index b8ae94a..30efd91 100644 (file)
@@ -22,7 +22,7 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-require(__DIR__.'../../config.php');
+require(__DIR__.'/../config.php');
 require_once("$CFG->libdir/adminlib.php");
 
 $enrol = optional_param('enrol', '', PARAM_RAW);
index 8868c6a..e7f8de7 100644 (file)
@@ -483,7 +483,6 @@ if ($config->stage == INSTALL_DATABASETYPE) {
                        'pgsql'  => moodle_database::get_driver_instance('pgsql',  'native'),
                        'oci'    => moodle_database::get_driver_instance('oci',    'native'),
                        'sqlsrv' => moodle_database::get_driver_instance('sqlsrv', 'native'), // MS SQL*Server PHP driver
-                       'mssql'  => moodle_database::get_driver_instance('mssql',  'native'), // FreeTDS driver
                       );
 
     echo '<div class="userinput">';
index eebf74f..f8289e0 100644 (file)
@@ -49,6 +49,7 @@ $string['allowbeforeblock'] = 'Allowed list will be processed first';
 $string['allowbeforeblockdesc'] = 'By default, entries in the blocked IPs list are matched first. If this option is enabled, entries in the allowed IPs list are processed before the blocked list.';
 $string['allowblockstodock'] = 'Allow blocks to use the dock';
 $string['allowcategorythemes'] = 'Allow category themes';
+$string['allowcohortthemes'] = 'Allow cohort themes';
 $string['allowcoursethemes'] = 'Allow course themes';
 $string['allowediplist'] = 'Allowed IP list';
 $string['allowedemaildomains'] = 'Allowed email domains';
@@ -144,6 +145,7 @@ $string['configallcountrycodes'] = 'This is the list of countries that may be se
 $string['configallowassign'] = 'You can allow people who have the roles on the left side to assign some of the column roles to other people';
 $string['configallowblockstodock'] = 'If enabled and supported by the selected theme users can choose to move blocks to a special dock.';
 $string['configallowcategorythemes'] = 'If you enable this, then themes can be set at the category level. This will affect all child categories and courses unless they have specifically set their own theme. WARNING: Enabling category themes may affect performance.';
+$string['configallowcohortthemes'] = 'If you enable this, then themes can be set at the cohort level. This will affect all users with only one cohort or more than one but with the same theme.';
 $string['configallowcoursethemes'] = 'If you enable this, then courses will be allowed to set their own themes.  Course themes override all other theme choices (site, user, or session themes)';
 $string['configallowedemaildomains'] = 'List email domains that are allowed to be disclosed in the "From" section of outgoing email. The default of "Empty" will use the No-reply address for all outgoing email. The use of wildcards is allowed e.g. *.example.com will allow emails sent from any subdomain of example.com, but not example.com itself. This will require separate entry.';
 $string['configallowemailaddresses'] = 'To restrict new email addresses to particular domains, list them here separated by spaces. All other domains will be rejected. To allow subdomains, add the domain with a preceding \'.\'. To allow a root domain together with its subdomains, add the domain twice - once with a preceding \'.\' and once without e.g. .ourcollege.edu.au ourcollege.edu.au.';
index 49e7845..ee7503b 100644 (file)
@@ -58,6 +58,7 @@ $string['eventcohortmemberadded'] = 'User added to a cohort';
 $string['eventcohortmemberremoved'] = 'User removed from a cohort';
 $string['eventcohortupdated'] = 'Cohort updated';
 $string['external'] = 'External cohort';
+$string['invalidtheme'] = 'Cohort theme does not exist';
 $string['idnumber'] = 'Cohort ID';
 $string['memberscount'] = 'Cohort size';
 $string['name'] = 'Name';
index bb41d0a..d5f5cd3 100644 (file)
@@ -167,7 +167,6 @@ $string['memorylimithelp'] = '<p>The PHP memory limit for your server is current
     <p>However, on some servers this will prevent <b>all</b> PHP pages from working 
     (you will see errors when you look at pages) so you\'ll have to remove the .htaccess file.</p></li>
 </ol>';
-$string['mssqlextensionisnotpresentinphp'] = 'PHP has not been properly configured with the MSSQL extension so that it can communicate with SQL*Server.  Please check your php.ini file or recompile PHP.';
 $string['mysqliextensionisnotpresentinphp'] = 'PHP has not been properly configured with the MySQLi extension for it to communicate with MySQL. Please check your php.ini file or recompile PHP.';
 $string['nativemariadb'] = 'MariaDB (native/mariadb)';
 $string['nativemariadbhelp'] = '<p>The database is where most of the Moodle settings and data are stored and must be configured here.</p>
@@ -180,9 +179,6 @@ $string['nativemysqlihelp'] = '<p>The database is where most of the Moodle setti
 <p>The database name, username, and password are required fields; table prefix is optional.</p>
 <p>The database name may contain only alphanumeric characters, dollar ($) and underscore (_).</p>
 <p>If the database currently does not exist, and the user you specify has permission, Moodle will attempt to create a new database with the correct permissions and settings.</p>';
-$string['nativemssql'] = 'SQL*Server FreeTDS (native/mssql)';
-$string['nativemssqlhelp'] = 'Now you need to configure the database where most Moodle data will be stored.
-This database must already have been created and a username and password created to access it. Table prefix is mandatory.';
 $string['nativeoci'] = 'Oracle (native/oci)';
 $string['nativeocihelp'] = 'Now you need to configure the database where most Moodle data will be stored.
 This database must already have been created and a username and password created to access it. Table prefix is mandatory.';
@@ -254,3 +250,4 @@ $string['welcomep60'] = 'The following pages will lead you through some easy to
 $string['welcomep70'] = 'Click the "Next" button below to continue with the set up of <strong>Moodle</strong>.';
 $string['wwwroot'] = 'Web address';
 $string['wwwrooterror'] = 'The \'Web Address\' does not appear to be valid - this Moodle installation doesn\'t appear to be there. The value below has been reset.';
+// Deprecated since 3.4.
\ No newline at end of file
index ecc3252..563e2e1 100644 (file)
@@ -93,7 +93,7 @@ $string['nothingtoupdate'] = 'Nothing to update';
 $string['owner'] = 'Owner';
 $string['prevpage'] = 'Back';
 $string['privacy:metadata:tag'] = 'The details of each unique tag are stored alongside their description and other related information';
-$string['privacy:metadataetag:name'] = 'The name of the tag - this is the normalised version of the name.';
+$string['privacy:metadata:tag:name'] = 'The name of the tag - this is the normalised version of the name.';
 $string['privacy:metadata:tag:rawname'] = 'The name of the tag - this is the display name.';
 $string['privacy:metadata:tag:description'] = 'The description of the tag.';
 $string['privacy:metadata:tag:flag'] = 'Whether a tag has been flagged as inappropriate.';
index 302e536..cb6c2fe 100644 (file)
@@ -1804,7 +1804,7 @@ class core_plugin_manager {
             ),
 
             'ltiservice' => array(
-                'memberships', 'profile', 'toolproxy', 'toolsettings'
+                'gradebookservices', 'memberships', 'profile', 'toolproxy', 'toolsettings'
             ),
 
             'mlbackend' => array(
index abd5c93..b9b9003 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20180222" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20180403" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
         <FIELD NAME="component" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false" COMMENT="Component (plugintype_pluignname) that manages the cohort, manual modifications are allowed only when set to NULL"/>
         <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="theme" TYPE="char" LENGTH="50" NOTNULL="false" SEQUENCE="false"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
index 08f2f75..edafff2 100644 (file)
@@ -2200,5 +2200,21 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2018032700.00);
     }
 
+    if ($oldversion < 2018040500.01) {
+
+        // Define field indexpriority to be added to search_index_requests. Allow null initially.
+        $table = new xmldb_table('cohort');
+        $field = new xmldb_field('theme', XMLDB_TYPE_CHAR, '50',
+                null, null, null, null, 'timemodified');
+
+        // Conditionally add field.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2018040500.01);
+    }
+
     return true;
 }
diff --git a/lib/dml/mssql_native_moodle_database.php b/lib/dml/mssql_native_moodle_database.php
deleted file mode 100644 (file)
index 2299472..0000000
+++ /dev/null
@@ -1,1468 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * Native mssql class representing moodle database interface.
- *
- * @package    core_dml
- * @copyright  2009 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-require_once(__DIR__.'/moodle_database.php');
-require_once(__DIR__.'/mssql_native_moodle_recordset.php');
-require_once(__DIR__.'/mssql_native_moodle_temptables.php');
-
-/**
- * Native mssql class representing moodle database interface.
- *
- * @package    core_dml
- * @copyright  2009 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class mssql_native_moodle_database extends moodle_database {
-
-    protected $mssql     = null;
-    protected $last_error_reporting; // To handle mssql driver default verbosity
-    protected $collation;  // current DB collation cache
-    /**
-     * Does the used db version support ANSI way of limiting (2012 and higher)
-     * @var bool
-     */
-    protected $supportsoffsetfetch;
-
-    /**
-     * Detects if all needed PHP stuff installed.
-     * Note: can be used before connect()
-     * @return mixed true if ok, string if something
-     */
-    public function driver_installed() {
-        if (!function_exists('mssql_connect')) {
-            return get_string('mssqlextensionisnotpresentinphp', 'install');
-        }
-        return true;
-    }
-
-    /**
-     * Returns database family type - describes SQL dialect
-     * Note: can be used before connect()
-     * @return string db family name (mysql, postgres, mssql, oracle, etc.)
-     */
-    public function get_dbfamily() {
-        return 'mssql';
-    }
-
-    /**
-     * Returns more specific database driver type
-     * Note: can be used before connect()
-     * @return string db type mysqli, pgsql, oci, mssql, sqlsrv
-     */
-    protected function get_dbtype() {
-        return 'mssql';
-    }
-
-    /**
-     * Returns general database library name
-     * Note: can be used before connect()
-     * @return string db type pdo, native
-     */
-    protected function get_dblibrary() {
-        return 'native';
-    }
-
-    /**
-     * Returns localised database type name
-     * Note: can be used before connect()
-     * @return string
-     */
-    public function get_name() {
-        return get_string('nativemssql', 'install');
-    }
-
-    /**
-     * Returns localised database configuration help.
-     * Note: can be used before connect()
-     * @return string
-     */
-    public function get_configuration_help() {
-        return get_string('nativemssqlhelp', 'install');
-    }
-
-    /**
-     * Diagnose database and tables, this function is used
-     * to verify database and driver settings, db engine types, etc.
-     *
-     * @return string null means everything ok, string means problem found.
-     */
-    public function diagnose() {
-        // Verify the database is running with READ_COMMITTED_SNAPSHOT enabled.
-        // (that's required to get snapshots/row versioning on READ_COMMITED mode).
-        $correctrcsmode = false;
-        $sql = "SELECT is_read_committed_snapshot_on
-                  FROM sys.databases
-                 WHERE name = '{$this->dbname}'";
-        $this->query_start($sql, null, SQL_QUERY_AUX);
-        $result = mssql_query($sql, $this->mssql);
-        $this->query_end($result);
-        if ($result) {
-            if ($row = mssql_fetch_assoc($result)) {
-                $correctrcsmode = (bool)reset($row);
-            }
-        }
-        $this->free_result($result);
-
-        if (!$correctrcsmode) {
-            return get_string('mssqlrcsmodemissing', 'error');
-        }
-
-        // Arrived here, all right.
-        return null;
-    }
-
-    /**
-     * Connect to db
-     * Must be called before other methods.
-     * @param string $dbhost The database host.
-     * @param string $dbuser The database username.
-     * @param string $dbpass The database username's password.
-     * @param string $dbname The name of the database being connected to.
-     * @param mixed $prefix string means moodle db prefix, false used for external databases where prefix not used
-     * @param array $dboptions driver specific options
-     * @return bool true
-     * @throws dml_connection_exception if error
-     */
-    public function connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, array $dboptions=null) {
-        if ($prefix == '' and !$this->external) {
-            //Enforce prefixes for everybody but mysql
-            throw new dml_exception('prefixcannotbeempty', $this->get_dbfamily());
-        }
-
-        $driverstatus = $this->driver_installed();
-
-        if ($driverstatus !== true) {
-            throw new dml_exception('dbdriverproblem', $driverstatus);
-        }
-
-        $this->store_settings($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions);
-
-        $dbhost = $this->dbhost;
-        // Zero shouldn't be used as a port number so doing a check with empty() should be fine.
-        if (!empty($dboptions['dbport'])) {
-            if (stristr(PHP_OS, 'win') && !stristr(PHP_OS, 'darwin')) {
-                $dbhost .= ','.$dboptions['dbport'];
-            } else {
-                $dbhost .= ':'.$dboptions['dbport'];
-            }
-        }
-        ob_start();
-        if (!empty($this->dboptions['dbpersist'])) { // persistent connection
-            $this->mssql = mssql_pconnect($dbhost, $this->dbuser, $this->dbpass, true);
-        } else {
-            $this->mssql = mssql_connect($dbhost, $this->dbuser, $this->dbpass, true);
-        }
-        $dberr = ob_get_contents();
-        ob_end_clean();
-
-        if ($this->mssql === false) {
-            $this->mssql = null;
-            throw new dml_connection_exception($dberr);
-        }
-
-        // Disable logging until we are fully setup.
-        $this->query_log_prevent();
-
-        // already connected, select database and set some env. variables
-        $this->query_start("--mssql_select_db", null, SQL_QUERY_AUX);
-        $result = mssql_select_db($this->dbname, $this->mssql);
-        $this->query_end($result);
-
-        // No need to set charset. It's UTF8, with transparent conversions
-        // back and forth performed both by FreeTDS or ODBTP
-
-        // Allow quoted identifiers
-        $sql = "SET QUOTED_IDENTIFIER ON";
-        $this->query_start($sql, null, SQL_QUERY_AUX);
-        $result = mssql_query($sql, $this->mssql);
-        $this->query_end($result);
-
-        $this->free_result($result);
-
-        // Force ANSI nulls so the NULL check was done by IS NULL and NOT IS NULL
-        // instead of equal(=) and distinct(<>) symbols
-        $sql = "SET ANSI_NULLS ON";
-        $this->query_start($sql, null, SQL_QUERY_AUX);
-        $result = mssql_query($sql, $this->mssql);
-        $this->query_end($result);
-
-        $this->free_result($result);
-
-        // Force ANSI warnings so arithmetic/string overflows will be
-        // returning error instead of transparently truncating data
-        $sql = "SET ANSI_WARNINGS ON";
-        $this->query_start($sql, null, SQL_QUERY_AUX);
-        $result = mssql_query($sql, $this->mssql);
-        $this->query_end($result);
-
-        // Concatenating null with anything MUST return NULL
-        $sql = "SET CONCAT_NULL_YIELDS_NULL  ON";
-        $this->query_start($sql, null, SQL_QUERY_AUX);
-        $result = mssql_query($sql, $this->mssql);
-        $this->query_end($result);
-
-        $this->free_result($result);
-
-        // Set transactions isolation level to READ_COMMITTED
-        // prevents dirty reads when using transactions +
-        // is the default isolation level of MSSQL
-        // Requires database to run with READ_COMMITTED_SNAPSHOT ON
-        $sql = "SET TRANSACTION ISOLATION LEVEL READ COMMITTED";
-        $this->query_start($sql, NULL, SQL_QUERY_AUX);
-        $result = mssql_query($sql, $this->mssql);
-        $this->query_end($result);
-
-        $this->free_result($result);
-
-        $serverinfo = $this->get_server_info();
-        // Fetch/offset is supported staring from SQL Server 2012.
-        $this->supportsoffsetfetch = $serverinfo['version'] > '11';
-
-        // We can enable logging now.
-        $this->query_log_allow();
-
-        // Connection stabilised and configured, going to instantiate the temptables controller
-        $this->temptables = new mssql_native_moodle_temptables($this);
-
-        return true;
-    }
-
-    /**
-     * Close database connection and release all resources
-     * and memory (especially circular memory references).
-     * Do NOT use connect() again, create a new instance if needed.
-     */
-    public function dispose() {
-        parent::dispose(); // Call parent dispose to write/close session and other common stuff before closing connection
-        if ($this->mssql) {
-            mssql_close($this->mssql);
-            $this->mssql = null;
-        }
-    }
-
-    /**
-     * Called before each db query.
-     * @param string $sql
-     * @param array array of parameters
-     * @param int $type type of query
-     * @param mixed $extrainfo driver specific extra information
-     * @return void
-     */
-    protected function query_start($sql, array $params=null, $type, $extrainfo=null) {
-        parent::query_start($sql, $params, $type, $extrainfo);
-        // mssql driver tends to send debug to output, we do not need that ;-)
-        $this->last_error_reporting = error_reporting(0);
-    }
-
-    /**
-     * Called immediately after each db query.
-     * @param mixed db specific result
-     * @return void
-     */
-    protected function query_end($result) {
-        // reset original debug level
-        error_reporting($this->last_error_reporting);
-        parent::query_end($result);
-    }
-
-    /**
-     * Returns database server info array
-     * @return array Array containing 'description' and 'version' info
-     */
-    public function get_server_info() {
-        static $info;
-        if (!$info) {
-            $info = array();
-            $sql = 'sp_server_info 2';
-            $this->query_start($sql, null, SQL_QUERY_AUX);
-            $result = mssql_query($sql, $this->mssql);
-            $this->query_end($result);
-            $row = mssql_fetch_row($result);
-            $info['description'] = $row[2];
-            $this->free_result($result);
-
-            $sql = 'sp_server_info 500';
-            $this->query_start($sql, null, SQL_QUERY_AUX);
-            $result = mssql_query($sql, $this->mssql);
-            $this->query_end($result);
-            $row = mssql_fetch_row($result);
-            $info['version'] = $row[2];
-            $this->free_result($result);
-        }
-        return $info;
-    }
-
-    /**
-     * Converts short table name {tablename} to real table name
-     * supporting temp tables (#) if detected
-     *
-     * @param string sql
-     * @return string sql
-     */
-    protected function fix_table_names($sql) {
-        if (preg_match_all('/\{([a-z][a-z0-9_]*)\}/', $sql, $matches)) {
-            foreach($matches[0] as $key=>$match) {
-                $name = $matches[1][$key];
-                if ($this->temptables->is_temptable($name)) {
-                    $sql = str_replace($match, $this->temptables->get_correct_name($name), $sql);
-                } else {
-                    $sql = str_replace($match, $this->prefix.$name, $sql);
-                }
-            }
-        }
-        return $sql;
-    }
-
-    /**
-     * Returns supported query parameter types
-     * @return int bitmask of accepted SQL_PARAMS_*
-     */
-    protected function allowed_param_types() {
-        return SQL_PARAMS_QM; // Not really, but emulated, see emulate_bound_params()
-    }
-
-    /**
-     * Returns last error reported by database engine.
-     * @return string error message
-     */
-    public function get_last_error() {
-        return mssql_get_last_message();
-    }
-
-    /**
-     * Return tables in database WITHOUT current prefix
-     * @param bool $usecache if true, returns list of cached tables.
-     * @return array of table names in lowercase and without prefix
-     */
-    public function get_tables($usecache=true) {
-        if ($usecache and $this->tables !== null) {
-            return $this->tables;
-        }
-        $this->tables = array();
-        $sql = "SELECT table_name
-                  FROM INFORMATION_SCHEMA.TABLES
-                 WHERE table_name LIKE '$this->prefix%'
-                   AND table_type = 'BASE TABLE'";
-        $this->query_start($sql, null, SQL_QUERY_AUX);
-        $result = mssql_query($sql, $this->mssql);
-        $this->query_end($result);
-
-        if ($result) {
-            while ($row = mssql_fetch_row($result)) {
-                $tablename = reset($row);
-                if ($this->prefix !== false && $this->prefix !== '') {
-                    if (strpos($tablename, $this->prefix) !== 0) {
-                        continue;
-                    }
-                    $tablename = substr($tablename, strlen($this->prefix));
-                }
-                $this->tables[$tablename] = $tablename;
-            }
-            $this->free_result($result);
-        }
-
-        // Add the currently available temptables
-        $this->tables = array_merge($this->tables, $this->temptables->get_temptables());
-        return $this->tables;
-    }
-
-    /**
-     * Return table indexes - everything lowercased.
-     * @param string $table The table we want to get indexes from.
-     * @return array An associative array of indexes containing 'unique' flag and 'columns' being indexed
-     */
-    public function get_indexes($table) {
-        $indexes = array();
-        $tablename = $this->prefix.$table;
-
-        // Indexes aren't covered by information_schema metatables, so we need to
-        // go to sys ones. Skipping primary key indexes on purpose.
-        $sql = "SELECT i.name AS index_name, i.is_unique, ic.index_column_id, c.name AS column_name
-                  FROM sys.indexes i
-                  JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
-                  JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
-                  JOIN sys.tables t ON i.object_id = t.object_id
-                 WHERE t.name = '$tablename'
-                   AND i.is_primary_key = 0
-              ORDER BY i.name, i.index_id, ic.index_column_id";
-
-        $this->query_start($sql, null, SQL_QUERY_AUX);
-        $result = mssql_query($sql, $this->mssql);
-        $this->query_end($result);
-
-        if ($result) {
-            $lastindex = '';
-            $unique = false;
-            $columns = array();
-            while ($row = mssql_fetch_assoc($result)) {
-                if ($lastindex and $lastindex != $row['index_name']) { // Save lastindex to $indexes and reset info
-                    $indexes[$lastindex] = array('unique' => $unique, 'columns' => $columns);
-                    $unique = false;
-                    $columns = array();
-                }
-                $lastindex = $row['index_name'];
-                $unique = empty($row['is_unique']) ? false : true;
-                $columns[] = $row['column_name'];
-            }
-            if ($lastindex ) { // Add the last one if exists
-                $indexes[$lastindex] = array('unique' => $unique, 'columns' => $columns);
-            }
-            $this->free_result($result);
-        }
-        return $indexes;
-    }
-
-    /**
-     * Returns datailed information about columns in table. This information is cached internally.
-     * @param string $table name
-     * @param bool $usecache
-     * @return array array of database_column_info objects indexed with column names
-     */
-    public function get_columns($table, $usecache=true) {
-
-        if ($usecache) {
-            if ($this->temptables->is_temptable($table)) {
-                if ($data = $this->get_temp_tables_cache()->get($table)) {
-                    return $data;
-                }
-            } else {
-                if ($data = $this->get_metacache()->get($table)) {
-                    return $data;
-                }
-            }
-        }
-
-        $structure = array();
-
-        if (!$this->temptables->is_temptable($table)) { // normal table, get metadata from own schema
-            $sql = "SELECT column_name AS name,
-                           data_type AS type,
-                           numeric_precision AS max_length,
-                           character_maximum_length AS char_max_length,
-                           numeric_scale AS scale,
-                           is_nullable AS is_nullable,
-                           columnproperty(object_id(quotename(table_schema) + '.' +
-                               quotename(table_name)), column_name, 'IsIdentity') AS auto_increment,
-                           column_default AS default_value
-                      FROM INFORMATION_SCHEMA.COLUMNS
-                     WHERE table_name = '{" . $table . "}'
-                  ORDER BY ordinal_position";
-        } else { // temp table, get metadata from tempdb schema
-            $sql = "SELECT column_name AS name,
-                           data_type AS type,
-                           numeric_precision AS max_length,
-                           character_maximum_length AS char_max_length,
-                           numeric_scale AS scale,
-                           is_nullable AS is_nullable,
-                           columnproperty(object_id(quotename(table_schema) + '.' +
-                               quotename(table_name)), column_name, 'IsIdentity') AS auto_increment,
-                           column_default AS default_value
-                      FROM tempdb.INFORMATION_SCHEMA.COLUMNS
-                      JOIN tempdb.sys.objects ON name = table_name
-                     WHERE object_id = object_id('tempdb..{" . $table . "}')
-                  ORDER BY ordinal_position";
-        }
-
-        list($sql, $params, $type) = $this->fix_sql_params($sql, null);
-
-        $this->query_start($sql, null, SQL_QUERY_AUX);
-        $result = mssql_query($sql, $this->mssql);
-        $this->query_end($result);
-
-        if (!$result) {
-            return array();
-        }
-
-        while ($rawcolumn = mssql_fetch_assoc($result)) {
-
-            $rawcolumn = (object)$rawcolumn;
-
-            $info = new stdClass();
-            $info->name = $rawcolumn->name;
-            $info->type = $rawcolumn->type;
-            $info->meta_type = $this->mssqltype2moodletype($info->type);
-
-            // Prepare auto_increment info
-            $info->auto_increment = $rawcolumn->auto_increment ? true : false;
-
-            // Define type for auto_increment columns
-            $info->meta_type = ($info->auto_increment && $info->meta_type == 'I') ? 'R' : $info->meta_type;
-
-            // id columns being auto_incremnt are PK by definition
-            $info->primary_key = ($info->name == 'id' && $info->meta_type == 'R' && $info->auto_increment);
-
-            if ($info->meta_type === 'C' and $rawcolumn->char_max_length == -1) {
-                // This is NVARCHAR(MAX), not a normal NVARCHAR.
-                $info->max_length = -1;
-                $info->meta_type = 'X';
-            } else {
-                // Put correct length for character and LOB types
-                $info->max_length = $info->meta_type == 'C' ? $rawcolumn->char_max_length : $rawcolumn->max_length;
-                $info->max_length = ($info->meta_type == 'X' || $info->meta_type == 'B') ? -1 : $info->max_length;
-            }
-
-            // Scale
-            $info->scale = $rawcolumn->scale;
-
-            // Prepare not_null info
-            $info->not_null = $rawcolumn->is_nullable == 'NO'  ? true : false;
-
-            // Process defaults
-            $info->has_default = !empty($rawcolumn->default_value);
-            if ($rawcolumn->default_value === NULL) {
-                $info->default_value = NULL;
-            } else {
-                $info->default_value = preg_replace("/^[\(N]+[']?(.*?)[']?[\)]+$/", '\\1', $rawcolumn->default_value);
-            }
-
-            // Process binary
-            $info->binary = $info->meta_type == 'B' ? true : false;
-
-            $structure[$info->name] = new database_column_info($info);
-        }
-        $this->free_result($result);
-
-        if ($usecache) {
-            if ($this->temptables->is_temptable($table)) {
-                $this->get_temp_tables_cache()->set($table, $structure);
-            } else {
-                $this->get_metacache()->set($table, $structure);
-            }
-        }
-
-        return $structure;
-    }
-
-    /**
-     * Normalise values based on varying RDBMS's dependencies (booleans, LOBs...)
-     *
-     * @param database_column_info $column column metadata corresponding with the value we are going to normalise
-     * @param mixed $value value we are going to normalise
-     * @return mixed the normalised value
-     */
-    protected function normalise_value($column, $value) {
-        $this->detect_objects($value);
-
-        if (is_bool($value)) { // Always, convert boolean to int
-            $value = (int)$value;
-        } // And continue processing because text columns with numeric info need special handling below
-
-        if ($column->meta_type == 'B') {   // BLOBs need to be properly "packed", but can be inserted directly if so.
-            if (!is_null($value)) {               // If value not null, unpack it to unquoted hexadecimal byte-string format
-                $value = unpack('H*hex', $value); // we leave it as array, so emulate_bound_params() can detect it
-            }                                     // easily and "bind" the param ok.
-
-        } else if ($column->meta_type == 'X') {             // MSSQL doesn't cast from int to text, so if text column
-            if (is_numeric($value)) {                       // and is numeric value then cast to string
-                $value = array('numstr' => (string)$value); // and put into array, so emulate_bound_params() will know how
-            }                                               // to "bind" the param ok, avoiding reverse conversion to number
-
-        } else if ($value === '') {
-            if ($column->meta_type == 'I' or $column->meta_type == 'F' or $column->meta_type == 'N') {
-                $value = 0; // prevent '' problems in numeric fields
-            }
-        }
-        return $value;
-    }
-
-    /**
-     * Selectively call mssql_free_result(), avoiding some warnings without using the horrible @
-     *
-     * @param mssql_resource $resource resource to be freed if possible
-     */
-    private function free_result($resource) {
-        if (!is_bool($resource)) { // true/false resources cannot be freed
-            mssql_free_result($resource);
-        }
-    }
-
-    /**
-     * Provides mapping between mssql native data types and moodle_database - database_column_info - ones)
-     *
-     * @param string $mssql_type native mssql data type
-     * @return string 1-char database_column_info data type
-     */
-    private function mssqltype2moodletype($mssql_type) {
-        $type = null;
-        switch (strtoupper($mssql_type)) {
-            case 'BIT':
-                $type = 'L';
-                break;
-            case 'INT':
-            case 'SMALLINT':
-            case 'INTEGER':
-            case 'BIGINT':
-                $type = 'I';
-                break;
-            case 'DECIMAL':
-            case 'REAL':
-            case 'FLOAT':
-                $type = 'N';
-                break;
-            case 'VARCHAR':
-            case 'NVARCHAR':
-                $type = 'C';
-                break;
-            case 'TEXT':
-            case 'NTEXT':
-            case 'VARCHAR(MAX)':
-            case 'NVARCHAR(MAX)':
-                $type = 'X';
-                break;
-            case 'IMAGE':
-            case 'VARBINARY':
-            case 'VARBINARY(MAX)':
-                $type = 'B';
-                break;
-            case 'DATETIME':
-                $type = 'D';
-                break;
-        }
-        if (!$type) {
-            throw new dml_exception('invalidmssqlnativetype', $mssql_type);
-        }
-        return $type;
-    }
-
-    /**
-     * Do NOT use in code, to be used by database_manager only!
-     * @param string|array $sql query
-     * @param array|null $tablenames an array of xmldb table names affected by this request.
-     * @return bool true
-     * @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors.
-     */
-    public function change_database_structure($sql, $tablenames = null) {
-        $this->get_manager(); // Includes DDL exceptions classes ;-)
-        $sqls = (array)$sql;
-
-        try {
-            foreach ($sqls as $sql) {
-                $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
-                $result = mssql_query($sql, $this->mssql);
-                $this->query_end($result);
-            }
-        } catch (ddl_change_structure_exception $e) {
-            $this->reset_caches($tablenames);
-            throw $e;
-        }
-
-        $this->reset_caches($tablenames);
-        return true;
-    }
-
-    /**
-     * Very ugly hack which emulates bound parameters in queries
-     * because the mssql driver doesn't support placeholders natively at all
-     */
-    protected function emulate_bound_params($sql, array $params=null) {
-        if (empty($params)) {
-            return $sql;
-        }
-        // ok, we have verified sql statement with ? and correct number of params
-        $parts = array_reverse(explode('?', $sql));
-        $return = array_pop($parts);
-        foreach ($params as $param) {
-            if (is_bool($param)) {
-                $return .= (int)$param;
-
-            } else if (is_array($param) && isset($param['hex'])) { // detect hex binary, bind it specially
-                $return .= '0x' . $param['hex'];
-
-            } else if (is_array($param) && isset($param['numstr'])) { // detect numerical strings that *must not*
-                $return .= "N'{$param['numstr']}'";                   // be converted back to number params, but bound as strings
-
-            } else if (is_null($param)) {
-                $return .= 'NULL';
-
-            } else if (is_number($param)) { // we can not use is_numeric() because it eats leading zeros from strings like 0045646
-                $return .= "'".$param."'"; //fix for MDL-24863 to prevent auto-cast to int.
-
-            } else if (is_float($param)) {
-                $return .= $param;
-
-            } else {
-                $param = str_replace("'", "''", $param);
-                $param = str_replace("\0", "", $param);
-                $return .= "N'$param'";
-            }
-
-            $return .= array_pop($parts);
-        }
-        return $return;
-    }
-
-    /**
-     * Execute general sql query. Should be used only when no other method suitable.
-     * Do NOT use this to make changes in db structure, use database_manager methods instead!
-     * @param string $sql query
-     * @param array $params query parameters
-     * @return bool true
-     * @throws dml_exception A DML specific exception is thrown for any errors.
-     */
-    public function execute($sql, array $params=null) {
-
-        list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
-        $rawsql = $this->emulate_bound_params($sql, $params);
-
-        if (strpos($sql, ';') !== false) {
-            throw new coding_exception('moodle_database::execute() Multiple sql statements found or bound parameters not used properly in query!');
-        }
-
-        $this->query_start($sql, $params, SQL_QUERY_UPDATE);
-        $result = mssql_query($rawsql, $this->mssql);
-        $this->query_end($result);
-        $this->free_result($result);
-
-        return true;
-    }
-
-    /**
-     * Get a number of records as a moodle_recordset using a SQL statement.
-     *
-     * Since this method is a little less readable, use of it should be restricted to
-     * code where it's possible there might be large datasets being returned.  For known
-     * small datasets use get_records_sql - it leads to simpler code.
-     *
-     * The return type is like:
-     * @see function get_recordset.
-     *
-     * @param string $sql the SQL select query to execute.
-     * @param array $params array of sql parameters
-     * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
-     * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
-     * @return moodle_recordset instance
-     * @throws dml_exception A DML specific exception is thrown for any errors.
-     */
-    public function get_recordset_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
-
-        list($limitfrom, $limitnum) = $this->normalise_limit_from_num($limitfrom, $limitnum);
-
-        if ($limitfrom or $limitnum) {
-            if (!$this->supportsoffsetfetch) {
-                if ($limitnum >= 1) { // Only apply TOP clause if we have any limitnum (limitfrom offset is handled later).
-                    $fetch = $limitfrom + $limitnum;
-                    if (PHP_INT_MAX - $limitnum < $limitfrom) { // Check PHP_INT_MAX overflow.
-                        $fetch = PHP_INT_MAX;
-                    }
-                    $sql = preg_replace('/^([\s(])*SELECT([\s]+(DISTINCT|ALL))?(?!\s*TOP\s*\()/i',
-                                        "\\1SELECT\\2 TOP $fetch", $sql);
-                }
-            } else {
-                $sql = (substr($sql, -1) === ';') ? substr($sql, 0, -1) : $sql;
-                // We need order by to use FETCH/OFFSET.
-                // Ordering by first column shouldn't break anything if there was no order in the first place.
-                if (!strpos(strtoupper($sql), "ORDER BY")) {
-                    $sql .= " ORDER BY 1";
-                }
-
-                $sql .= " OFFSET ".$limitfrom." ROWS ";
-
-                if ($limitnum > 0) {
-                    $sql .= " FETCH NEXT ".$limitnum." ROWS ONLY";
-                }
-            }
-        }
-
-        list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
-        $rawsql = $this->emulate_bound_params($sql, $params);
-
-        $this->query_start($sql, $params, SQL_QUERY_SELECT);
-        $result = mssql_query($rawsql, $this->mssql);
-        $this->query_end($result);
-
-        if ($limitfrom && !$this->supportsoffsetfetch) { // Skip $limitfrom records.
-            if (!@mssql_data_seek($result, $limitfrom)) {
-                // Nothing, most probably seek past the end.
-                mssql_free_result($result);
-                $result = null;
-            }
-        }
-
-        return $this->create_recordset($result);
-    }
-
-    protected function create_recordset($result) {
-        return new mssql_native_moodle_recordset($result);
-    }
-
-    /**
-     * Get a number of records as an array of objects using a SQL statement.
-     *
-     * Return value is like:
-     * @see function get_records.
-     *
-     * @param string $sql the SQL select query to execute. The first column of this SELECT statement
-     *   must be a unique value (usually the 'id' field), as it will be used as the key of the
-     *   returned array.
-     * @param array $params array of sql parameters
-     * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
-     * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
-     * @return array of objects, or empty array if no records were found
-     * @throws dml_exception A DML specific exception is thrown for any errors.
-     */
-    public function get_records_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
-
-        $rs = $this->get_recordset_sql($sql, $params, $limitfrom, $limitnum);
-
-        $results = array();
-
-        foreach ($rs as $row) {
-            $id = reset($row);
-            if (isset($results[$id])) {
-                $colname = key($row);
-                debugging("Did you remember to make the first column something unique in your call to get_records? Duplicate value '$id' found in column '$colname'.", DEBUG_DEVELOPER);
-            }
-            $results[$id] = $row;
-        }
-        $rs->close();
-
-        return $results;
-    }
-
-    /**
-     * Selects records and return values (first field) as an array using a SQL statement.
-     *
-     * @param string $sql The SQL query
-     * @param array $params array of sql parameters
-     * @return array of values
-     * @throws dml_exception A DML specific exception is thrown for any errors.
-     */
-    public function get_fieldset_sql($sql, array $params=null) {
-
-        $rs = $this->get_recordset_sql($sql, $params);
-
-        $results = array();
-
-        foreach ($rs as $row) {
-            $results[] = reset($row);
-        }
-        $rs->close();
-
-        return $results;
-    }
-
-    /**
-     * Insert new record into database, as fast as possible, no safety checks, lobs not supported.
-     * @param string $table name
-     * @param mixed $params data record as object or array
-     * @param bool $returnit return it of inserted record
-     * @param bool $bulk true means repeated inserts expected
-     * @param bool $customsequence true if 'id' included in $params, disables $returnid
-     * @return bool|int true or new id
-     * @throws dml_exception A DML specific exception is thrown for any errors.
-     */
-    public function insert_record_raw($table, $params, $returnid=true, $bulk=false, $customsequence=false) {
-        if (!is_array($params)) {
-            $params = (array)$params;
-        }
-
-        $returning = "";
-        $isidentity = false;
-
-        if ($customsequence) {
-            if (!isset($params['id'])) {
-                throw new coding_exception('moodle_database::insert_record_raw() id field must be specified if custom sequences used.');
-            }
-            $returnid = false;
-
-            $columns = $this->get_columns($table);
-            if (isset($columns['id']) and $columns['id']->auto_increment) {
-                $isidentity = true;
-            }
-
-            // Disable IDENTITY column before inserting record with id, only if the
-            // column is identity, from meta information.
-            if ($isidentity) {
-                $sql = 'SET IDENTITY_INSERT {' . $table . '} ON'; // Yes, it' ON!!
-                list($sql, $xparams, $xtype) = $this->fix_sql_params($sql, null);
-                $this->query_start($sql, null, SQL_QUERY_AUX);
-                $result = mssql_query($sql, $this->mssql);
-                $this->query_end($result);
-                $this->free_result($result);
-            }
-
-        } else {
-            unset($params['id']);
-            if ($returnid) {
-                $returning = "OUTPUT inserted.id";
-            }
-        }
-
-        if (empty($params)) {
-            throw new coding_exception('moodle_database::insert_record_raw() no fields found.');
-        }
-
-        $fields = implode(',', array_keys($params));
-        $qms    = array_fill(0, count($params), '?');
-        $qms    = implode(',', $qms);
-
-        $sql = "INSERT INTO {" . $table . "} ($fields) $returning VALUES ($qms)";
-
-        list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
-        $rawsql = $this->emulate_bound_params($sql, $params);
-
-        $this->query_start($sql, $params, SQL_QUERY_INSERT);
-        $result = mssql_query($rawsql, $this->mssql);
-        // Expected results are:
-        //     - true: insert ok and there isn't returned information.
-        //     - false: insert failed and there isn't returned information.
-        //     - resource: insert executed, need to look for returned (output)
-        //           values to know if the insert was ok or no. Posible values
-        //           are false = failed, integer = insert ok, id returned.
-        $end = false;
-        if (is_bool($result)) {
-            $end = $result;
-        } else if (is_resource($result)) {
-            $end = mssql_result($result, 0, 0); // Fetch 1st column from 1st row.
-        }
-        $this->query_end($end); // End the query with the calculated $end.
-
-        if ($returning !== "") {
-            $params['id'] = $end;
-        }
-        $this->free_result($result);
-
-        if ($customsequence) {
-            // Enable IDENTITY column after inserting record with id, only if the
-            // column is identity, from meta information.
-            if ($isidentity) {
-                $sql = 'SET IDENTITY_INSERT {' . $table . '} OFF'; // Yes, it' OFF!!
-                list($sql, $xparams, $xtype) = $this->fix_sql_params($sql, null);
-                $this->query_start($sql, null, SQL_QUERY_AUX);
-                $result = mssql_query($sql, $this->mssql);
-                $this->query_end($result);
-                $this->free_result($result);
-            }
-        }
-
-        if (!$returnid) {
-            return true;
-        }
-
-        return (int)$params['id'];
-    }
-
-    /**
-     * Insert a record into a table and return the "id" field if required.
-     *
-     * Some conversions and safety checks are carried out. Lobs are supported.
-     * If the return ID isn't required, then this just reports success as true/false.
-     * $data is an object containing needed data
-     * @param string $table The database table to be inserted into
-     * @param object $data A data object with values for one or more fields in the record
-     * @param bool $returnid Should the id of the newly created record entry be returned? If this option is not requested then true/false is returned.
-     * @return bool|int true or new id
-     * @throws dml_exception A DML specific exception is thrown for any errors.
-     */
-    public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
-        $dataobject = (array)$dataobject;
-
-        $columns = $this->get_columns($table);
-        if (empty($columns)) {
-            throw new dml_exception('ddltablenotexist', $table);
-        }
-        $cleaned = array();
-
-        foreach ($dataobject as $field => $value) {
-            if ($field === 'id') {
-                continue;
-            }
-            if (!isset($columns[$field])) {
-                continue;
-            }
-            $column = $columns[$field];
-            $cleaned[$field] = $this->normalise_value($column, $value);
-        }
-
-        return $this->insert_record_raw($table, $cleaned, $returnid, $bulk);
-    }
-
-    /**
-     * Import a record into a table, id field is required.
-     * Safety checks are NOT carried out. Lobs are supported.
-     *
-     * @param string $table name of database table to be inserted into
-     * @param object $dataobject A data object with values for one or more fields in the record
-     * @return bool true
-     * @throws dml_exception A DML specific exception is thrown for any errors.
-     */
-    public function import_record($table, $dataobject) {
-        $dataobject = (array)$dataobject;
-
-        $columns = $this->get_columns($table);
-        $cleaned = array();
-
-        foreach ($dataobject as $field => $value) {
-            if (!isset($columns[$field])) {
-                continue;
-            }
-            $column = $columns[$field];
-            $cleaned[$field] = $this->normalise_value($column, $value);
-        }
-
-        $this->insert_record_raw($table, $cleaned, false, false, true);
-
-        return true;
-    }
-
-    /**
-     * Update record in database, as fast as possible, no safety checks, lobs not supported.
-     * @param string $table name
-     * @param mixed $params data record as object or array
-     * @param bool true means repeated updates expected
-     * @return bool true
-     * @throws dml_exception A DML specific exception is thrown for any errors.
-     */
-    public function update_record_raw($table, $params, $bulk=false) {
-        $params = (array)$params;
-
-        if (!isset($params['id'])) {
-            throw new coding_exception('moodle_database::update_record_raw() id field must be specified.');
-        }
-        $id = $params['id'];
-        unset($params['id']);
-
-        if (empty($params)) {
-            throw new coding_exception('moodle_database::update_record_raw() no fields found.');
-        }
-
-        $sets = array();
-        foreach ($params as $field=>$value) {
-            $sets[] = "$field = ?";
-        }
-
-        $params[] = $id; // last ? in WHERE condition
-
-        $sets = implode(',', $sets);
-        $sql = "UPDATE {" . $table . "} SET $sets WHERE id = ?";
-
-        list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
-        $rawsql = $this->emulate_bound_params($sql, $params);
-
-        $this->query_start($sql, $params, SQL_QUERY_UPDATE);
-        $result = mssql_query($rawsql, $this->mssql);
-        $this->query_end($result);
-
-        $this->free_result($result);
-        return true;
-    }
-
-    /**
-     * Update a record in a table
-     *
-     * $dataobject is an object containing needed data
-     * Relies on $dataobject having a variable "id" to
-     * specify the record to update
-     *
-     * @param string $table The database table to be checked against.
-     * @param object $dataobject An object with contents equal to fieldname=>fieldvalue. Must have an entry for 'id' to map to the table specified.
-     * @param bool true means repeated updates expected
-     * @return bool true
-     * @throws dml_exception A DML specific exception is thrown for any errors.
-     */
-    public function update_record($table, $dataobject, $bulk=false) {
-        $dataobject = (array)$dataobject;
-
-        $columns = $this->get_columns($table);
-        $cleaned = array();
-
-        foreach ($dataobject as $field => $value) {
-            if (!isset($columns[$field])) {
-                continue;
-            }
-            $column = $columns[$field];
-            $cleaned[$field] = $this->normalise_value($column, $value);
-        }
-
-        return $this->update_record_raw($table, $cleaned, $bulk);
-    }
-
-    /**
-     * Set a single field in every table record which match a particular WHERE clause.
-     *
-     * @param string $table The database table to be checked against.
-     * @param string $newfield the field to set.
-     * @param string $newvalue the value to set the field to.
-     * @param string $select A fragment of SQL to be used in a where clause in the SQL call.
-     * @param array $params array of sql parameters
-     * @return bool true
-     * @throws dml_exception A DML specific exception is thrown for any errors.
-     */
-    public function set_field_select($table, $newfield, $newvalue, $select, array $params=null) {
-
-        if ($select) {
-            $select = "WHERE $select";
-        }
-        if (is_null($params)) {
-            $params = array();
-        }
-
-        // convert params to ? types
-        list($select, $params, $type) = $this->fix_sql_params($select, $params);
-
-        // Get column metadata
-        $columns = $this->get_columns($table);
-        $column = $columns[$newfield];
-
-        $newvalue = $this->normalise_value($column, $newvalue);
-
-        if (is_null($newvalue)) {
-            $newfield = "$newfield = NULL";
-        } else {
-            $newfield = "$newfield = ?";
-            array_unshift($params, $newvalue);
-        }
-        $sql = "UPDATE {" . $table . "} SET $newfield $select";
-
-        list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
-        $rawsql = $this->emulate_bound_params($sql, $params);
-
-        $this->query_start($sql, $params, SQL_QUERY_UPDATE);
-        $result = mssql_query($rawsql, $this->mssql);
-        $this->query_end($result);
-
-        $this->free_result($result);
-
-        return true;
-    }
-
-    /**
-     * Delete one or more records from a table which match a particular WHERE clause.
-     *
-     * @param string $table The database table to be checked against.
-     * @param string $select A fragment of SQL to be used in a where clause in the SQL call (used to define the selection criteria).
-     * @param array $params array of sql parameters
-     * @return bool true
-     * @throws dml_exception A DML specific exception is thrown for any errors.
-     */
-    public function delete_records_select($table, $select, array $params=null) {
-
-        if ($select) {
-            $select = "WHERE $select";
-        }
-
-        $sql = "DELETE FROM {" . $table . "} $select";
-
-        list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
-        $rawsql = $this->emulate_bound_params($sql, $params);
-
-        $this->query_start($sql, $params, SQL_QUERY_UPDATE);
-        $result = mssql_query($rawsql, $this->mssql);
-        $this->query_end($result);
-
-        $this->free_result($result);
-
-        return true;
-    }
-
-    public function sql_cast_char2int($fieldname, $text=false) {
-        if (!$text) {
-            return ' CAST(' . $fieldname . ' AS INT) ';
-        } else {
-            return ' CAST(' . $this->sql_compare_text($fieldname) . ' AS INT) ';
-        }
-    }
-
-    public function sql_cast_char2real($fieldname, $text=false) {
-        if (!$text) {
-            return ' CAST(' . $fieldname . ' AS REAL) ';
-        } else {
-            return ' CAST(' . $this->sql_compare_text($fieldname) . ' AS REAL) ';
-        }
-    }
-
-    public function sql_ceil($fieldname) {
-        return ' CEILING(' . $fieldname . ')';
-    }
-
-
-    protected function get_collation() {
-        if (isset($this->collation)) {
-            return $this->collation;
-        }
-        if (!empty($this->dboptions['dbcollation'])) {
-            // perf speedup
-            $this->collation = $this->dboptions['dbcollation'];
-            return $this->collation;
-        }
-
-        // make some default
-        $this->collation = 'Latin1_General_CI_AI';
-
-        $sql = "SELECT CAST(DATABASEPROPERTYEX('$this->dbname', 'Collation') AS varchar(255)) AS SQLCollation";
-        $this->query_start($sql, null, SQL_QUERY_AUX);
-        $result = mssql_query($sql, $this->mssql);
-        $this->query_end($result);
-
-        if ($result) {
-            if ($rawcolumn = mssql_fetch_assoc($result)) {
-                $this->collation = reset($rawcolumn);
-            }
-            $this->free_result($result);
-        }
-
-        return $this->collation;
-    }
-
-    public function sql_equal($fieldname, $param, $casesensitive = true, $accentsensitive = true, $notequal = false) {
-        $equalop = $notequal ? '<>' : '=';
-        $collation = $this->get_collation();
-
-        if ($casesensitive) {
-            $collation = str_replace('_CI', '_CS', $collation);
-        } else {
-            $collation = str_replace('_CS', '_CI', $collation);
-        }
-        if ($accentsensitive) {
-            $collation = str_replace('_AI', '_AS', $collation);
-        } else {
-            $collation = str_replace('_AS', '_AI', $collation);
-        }
-
-        return "$fieldname COLLATE $collation $equalop $param";
-    }
-
-    /**
-     * Returns 'LIKE' part of a query.
-     *
-     * @param string $fieldname usually name of the table column
-     * @param string $param usually bound query parameter (?, :named)
-     * @param bool $casesensitive use case sensitive search
-     * @param bool $accensensitive use accent sensitive search (not all databases support accent insensitive)
-     * @param bool $notlike true means "NOT LIKE"
-     * @param string $escapechar escape char for '%' and '_'
-     * @return string SQL code fragment
-     */
-    public function sql_like($fieldname, $param, $casesensitive = true, $accentsensitive = true, $notlike = false, $escapechar = '\\') {
-        if (strpos($param, '%') !== false) {
-            debugging('Potential SQL injection detected, sql_like() expects bound parameters (? or :named)');
-        }
-
-        $collation = $this->get_collation();
-
-        if ($casesensitive) {
-            $collation = str_replace('_CI', '_CS', $collation);
-        } else {
-            $collation = str_replace('_CS', '_CI', $collation);
-        }
-        if ($accentsensitive) {
-            $collation = str_replace('_AI', '_AS', $collation);
-        } else {
-            $collation = str_replace('_AS', '_AI', $collation);
-        }
-
-        $LIKE = $notlike ? 'NOT LIKE' : 'LIKE';
-
-        return "$fieldname COLLATE $collation $LIKE $param ESCAPE '$escapechar'";
-    }
-
-    public function sql_concat() {
-        $arr = func_get_args();
-        foreach ($arr as $key => $ele) {
-            $arr[$key] = ' CAST(' . $ele . ' AS NVARCHAR(255)) ';
-        }
-        $s = implode(' + ', $arr);
-        if ($s === '') {
-            return " '' ";
-        }
-        return " $s ";
-    }
-
-    public function sql_concat_join($separator="' '", $elements=array()) {
-        for ($n=count($elements)-1; $n > 0 ; $n--) {
-            array_splice($elements, $n, 0, $separator);
-        }
-        return call_user_func_array(array($this, 'sql_concat'), $elements);
-    }
-
-   public function sql_isempty($tablename, $fieldname, $nullablefield, $textfield) {
-        if ($textfield) {
-            return ' (' . $this->sql_compare_text($fieldname) . " = '') ";
-        } else {
-            return " ($fieldname = '') ";
-        }
-    }
-
-   /**
-     * Returns the SQL text to be used to calculate the length in characters of one expression.
-     * @param string fieldname or expression to calculate its length in characters.
-     * @return string the piece of SQL code to be used in the statement.
-     */
-    public function sql_length($fieldname) {
-        return ' LEN(' . $fieldname . ')';
-    }
-
-    public function sql_order_by_text($fieldname, $numchars=32) {
-        return " CONVERT(varchar({$numchars}), {$fieldname})";
-    }
-
-   /**
-     * Returns the SQL for returning searching one string for the location of another.
-     */
-    public function sql_position($needle, $haystack) {
-        return "CHARINDEX(($needle), ($haystack))";
-    }
-
-    /**
-     * Returns the proper substr() SQL text used to extract substrings from DB
-     * NOTE: this was originally returning only function name
-     *
-     * @param string $expr some string field, no aggregates
-     * @param mixed $start integer or expression evaluating to int
-     * @param mixed $length optional integer or expression evaluating to int
-     * @return string sql fragment
-     */
-    public function sql_substr($expr, $start, $length=false) {
-        if (count(func_get_args()) < 2) {
-            throw new coding_exception('moodle_database::sql_substr() requires at least two parameters', 'Originaly this function wa
-s only returning name of SQL substring function, it now requires all parameters.');
-        }
-        if ($length === false) {
-            return "SUBSTRING($expr, " . $this->sql_cast_char2int($start) . ", 2^31-1)";
-        } else {
-            return "SUBSTRING($expr, " . $this->sql_cast_char2int($start) . ", " . $this->sql_cast_char2int($length) . ")";
-        }
-    }
-
-    /**
-     * Does this driver support tool_replace?
-     *
-     * @since Moodle 2.6.1
-     * @return bool
-     */
-    public function replace_all_text_supported() {
-        return true;
-    }
-
-    public function session_lock_supported() {
-        return true;
-    }
-
-    /**
-     * Obtain session lock
-     * @param int $rowid id of the row with session record
-     * @param int $timeout max allowed time to wait for the lock in seconds
-     * @return bool success
-     */
-    public function get_session_lock($rowid, $timeout) {
-        if (!$this->session_lock_supported()) {
-            return;
-        }
-        parent::get_session_lock($rowid, $timeout);
-
-        $timeoutmilli = $timeout * 1000;
-
-        $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
-        // There is one bug in PHP/freetds (both reproducible with mssql_query()
-        // and its mssql_init()/mssql_bind()/mssql_execute() alternative) for
-        // stored procedures, causing scalar results of the execution
-        // to be cast to boolean (true/fals). Here there is one
-        // workaround that forces the return of one recordset resource.
-        // $sql = "sp_getapplock '$fullname', 'Exclusive', 'Session',  $timeoutmilli";
-        $sql = "BEGIN
-                    DECLARE @result INT
-                    EXECUTE @result = sp_getapplock @Resource='$fullname',
-                                                    @LockMode='Exclusive',
-                                                    @LockOwner='Session',
-                                                    @LockTimeout='$timeoutmilli'
-                    SELECT @result
-                END";
-        $this->query_start($sql, null, SQL_QUERY_AUX);
-        $result = mssql_query($sql, $this->mssql);
-        $this->query_end($result);
-
-        if ($result) {
-            $row = mssql_fetch_row($result);
-            if ($row[0] < 0) {
-                throw new dml_sessionwait_exception();
-            }
-        }
-
-        $this->free_result($result);
-    }
-
-    public function release_session_lock($rowid) {
-        if (!$this->session_lock_supported()) {
-            return;
-        }
-        if (!$this->used_for_db_sessions) {
-            return;
-        }
-
-        parent::release_session_lock($rowid);
-
-        $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
-        $sql = "sp_releaseapplock '$fullname', 'Session'";
-        $this->query_start($sql, null, SQL_QUERY_AUX);
-        $result = mssql_query($sql, $this->mssql);
-        $this->query_end($result);
-
-        $this->free_result($result);
-    }
-
-    /**
-     * Driver specific start of real database transaction,
-     * this can not be used directly in code.
-     * @return void
-     */
-    protected function begin_transaction() {
-        // requires database to run with READ_COMMITTED_SNAPSHOT ON
-        $sql = "BEGIN TRANSACTION"; // Will be using READ COMMITTED isolation
-        $this->query_start($sql, NULL, SQL_QUERY_AUX);
-        $result = mssql_query($sql, $this->mssql);
-        $this->query_end($result);
-
-        $this->free_result($result);
-    }
-
-    /**
-     * Driver specific commit of real database transaction,
-     * this can not be used directly in code.
-     * @return void
-     */
-    protected function commit_transaction() {
-        $sql = "COMMIT TRANSACTION";
-        $this->query_start($sql, NULL, SQL_QUERY_AUX);
-        $result = mssql_query($sql, $this->mssql);
-        $this->query_end($result);
-
-        $this->free_result($result);
-    }
-
-    /**
-     * Driver specific abort of real database transaction,
-     * this can not be used directly in code.
-     * @return void
-     */
-    protected function rollback_transaction() {
-        $sql = "ROLLBACK TRANSACTION";
-        $this->query_start($sql, NULL, SQL_QUERY_AUX);
-        $result = mssql_query($sql, $this->mssql);
-        $this->query_end($result);
-
-        $this->free_result($result);
-    }
-}
diff --git a/lib/dml/mssql_native_moodle_recordset.php b/lib/dml/mssql_native_moodle_recordset.php
deleted file mode 100644 (file)
index 39cf1eb..0000000
+++ /dev/null
@@ -1,94 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * MSSQL specific recordset.
- *
- * @package    core_dml
- * @copyright  2009 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-require_once(__DIR__.'/moodle_recordset.php');
-
-class mssql_native_moodle_recordset extends moodle_recordset {
-
-    protected $rsrc;
-    protected $current;
-
-    public function __construct($rsrc) {
-        $this->rsrc  = $rsrc;
-        $this->current = $this->fetch_next();
-    }
-
-    public function __destruct() {
-        $this->close();
-    }
-
-    private function fetch_next() {
-        if (!$this->rsrc) {
-            return false;
-        }
-        if (!$row = mssql_fetch_assoc($this->rsrc)) {
-            mssql_free_result($this->rsrc);
-            $this->rsrc = null;
-            return false;
-        }
-
-        $row = array_change_key_case($row, CASE_LOWER);
-        // Moodle expects everything from DB as strings.
-        foreach ($row as $k=>$v) {
-            if (is_null($v)) {
-                continue;
-            }
-            if (!is_string($v)) {
-                $row[$k] = (string)$v;
-            }
-        }
-        return $row;
-    }
-
-    public function current() {
-        return (object)$this->current;
-    }
-
-    public function key() {
-        // return first column value as key
-        if (!$this->current) {
-            return false;
-        }
-        $key = reset($this->current);
-        return $key;
-    }
-
-    public function next() {
-        $this->current = $this->fetch_next();
-    }
-
-    public function valid() {
-        return !empty($this->current);
-    }
-
-    public function close() {
-        if ($this->rsrc) {
-            mssql_free_result($this->rsrc);
-            $this->rsrc  = null;
-        }
-        $this->current = null;
-    }
-}
diff --git a/lib/dml/mssql_native_moodle_temptables.php b/lib/dml/mssql_native_moodle_temptables.php
deleted file mode 100644 (file)
index df04cca..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * MSSQL specific temptables store. Needed because temporary tables
- * are named differently than normal tables. Also used to be able to retrieve
- * temp table names included in the get_tables() method of the DB.
- *
- * @package    core_dml
- * @copyright  2009 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-require_once(__DIR__.'/moodle_temptables.php');
-
-class mssql_native_moodle_temptables extends moodle_temptables {
-
-    /**
-     * Add one temptable to the store.
-     *
-     * Overriden because MSSQL requires to add # for local (session) temporary
-     * tables before the prefix.
-     *
-     * Given one moodle temptable name (without prefix), add it to the store, with the
-     * key being the original moodle name and the value being the real db temptable name
-     * already prefixed
-     *
-     * Override and use this *only* if the database requires modification in the table name.
-     *
-     * @param string $tablename name without prefix of the table created as temptable
-     */
-    public function add_temptable($tablename) {
-        // TODO: throw exception if exists: if ($this->is_temptable...
-        $this->temptables[$tablename] = '#' . $this->prefix . $tablename;
-    }
-}
index 2d9cc62..ae733b8 100644 (file)
@@ -26,7 +26,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-require_once(__DIR__.'/mssql_native_moodle_temptables.php');
+require_once(__DIR__.'/moodle_temptables.php');
 
 /**
  * This class is not specific to the SQL Server Native Driver but rather
@@ -36,4 +36,24 @@ require_once(__DIR__.'/mssql_native_moodle_temptables.php');
  * @copyright  2009 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v2 or later
  */
-class sqlsrv_native_moodle_temptables extends mssql_native_moodle_temptables {}
+class sqlsrv_native_moodle_temptables extends moodle_temptables {
+
+    /**
+     * Add one temptable to the store.
+     *
+     * Overriden because SQLSRV requires to add # for local (session) temporary
+     * tables before the prefix.
+     *
+     * Given one moodle temptable name (without prefix), add it to the store, with the
+     * key being the original moodle name and the value being the real db temptable name
+     * already prefixed
+     *
+     * Override and use this *only* if the database requires modification in the table name.
+     *
+     * @param string $tablename name without prefix of the table created as temptable
+     */
+    public function add_temptable($tablename) {
+        // TODO: throw exception if exists: if ($this->is_temptable...
+        $this->temptables[$tablename] = '#' . $this->prefix . $tablename;
+    }
+}
index 58ab46b..1a2f514 100644 (file)
@@ -314,10 +314,6 @@ function setup_DB() {
                 $CFG->dbtype = 'pgsql';
                 break;
 
-            case 'mssql_n':
-                $CFG->dbtype = 'mssql';
-                break;
-
             case 'oci8po':
                 $CFG->dbtype = 'oci';
                 break;
index d6a5dab..0d48e7e 100644 (file)
@@ -4694,6 +4694,14 @@ function get_complete_user_data($field, $value, $mnethostid = null) {
         }
     }
 
+    // Add cohort theme.
+    if (!empty($CFG->allowcohortthemes)) {
+        require_once($CFG->dirroot . '/cohort/lib.php');
+        if ($cohorttheme = cohort_get_user_cohort_theme($user->id)) {
+            $user->cohorttheme = $cohorttheme;
+        }
+    }
+
     // Add the custom profile fields to the user record.
     $user->profile = array();
     if (!isguestuser($user)) {
index b088e09..1e52dec 100644 (file)
@@ -194,14 +194,16 @@ class renderer_base {
      * If will then be rendered by a method based upon the classname for the widget.
      * For instance a widget of class `crazywidget` will be rendered by a protected
      * render_crazywidget method of this renderer.
+     * If no render_crazywidget method exists and crazywidget implements templatable,
+     * look for the 'crazywidget' template in the same component and render that.
      *
      * @param renderable $widget instance with renderable interface
      * @return string
      */
     public function render(renderable $widget) {
-        $classname = get_class($widget);
+        $classparts = explode('\\', get_class($widget));
         // Strip namespaces.
-        $classname = preg_replace('/^.*\\\/', '', $classname);
+        $classname = array_pop($classparts);
         // Remove _renderable suffixes
         $classname = preg_replace('/_renderable$/', '', $classname);
 
@@ -209,6 +211,15 @@ class renderer_base {
         if (method_exists($this, $rendermethod)) {
             return $this->$rendermethod($widget);
         }
+        if ($widget instanceof templatable) {
+            $component = array_shift($classparts);
+            if (!$component) {
+                $component = 'core';
+            }
+            $template = $component . '/' . $classname;
+            $context = $widget->export_for_template($this);
+            return $this->render_from_template($template, $context);
+        }
         throw new coding_exception('Can not render widget, renderer method ('.$rendermethod.') not found.');
     }
 
index bd0c9f0..dab291a 100644 (file)
@@ -1608,7 +1608,7 @@ class moodle_page {
         global $CFG, $USER, $SESSION;
 
         if (empty($CFG->themeorder)) {
-            $themeorder = array('course', 'category', 'session', 'user', 'site');
+            $themeorder = array('course', 'category', 'session', 'user', 'cohort', 'site');
         } else {
             $themeorder = $CFG->themeorder;
             // Just in case, make sure we always use the site theme if nothing else matched.
@@ -1666,6 +1666,12 @@ class moodle_page {
                     }
                 break;
 
+                case 'cohort':
+                    if (!empty($CFG->allowcohortthemes) && !empty($USER->cohorttheme) && !$hascustomdevicetheme) {
+                        return $USER->cohorttheme;
+                    }
+                break;
+
                 case 'site':
                     if ($mnetpeertheme) {
                         return $mnetpeertheme;
index 1f4091e..514b3b1 100644 (file)
@@ -665,6 +665,112 @@ class core_moodle_page_testcase extends advanced_testcase {
         $footer = $OUTPUT->footer();
         $this->assertEmpty($footer, 'cli output does not have a footer.');
     }
+
+    /**
+     * Validate the theme value depending on the user theme and cohorts.
+     *
+     * @dataProvider get_user_theme_provider
+     */
+    public function test_cohort_get_user_theme($usertheme, $sitetheme, $cohortthemes, $expected) {
+        global $DB, $PAGE, $USER;
+
+        $this->resetAfterTest();
+
+        // Enable cohort themes.
+        set_config('allowuserthemes', 1);
+        set_config('allowcohortthemes', 1);
+
+        $systemctx = context_system::instance();
+
+        set_config('theme', $sitetheme);
+        // Create user.
+        $user = $this->getDataGenerator()->create_user(array('theme' => $usertheme));
+
+        // Create cohorts and add user as member.
+        $cohorts = array();
+        foreach ($cohortthemes as $cohorttheme) {
+            $cohort = $this->getDataGenerator()->create_cohort(array('contextid' => $systemctx->id, 'name' => 'Cohort',
+                'idnumber' => '', 'description' => '', 'theme' => $cohorttheme));
+            $cohorts[] = $cohort;
+            cohort_add_member($cohort->id, $user->id);
+        }
+
+        // Get the theme and compare to the expected.
+        $this->setUser($user);
+
+        // Initialise user theme.
+        $USER = get_complete_user_data('id', $user->id);
+
+        // Initialise site theme.
+        $PAGE->reset_theme_and_output();
+        $PAGE->initialise_theme_and_output();
+        $result = $PAGE->theme->name;
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
+     * Some user cases for validating the expected theme depending on the cohorts, site and user values.
+     *
+     * The result is an array of:
+     *     'User case description' => [
+     *      'usertheme' => '', // User theme.
+     *      'sitetheme' => '', // Site theme.
+     *      'cohorts' => [],   // Cohort themes.
+     *      'expected' => '',  // Expected value returned by cohort_get_user_cohort_theme.
+     *    ]
+     *
+     * @return array
+     */
+    public function get_user_theme_provider() {
+        return [
+            'User not a member of any cohort' => [
+                'usertheme' => '',
+                'sitetheme' => 'boost',
+                'cohorts' => [],
+                'expected' => 'boost',
+            ],
+            'User member of one cohort which has a theme set' => [
+                'usertheme' => '',
+                'sitetheme' => 'boost',
+                'cohorts' => [
+                    'clean',
+                ],
+                'expected' => 'clean',
+            ],
+            'User member of one cohort which has a theme set, and one without a theme' => [
+                'usertheme' => '',
+                'sitetheme' => 'boost',
+                'cohorts' => [
+                    'clean',
+                    '',
+                ],
+                'expected' => 'clean',
+            ],
+            'User member of one cohort which has a theme set, and one with a different theme' => [
+                'usertheme' => '',
+                'sitetheme' => 'boost',
+                'cohorts' => [
+                    'clean',
+                    'someother',
+                ],
+                'expected' => 'boost',
+            ],
+            'User with a theme but not a member of any cohort' => [
+                'usertheme' => 'more',
+                'sitetheme' => 'boost',
+                'cohorts' => [],
+                'expected' => 'more',
+            ],
+            'User with a theme and member of one cohort which has a theme set' => [
+                'usertheme' => 'more',
+                'sitetheme' => 'boost',
+                'cohorts' => [
+                    'clean',
+                ],
+                'expected' => 'more',
+            ],
+        ];
+    }
 }
 
 /**
index 7272751..0c1071a 100644 (file)
@@ -430,6 +430,10 @@ function upgrade_stale_php_files_present() {
     global $CFG;
 
     $someexamplesofremovedfiles = array(
+        // Removed in 3.5.
+        '/lib/dml/mssql_native_moodle_database.php',
+        '/lib/dml/mssql_native_moodle_recordset.php',
+        '/lib/dml/mssql_native_moodle_temptables.php',
         // Removed in 3.4.
         '/auth/README.txt',
         '/calendar/set.php',
index b3c6df9..5315e50 100644 (file)
@@ -85,6 +85,14 @@ class media_videojs_plugin extends core_media_player_native {
                 // Fix for VideoJS/Chrome bug https://github.com/videojs/video.js/issues/423 .
                 $mimetype = 'video/mp4';
             }
+            // If this is RTMP stream, adjust mimetype to those VideoJS suggests to use (either flash or mp4).
+            if ($url->get_scheme() === 'rtmp') {
+                if ($mimetype === 'video/x-flv') {
+                    $mimetype = 'rtmp/flv';
+                } else {
+                    $mimetype = 'rtmp/mp4';
+                }
+            }
             $source = html_writer::empty_tag('source', array('src' => $url, 'type' => $mimetype));
             $sources[] = $source;
             if ($isaudio === null) {
@@ -93,7 +101,8 @@ class media_videojs_plugin extends core_media_player_native {
             if ($responsive === null) {
                 $responsive = core_useragent::supports_html5($extension);
             }
-            if (!core_useragent::supports_html5($extension) && get_config('media_videojs', 'useflash')) {
+            if (($url->get_scheme() === 'rtmp' || !core_useragent::supports_html5($extension))
+                    && get_config('media_videojs', 'useflash')) {
                 $flashtech = true;
             }
         }
@@ -240,15 +249,28 @@ class media_videojs_plugin extends core_media_player_native {
             }
         }
 
-        if (!get_config('media_videojs', 'useflash')) {
-            return parent::list_supported_urls($urls, $options);
-        }
         // If Flash fallback is enabled we can not check if/when browser supports flash.
         $extensions = $this->get_supported_extensions();
+        $rtmpallowed = get_config('media_videojs', 'rtmp') && get_config('media_videojs', 'useflash');
         foreach ($urls as $url) {
-            $ext = core_media_manager::instance()->get_extension($url);
-            if (in_array('.' . $ext, $extensions)) {
+            // If RTMP support is disabled, skip the URL that is using RTMP (which
+            // might have been picked to the list by its valid extension).
+            if (!$rtmpallowed && ($url->get_scheme() === 'rtmp')) {
+                continue;
+            }
+            // If RTMP support is allowed, URL with RTMP scheme is supported irrespective to extension.
+            if ($rtmpallowed && ($url->get_scheme() === 'rtmp')) {
                 $result[] = $url;
+                continue;
+            }
+
+            if (!get_config('media_videojs', 'useflash')) {
+                return parent::list_supported_urls($urls, $options);
+            } else {
+                $ext = core_media_manager::instance()->get_extension($url);
+                if (in_array('.' . $ext, $extensions)) {
+                    $result[] = $url;
+                }
             }
         }
         return $result;
@@ -310,14 +332,23 @@ class media_videojs_plugin extends core_media_player_native {
         if (get_config('media_videojs', 'youtube')) {
             $supports .= ($supports ? '<br>' : '') . get_string('youtube', 'media_videojs');
         }
+        if (get_config('media_videojs', 'rtmp') && get_config('media_videojs', 'useflash')) {
+            $supports .= ($supports ? '<br>' : '') . get_string('rtmp', 'media_videojs');
+        }
         return $supports;
     }
 
     public function get_embeddable_markers() {
         $markers = parent::get_embeddable_markers();
+        // Add YouTube support if enabled.
         if (get_config('media_videojs', 'youtube')) {
             $markers = array_merge($markers, array('youtube.com', 'youtube-nocookie.com', 'youtu.be', 'y2u.be'));
         }
+        // Add RTMP support if enabled.
+        if (get_config('media_videojs', 'rtmp') && get_config('media_videojs', 'useflash')) {
+            $markers[] = 'rtmp://';
+        }
+
         return $markers;
     }
 
index ce750ec..a8c10cd 100644 (file)
@@ -29,6 +29,7 @@ $string['audioextensions'] = 'Audio file extensions';
 $string['configaudiocssclass'] = 'A CSS class that will be added to the &lt;audio&gt; element.';
 $string['configaudioextensions'] = 'A comma-separated list of supported audio file extensions. VideoJS will try to use the browser\'s native video player when available, and fall back to a Flash player for other formats if Flash is supported by the browser and Flash fallback is enabled below.';
 $string['configlimitsize'] = 'If enabled, and width and height are not specified, the video will display with default width and height. Otherwise it will stretch to the maximum possible width.';
+$string['configrtmp'] = 'If enabled, links that start with rtmp:// will be handled by the plugin, irrespective of whether its extension is enabled in the supported extensions setting. Flash fallback must be enabled for RTMP to work.';
 $string['configvideocssclass'] = 'A CSS class that will be added to the &lt;video&gt; element. For example, the CSS class "vjs-big-play-centered" will place the play button in the middle. For details, including how to set a custom skin, see docs.videojs.com.';
 $string['configvideoextensions'] = 'A comma-separated list of supported video file extensions. VideoJS will try to use the browser\'s native video player when available, and fall back to a Flash player for other formats if Flash is supported by the browser and Flash fallback is enabled below.';
 $string['configyoutube'] = 'Use VideoJS to play YouTube videos. Note that YouTube playlists are not yet supported by VideoJS.';
@@ -36,6 +37,7 @@ $string['configuseflash'] = 'Use Flash player if video format is not natively su
 $string['limitsize'] = 'Limit size';
 $string['pluginname'] = 'VideoJS player';
 $string['pluginname_help'] = 'A JavaScript wrapper for video files played by the browser\'s native video player with a Flash player fallback. (Format support depends on the browser.)';
+$string['rtmp'] = 'RTMP streams';
 $string['videoextensions'] = 'Video file extensions';
 $string['useflash'] = 'Use Flash fallback';
 $string['videocssclass'] = 'CSS class for video';
index 7d344c7..d3d2235 100644 (file)
@@ -36,6 +36,10 @@ if ($ADMIN->fulltree) {
         new lang_string('configaudioextensions', 'media_videojs'),
         'html_audio'));
 
+    $settings->add(new admin_setting_configcheckbox('media_videojs/rtmp',
+        new lang_string('rtmp', 'media_videojs'),
+        new lang_string('configrtmp', 'media_videojs'), 0));
+
     $settings->add(new admin_setting_configcheckbox('media_videojs/useflash',
         new lang_string('useflash', 'media_videojs'),
         new lang_string('configuseflash', 'media_videojs'), 0));
index 7bc50ea..cb94182 100644 (file)
@@ -79,6 +79,19 @@ class media_videojs_testcase extends advanced_testcase {
         $this->assertTrue(in_array('.mp3', $player->get_supported_extensions()));
         $this->assertFalse(in_array('.ra', $player->get_supported_extensions()));
         $this->assertEmpty(array_diff($player->get_supported_extensions(), $nativeextensions));
+
+        // Try to use flash extensions and make sure they are not returned as supported.
+        set_config('videoextensions', '.flv,.f4v', 'media_videojs');
+        $player = new media_videojs_plugin();
+        $this->assertFalse(in_array('.flv', $player->get_supported_extensions()));
+        $this->assertFalse(in_array('.f4v', $player->get_supported_extensions()));
+
+        // Enable flash and test if flash extenstions are supported.
+        set_config('useflash', 1, 'media_videojs');
+        set_config('videoextensions', '.flv,.f4v', 'media_videojs');
+        $player = new media_videojs_plugin();
+        $this->assertTrue(in_array('.flv', $player->get_supported_extensions()));
+        $this->assertTrue(in_array('.f4v', $player->get_supported_extensions()));
     }
 
     /**
@@ -228,6 +241,11 @@ class media_videojs_testcase extends advanced_testcase {
         $this->assertNotRegExp('~somethinginvalid~i', $content);
     }
 
+    /**
+     * Helper function for testing youtube videos embedding.
+     *
+     * @param string $t output of core_media_manager::embed_url.
+     */
     protected function youtube_plugin_engaged($t) {
         $this->assertContains('mediaplugin_videojs', $t);
         $this->assertContains('data-setup-lazy="{&quot;techOrder&quot;: [&quot;youtube&quot;]', $t);
@@ -281,4 +299,80 @@ class media_videojs_testcase extends advanced_testcase {
         $this->assertNotContains('mediaplugin_videojs', $t);
 
     }
+
+    /**
+     * Helper function for testing flash videos embedding.
+     *
+     * @param string $t output of core_media_manager::embed_url.
+     */
+    protected function flash_plugin_engaged($t) {
+        $this->assertContains('mediaplugin_videojs', $t);
+        $this->assertContains('data-setup-lazy="{&quot;techOrder&quot;: [&quot;flash&quot;, &quot;html5&quot;]', $t);
+    }
+
+    /**
+     * Test that VideoJS can embed flash videos.
+     */
+    public function test_flash() {
+        $manager = core_media_manager::instance();
+
+        // Flash enabled.
+        set_config('useflash', 1, 'media_videojs');
+        $url = new moodle_url('http://example.org/some_filename.flv');
+        $t = $manager->embed_url($url);
+        $this->flash_plugin_engaged($t);
+        $this->assertRegExp('~</video>~', $t);
+        $this->assertRegExp('~<source src="http://example.org/some_filename.flv"~', $t);
+        $this->assertRegExp('~<a class="mediafallbacklink" href="http://example.org/some_filename.flv">some_filename.flv</a>~', $t);
+
+        // Flash disabled.
+        set_config('useflash', 0, 'media_videojs');
+        $url = new moodle_url('http://example.org/some_filename.flv');
+        $t = $manager->embed_url($url);
+        $this->assertNotContains('mediaplugin_videojs', $t);
+        $this->assertRegExp('~<a class="mediafallbacklink" href="http://example.org/some_filename.flv">some_filename.flv</a>~', $t);
+    }
+
+    /**
+     * Test that VideoJS can embed RTMP streams.
+     */
+    public function test_rtmp() {
+        $manager = core_media_manager::instance();
+
+        // RTMP disabled, flash disabled.
+        set_config('useflash', 0, 'media_videojs');
+        set_config('rtmp', 0, 'media_videojs');
+        $url = new moodle_url('rtmp://example.com/fms&mp4:path/to/file.mp4');
+        $t = $manager->embed_url($url);
+        $this->assertNotContains('mediaplugin_videojs', $t);
+        $this->assertRegExp('~<a class="mediafallbacklink" href="rtmp://example.com/fms&mp4:path/to/file.mp4">file.mp4</a>~', $t);
+
+        // RTMP enabled, flash disabled.
+        set_config('useflash', 0, 'media_videojs');
+        set_config('rtmp', 1, 'media_videojs');
+        $url = new moodle_url('rtmp://example.com/fms&mp4:path/to/file.mp4');
+        $t = $manager->embed_url($url);
+        $this->assertNotContains('mediaplugin_videojs', $t);
+        $this->assertRegExp('~<a class="mediafallbacklink" href="rtmp://example.com/fms&mp4:path/to/file.mp4">file.mp4</a>~', $t);
+
+        // RTMP enabled, flash enabled, rtmp/mp4 type expected.
+        set_config('useflash', 1, 'media_videojs');
+        set_config('rtmp', 1, 'media_videojs');
+        $url = new moodle_url('rtmp://example.com/fms&mp4:path/to/file.mp4');
+        $t = $manager->embed_url($url);
+        $this->flash_plugin_engaged($t);
+        $this->assertRegExp('~</video>~', $t);
+        $this->assertRegExp('~<source src="rtmp://example.com/fms&mp4:path/to/file.mp4" type="rtmp/mp4"~', $t);
+        $this->assertRegExp('~<a class="mediafallbacklink" href="rtmp://example.com/fms&mp4:path/to/file.mp4">file.mp4</a>~', $t);
+
+        // RTMP enabled, flash enabled, rtmp/flv type expected.
+        set_config('useflash', 1, 'media_videojs');
+        set_config('rtmp', 1, 'media_videojs');
+        $url = new moodle_url('rtmp://example.com/fms&flv:path/to/file.flv');
+        $t = $manager->embed_url($url);
+        $this->flash_plugin_engaged($t);
+        $this->assertRegExp('~</video>~', $t);
+        $this->assertRegExp('~<source src="rtmp://example.com/fms&flv:path/to/file.flv" type="rtmp/flv"~', $t);
+        $this->assertRegExp('~<a class="mediafallbacklink" href="rtmp://example.com/fms&flv:path/to/file.flv">file.flv</a>~', $t);
+    }
 }
index 136e57e..902455c 100644 (file)
@@ -165,7 +165,7 @@ if ($xml = glossary_read_imported_file($result)) {
     }
 
     $xmlentries = $xml['GLOSSARY']['#']['INFO'][0]['#']['ENTRIES'][0]['#']['ENTRY'];
-    $sizeofxmlentries = sizeof($xmlentries);
+    $sizeofxmlentries = is_array($xmlentries) ? count($xmlentries) : 0;
     for($i = 0; $i < $sizeofxmlentries; $i++) {
         // Inserting the entries
         $xmlentry = $xmlentries[$i];
@@ -230,7 +230,7 @@ if ($xml = glossary_read_imported_file($result)) {
             $importedentries++;
 
             $xmlaliases = @$xmlentry['#']['ALIASES'][0]['#']['ALIAS']; // ignore missing ALIASES
-            $sizeofxmlaliases = sizeof($xmlaliases);
+            $sizeofxmlaliases = is_array($xmlaliases) ? count($xmlaliases) : 0;
             for($k = 0; $k < $sizeofxmlaliases; $k++) {
             /// Importing aliases
                 $xmlalias = $xmlaliases[$k];
@@ -247,7 +247,7 @@ if ($xml = glossary_read_imported_file($result)) {
             if (!empty($data->catsincl)) {
                 // If the categories must be imported...
                 $xmlcats = @$xmlentry['#']['CATEGORIES'][0]['#']['CATEGORY']; // ignore missing CATEGORIES
-                $sizeofxmlcats = sizeof($xmlcats);
+                $sizeofxmlcats = is_array($xmlcats) ? count($xmlcats) : 0;
                 for($k = 0; $k < $sizeofxmlcats; $k++) {
                     $xmlcat = $xmlcats[$k];
 
@@ -280,6 +280,19 @@ if ($xml = glossary_read_imported_file($result)) {
                 $DB->update_record("glossary_entries", array('id' => $newentry->id, 'attachment' => '1'));
             }
 
+            // Import tags associated with the entry.
+            if (core_tag_tag::is_enabled('mod_glossary', 'glossary_entries')) {
+                $xmltags = @$xmlentry['#']['TAGS'][0]['#']['TAG']; // Ignore missing TAGS.
+                $sizeofxmltags = is_array($xmltags) ? count($xmltags) : 0;
+                for ($k = 0; $k < $sizeofxmltags; $k++) {
+                    // Importing tags.
+                    $tag = $xmltags[$k]['#'];
+                    if (!empty($tag)) {
+                        core_tag_tag::add_item_tag('mod_glossary', 'glossary_entries', $newentry->id, $glossarycontext, $tag);
+                    }
+                }
+            }
+
         } else {
             $entriesrejected++;
             if ( $newentry->concept and $newentry->definition ) {
index 83ec65d..66477cb 100644 (file)
@@ -2387,6 +2387,16 @@ function glossary_generate_export_file($glossary, $ignored = "", $hook = 0) {
                     // Export attachments.
                     $co .= glossary_xml_export_files('ATTACHMENTFILES', 4, $context->id, 'attachment', $entry->id);
 
+                    // Export tags.
+                    $tags = core_tag_tag::get_item_tags_array('mod_glossary', 'glossary_entries', $entry->id);
+                    if (count($tags)) {
+                        $co .= glossary_start_tag("TAGS", 4, true);
+                        foreach ($tags as $tag) {
+                            $co .= glossary_full_tag("TAG", 5, false, $tag);
+                        }
+                        $co .= glossary_end_tag("TAGS", 4, true);
+                    }
+
                     $co .= glossary_end_tag("ENTRY",3,true);
                 }
             }
index c33372c..8bb84b9 100644 (file)
@@ -32,3 +32,16 @@ Feature: Importing glossary entries
     And I am on "Course 1" course homepage
     And I should see "Added Glossary" in the "Recent activity" "block"
     And I should see "New glossary entries:" in the "Recent activity" "block"
+
+  @javascript @block_tags
+  Scenario: Importing glossary entries and checking Tags block
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add the "Tags" block
+    And I follow "Glossary 1"
+    And I navigate to "Import entries" in current page administration
+    And I upload "mod/glossary/tests/fixtures/musicians.xml" file to "File to import" filemanager
+    When I press "Submit"
+    And I am on "Course 1" course homepage
+    And I click on "Beatles" "link" in the "Tags" "block"
+    Then I should see "Paul McCartney"
diff --git a/mod/glossary/tests/fixtures/musicians.xml b/mod/glossary/tests/fixtures/musicians.xml
new file mode 100644 (file)
index 0000000..442590d
--- /dev/null
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<GLOSSARY>
+  <INFO>
+    <NAME>Musicians</NAME>
+    <INTRO></INTRO>
+    <INTROFORMAT>1</INTROFORMAT>
+    <ALLOWDUPLICATEDENTRIES>0</ALLOWDUPLICATEDENTRIES>
+    <DISPLAYFORMAT>dictionary</DISPLAYFORMAT>
+    <SHOWSPECIAL>1</SHOWSPECIAL>
+    <SHOWALPHABET>1</SHOWALPHABET>
+    <SHOWALL>1</SHOWALL>
+    <ALLOWCOMMENTS>0</ALLOWCOMMENTS>
+    <USEDYNALINK>1</USEDYNALINK>
+    <DEFAULTAPPROVAL>1</DEFAULTAPPROVAL>
+    <GLOBALGLOSSARY>0</GLOBALGLOSSARY>
+    <ENTBYPAGE>10</ENTBYPAGE>
+    <ENTRIES>
+      <ENTRY>
+        <CONCEPT>Paul McCartney</CONCEPT>
+        <DEFINITION>&lt;p&gt;Popular British composer, guitarist, and vocalist. &lt;br&gt;&lt;/p&gt;</DEFINITION>
+        <FORMAT>1</FORMAT>
+        <USEDYNALINK>1</USEDYNALINK>
+        <CASESENSITIVE>0</CASESENSITIVE>
+        <FULLMATCH>0</FULLMATCH>
+        <TEACHERENTRY>1</TEACHERENTRY>
+        <TAGS>
+          <TAG>Beatles</TAG>
+          <TAG>The Quarrymen</TAG>
+          <TAG>Wings</TAG>
+        </TAGS>
+      </ENTRY>
+    </ENTRIES>
+  </INFO>
+</GLOSSARY>
index 4d617fe..1fdb72e 100644 (file)
@@ -28,6 +28,7 @@ namespace mod_lti\local\ltiservice;
 
 defined('MOODLE_INTERNAL') || die();
 
+global $CFG;
 require_once($CFG->dirroot . '/mod/lti/locallib.php');
 
 
@@ -41,7 +42,16 @@ require_once($CFG->dirroot . '/mod/lti/locallib.php');
  */
 abstract class resource_base {
 
-    /** @var object Service associated with this resource. */
+    /**  HTTP Post method */
+    const HTTP_POST = 'POST';
+    /**  HTTP Get method */
+    const HTTP_GET = 'GET';
+    /**  HTTP Put method */
+    const HTTP_PUT = 'PUT';
+    /**  HTTP Delete method */
+    const HTTP_DELETE = 'DELETE';
+
+    /** @var service_base Service associated with this resource. */
     private $service;
     /** @var string Type for this resource. */
     protected $type;
@@ -62,7 +72,7 @@ abstract class resource_base {
     /**
      * Class constructor.
      *
-     * @param mod_lti\local\ltiservice\service_base $service Service instance
+     * @param service_base $service Service instance
      */
     public function __construct($service) {
 
@@ -125,7 +135,7 @@ abstract class resource_base {
     /**
      * Get the resource's service.
      *
-     * @return mod_lti\local\ltiservice\service_base
+     * @return mixed
      */
     public function get_service() {
 
@@ -190,7 +200,7 @@ abstract class resource_base {
     /**
      * Execute the request for this resource.
      *
-     * @param mod_lti\local\ltiservice\response $response  Response object for this request.
+     * @param response $response  Response object for this request.
      */
     public abstract function execute($response);
 
@@ -224,7 +234,7 @@ abstract class resource_base {
                     }
                 }
                 if (!$ok) {
-                    debugging('Requested service not included in tool proxy: ' . $this->get_id());
+                    debugging('Requested service not included in tool proxy: ' . $this->get_id(), DEBUG_DEVELOPER);
                 }
             }
         }
@@ -233,6 +243,45 @@ abstract class resource_base {
 
     }
 
+    /**
+     * Check to make sure the request is valid.
+     *
+     * @param int $typeid                   The typeid we want to use
+     * @param int $contextid                The course we are at
+     * @param string $permissionrequested   The permission to be checked
+     * @param string $body                  Body of HTTP request message
+     *
+     * @return boolean
+     */
+    public function check_type($typeid, $contextid, $permissionrequested, $body = null) {
+        $ok = false;
+        if ($this->get_service()->check_type($typeid, $contextid, $body)) {
+            $neededpermissions = $this->get_permissions($typeid);
+            foreach ($neededpermissions as $permission) {
+                if ($permission == $permissionrequested) {
+                    $ok = true;
+                    break;
+                }
+            }
+            if (!$ok) {
+                debugging('Requested service ' . $permissionrequested . ' not included in tool type: ' . $typeid,
+                    DEBUG_DEVELOPER);
+            }
+        }
+        return $ok;
+
+    }
+
+    /**
+     * get permissions from the config of the tool for that resource
+     *
+     * @param int $ltitype Type of LTI
+     * @return array with the permissions related to this resource by the $ltitype or empty if none.
+     */
+    public function get_permissions($ltitype) {
+        return array();
+    }
+
     /**
      * Parse a value for custom parameter substitution variables.
      *
index 5029d21..8f4bcab 100644 (file)
@@ -54,6 +54,8 @@ class response {
     private $body;
     /** @var array HTTP response codes. */
     private $responsecodes;
+    /** @var array HTTP additional headers. */
+    private $additionalheaders;
 
     /**
      * Class constructor.
@@ -83,6 +85,7 @@ class response {
             500 => 'Internal Server Error',
             501 => 'Not Implemented'
         );
+        $this->additionalheaders = array();
 
     }
 
@@ -202,11 +205,23 @@ class response {
         $this->body = $body;
     }
 
+    /**
+     * Add an additional header.
+     *
+     * @param string $header The new header
+     */
+    public function add_additional_header($header) {
+        array_push($this->additionalheaders, $header);
+    }
+
     /**
      * Send the response.
      */
     public function send() {
         header("HTTP/1.0 {$this->code} {$this->get_reason()}");
+        foreach ($this->additionalheaders as $header) {
+            header($header);
+        }
         if (($this->code >= 200) && ($this->code < 300)) {
             if (!empty($this->contenttype)) {
                 header("Content-Type: {$this->contenttype};charset=UTF-8");
index 73726bd..26a89c1 100644 (file)
@@ -28,11 +28,13 @@ namespace mod_lti\local\ltiservice;
 
 defined('MOODLE_INTERNAL') || die();
 
+global $CFG;
 require_once($CFG->dirroot . '/mod/lti/locallib.php');
 require_once($CFG->dirroot . '/mod/lti/OAuthBody.php');
 
 // TODO: Switch to core oauthlib once implemented - MDL-30149.
 use moodle\mod\lti as lti;
+use stdClass;
 
 
 /**
@@ -133,10 +135,80 @@ abstract class service_base {
     /**
      * Get the resources for this service.
      *
-     * @return array
+     * @return resource_base[]
      */
     abstract public function get_resources();
 
+    /**
+     * Returns the configuration options for this service.
+     *
+     * @param \MoodleQuickForm $mform Moodle quickform object definition
+     */
+    public function get_configuration_options(&$mform) {
+
+    }
+
+    /**
+     * Return an array with the names of the parameters that the service will be saving in the configuration
+     *
+     * @return array  Names list of the parameters that the service will be saving in the configuration
+     */
+    public function get_configuration_parameter_names() {
+        return array();
+    }
+
+    /**
+     * Default implementation will check for the existence of at least one mod_lti entry for that tool and context.
+     *
+     * It may be overridden if other inferences can be done.
+     *
+     * Ideally a Site Tool should be explicitly engaged with a course, the check on the presence of a link is a proxy
+     * to infer a Site Tool engagement until an explicit Site Tool - Course relationship exists.
+     *
+     * @param int $typeid The tool lti type id.
+     * @param int $courseid The course id.
+     * @return bool returns True if tool is used in context, false otherwise.
+     */
+    public function is_used_in_context($typeid, $courseid) {
+        global $DB;
+
+        $ok = $DB->record_exists('lti', array('course' => $courseid, 'typeid' => $typeid));
+        return $ok || $DB->record_exists('lti_types', array('course' => $courseid, 'id' => $typeid));
+    }
+
+    /**
+     * Checks if there is a site tool or a course tool for this site.
+     *
+     * @param int $typeid The tool lti type id.
+     * @param int $courseid The course id.
+     * @return bool returns True if tool is allowed in context, false otherwise.
+     */
+    public function is_allowed_in_context($typeid, $courseid) {
+        global $DB;
+
+        // Check if it is a Course tool for this course or a Site tool.
+        $type = $DB->get_record('lti_types', array('id' => $typeid));
+
+        return $type && ($type->course == $courseid || $type->course == SITEID);
+    }
+
+    /**
+     * Return an array of key/values to add to the launch parameters.
+     *
+     * @param string $messagetype  'basic-lti-launch-request' or 'ContentItemSelectionRequest'.
+     * @param string $courseid     The course id.
+     * @param string $userid       The user id.
+     * @param string $typeid       The tool lti type id.
+     * @param string $modlti       The id of the lti activity.
+     *
+     * The type is passed to check the configuration and not return parameters for services not used.
+     *
+     * @return array Key/value pairs to add as launch parameters.
+     */
+    public function get_launch_parameters($messagetype, $courseid, $userid, $typeid, $modlti = null) {
+        return array();
+    }
+
     /**
      * Get the path for service requests.
      *
@@ -202,9 +274,35 @@ abstract class service_base {
         if ($ok) {
             $this->toolproxy = $toolproxy;
         }
-
         return $ok;
+    }
 
+    /**
+     * Check that the request has been properly signed.
+     *
+     * @param int $typeid The tool id
+     * @param int $courseid The course we are at
+     * @param string $body Request body (null if none)
+     *
+     * @return bool
+     */
+    public function check_type($typeid, $courseid, $body = null) {
+        $ok = false;
+        $tool = null;
+        $consumerkey = lti\get_oauth_key_from_headers();
+        if (empty($typeid)) {
+            return $ok;
+        } else if ($this->is_allowed_in_context($typeid, $courseid)) {
+            $tool = lti_get_type_type_config($typeid);
+            if ($tool !== false) {
+                if (!$this->is_unsigned() && ($tool->lti_resourcekey == $consumerkey)) {
+                    $ok = $this->check_signature($tool->lti_resourcekey, $tool->lti_password, $body);
+                } else {
+                    $ok = $this->is_unsigned();
+                }
+            }
+        }
+        return $ok;
     }
 
     /**
index 8db9ca5..5e618de 100644 (file)
 
 defined('MOODLE_INTERNAL') || die;
 
+global $CFG;
 require_once($CFG->libdir.'/formslib.php');
 require_once($CFG->dirroot.'/mod/lti/locallib.php');
 
-class mod_lti_edit_types_form extends moodleform{
+/**
+ * LTI Edit Form
+ *
+ * @package    mod_lti
+ * @copyright  2009 Marc Alier, Jordi Piguillem, Nikolas Galanis
+ *  marc.alier@upc.edu
+ * @copyright  2009 Universitat Politecnica de Catalunya http://www.upc.edu
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mod_lti_edit_types_form extends moodleform {
+
+    /**
+     * Define this form.
+     */
     public function definition() {
         global $CFG;
 
@@ -147,6 +161,16 @@ class mod_lti_edit_types_form extends moodleform{
             $mform->disabledIf('lti_contentitem', null);
         }
 
+        $mform->addElement('text', 'lti_toolurl_ContentItemSelectionRequest',
+            get_string('toolurl_contentitemselectionrequest', 'lti'), array('size' => '64'));
+        $mform->setType('lti_toolurl_ContentItemSelectionRequest', PARAM_URL);
+        $mform->setAdvanced('lti_toolurl_ContentItemSelectionRequest');
+        $mform->addHelpButton('lti_toolurl_ContentItemSelectionRequest', 'toolurl_contentitemselectionrequest', 'lti');
+        $mform->disabledIf('lti_toolurl_ContentItemSelectionRequest', 'lti_contentitem', 'notchecked');
+        if ($istool) {
+            $mform->disabledIf('lti_toolurl__ContentItemSelectionRequest', null);
+        }
+
         $mform->addElement('hidden', 'oldicon');
         $mform->setType('oldicon', PARAM_URL);
 
@@ -160,6 +184,11 @@ class mod_lti_edit_types_form extends moodleform{
         $mform->setAdvanced('lti_secureicon');
         $mform->addHelpButton('lti_secureicon', 'secure_icon_url', 'lti');
 
+        if (!$istool) {
+            // Display the lti advantage services.
+            $this->get_lti_advantage_services($mform);
+        }
+
         if (!$istool) {
             // Add privacy preferences fieldset where users choose whether to send their data.
             $mform->addElement('header', 'privacy', get_string('privacy', 'lti'));
@@ -253,4 +282,19 @@ class mod_lti_edit_types_form extends moodleform{
         }
         return $data;
     }
+
+    /**
+     * Generates the lti advantage extra configuration adding it to the mform
+     *
+     * @param MoodleQuickForm $mform
+     */
+    public function get_lti_advantage_services(&$mform) {
+        // For each service add the label and get the array of configuration.
+        $services = lti_get_services();
+        $mform->addElement('header', 'services', get_string('services', 'lti'));
+        foreach ($services as $service) {
+            /** @var \mod_lti\local\ltiservice\service_base $service */
+            $service->get_configuration_options($mform);
+        }
+    }
 }
index 11391c1..12e6800 100644 (file)
@@ -510,9 +510,10 @@ A base URL of *quiz.tool.com* would match the following:
 If two different tool configurations are for the same domain, the most specific match will be used.
 
 You can also insert a cartridge URL if you have one and the details for the tool will be automatically filled.';
+$string['toolurl_contentitemselectionrequest'] = 'Content Selection URL';
+$string['toolurl_contentitemselectionrequest_help'] = 'The Content Selection URL will be used to launch the content selection page from the tool provider. If it is empty, the Tool URL will be used';
 $string['typename'] = 'Tool name';
-$string['typename_help'] = 'The tool name is used to identify the tool provider within Moodle. The name entered will be visible
-to teachers when adding external tools within courses.';
+$string['typename_help'] = 'The tool name is used to identify the tool provider within Moodle. The name entered will be visible to teachers when adding external tools within courses.';
 $string['types'] = 'Types';
 $string['unabletocreatetooltype'] = 'Unable to create tool';
 $string['unabletofindtooltype'] = 'Unable to find tool for {$a->id}';
index 13ca7c5..b501b2c 100644 (file)
@@ -53,6 +53,7 @@ defined('MOODLE_INTERNAL') || die;
 // TODO: Switch to core oauthlib once implemented - MDL-30149.
 use moodle\mod\lti as lti;
 
+global $CFG;
 require_once($CFG->dirroot.'/mod/lti/OAuth.php');
 require_once($CFG->libdir.'/weblib.php');
 require_once($CFG->dirroot . '/course/modlib.php');
@@ -96,7 +97,7 @@ define('LTI_VERSION_2', 'LTI-2p0');
  * @since  Moodle 3.0
  */
 function lti_get_launch_data($instance) {
-    global $PAGE, $CFG;
+    global $PAGE, $CFG, $USER;
 
     if (empty($instance->typeid)) {
         $tool = lti_get_tool_by_url_match($instance->toolurl, $instance->course);
@@ -230,6 +231,18 @@ function lti_get_launch_data($instance) {
 
     $requestparams['launch_presentation_return_url'] = $returnurl;
 
+    // Add the parameters configured by the LTI advantage services.
+    if ($typeid && !$islti2) {
+        $services = lti_get_services();
+        foreach ($services as $service) {
+            $ltiadvantageparameters = $service->get_launch_parameters('basic-lti-launch-request',
+                    $course->id, $USER->id , $typeid, $instance->id);
+            foreach ($ltiadvantageparameters as $ltiadvantagekey => $ltiadvantagevalue) {
+                $requestparams[$ltiadvantagekey] = $ltiadvantagevalue;
+            }
+        }
+    }
+
     // Allow request params to be updated by sub-plugins.
     $plugins = core_component::get_plugin_list('ltisource');
     foreach (array_keys($plugins) as $plugin) {
@@ -284,7 +297,7 @@ function lti_launch_tool($instance) {
 /**
  * Prepares an LTI registration request message
  *
- * $param object $instance       Tool Proxy instance object
+ * @param object $toolproxy  Tool Proxy instance object
  */
 function lti_register($toolproxy) {
     $endpoint = $toolproxy->regurl;
@@ -617,6 +630,8 @@ function lti_build_custom_parameters($toolproxy, $tool, $instance, $params, $cus
 function lti_build_content_item_selection_request($id, $course, moodle_url $returnurl, $title = '', $text = '', $mediatypes = [],
                                                   $presentationtargets = [], $autocreate = false, $multiple = false,
                                                   $unsigned = false, $canconfirm = false, $copyadvice = false) {
+    global $USER;
+
     $tool = lti_get_type($id);
     // Validate parameters.
     if (!$tool) {
@@ -693,6 +708,18 @@ function lti_build_content_item_selection_request($id, $course, moodle_url $retu
         $requestparams = array_merge($requestparams, $lti2params);
     }
 
+    // Add the parameters configured by the LTI advantage services.
+    if ($id && !$islti2) {
+        $services = lti_get_services();
+        foreach ($services as $service) {
+            $ltiadvantageparameters = $service->get_launch_parameters('ContentItemSelectionRequest',
+                    $course->id, $USER->id , $id);
+            foreach ($ltiadvantageparameters as $ltiadvantagekey => $ltiadvantagevalue) {
+                $requestparams[$ltiadvantagekey] = $ltiadvantagevalue;
+            }
+        }
+    }
+
     // Get standard request parameters and merge to the request parameters.
     $orgid = !empty($typeconfig['organizationid']) ? $typeconfig['organizationid'] : '';
     $standardparams = lti_build_standard_request(null, $orgid, $islti2, 'ContentItemSelectionRequest');
@@ -856,9 +883,6 @@ function lti_tool_configuration_from_content_item($typeid, $messagetype, $ltiver
     if (empty($items)) {
         throw new moodle_exception('errorinvaliddata', 'mod_lti', '', $contentitemsjson);
     }
-    if ($items->{'@context'} !== 'http://purl.imsglobal.org/ctx/lti/v1/ContentItem') {
-        throw new moodle_exception('errorinvalidmediatype', 'mod_lti', '', $items->{'@context'});
-    }
     if (!isset($items->{'@graph'}) || !is_array($items->{'@graph'}) || (count($items->{'@graph'}) > 1)) {
         throw new moodle_exception('errorinvalidresponseformat', 'mod_lti');
     }
@@ -922,7 +946,7 @@ function lti_tool_configuration_from_content_item($typeid, $messagetype, $ltiver
 }
 
 function lti_get_tool_table($tools, $id) {
-    global $CFG, $OUTPUT, $USER;
+    global $OUTPUT;
     $html = '';
 
     $typename = get_string('typename', 'lti');
@@ -1124,9 +1148,9 @@ EOD;
 /**
  * Extracts the enabled capabilities into an array, including those implicitly declared in a parameter
  *
- * @param object    $tool           Tool instance object
+ * @param object $tool  Tool instance object
  *
- * @return Array of enabled capabilities
+ * @return array List of enabled capabilities
  */
 function lti_get_enabled_capabilities($tool) {
     if (!isset($tool)) {
@@ -1224,10 +1248,11 @@ function lti_get_custom_parameters($toolproxy, $tool, $params, $parameters) {
  * @param string    $value          Custom parameter value
  * @param boolean   $islti2         True if an LTI 2 tool is being launched
  *
- * @return Parsed value of custom parameter
+ * @return string Parsed value of custom parameter
  */
 function lti_parse_custom_parameter($toolproxy, $tool, $params, $value, $islti2) {
-    global $USER, $COURSE;
+    // This is required as {${$valarr[0]}->{$valarr[1]}}" may be using the USER var.
+    global $USER;
 
     if ($value) {
         if (substr($value, 0, 1) == '\\') {
@@ -1403,8 +1428,6 @@ function lti_get_tools_by_url($url, $state, $courseid = null) {
 function lti_get_tools_by_domain($domain, $state = null, $courseid = null) {
     global $DB, $SITE;
 
-    $filters = array('tooldomain' => $domain);
-
     $statefilter = '';
     $coursefilter = '';
 
@@ -1433,6 +1456,9 @@ function lti_get_tools_by_domain($domain, $state = null, $courseid = null) {
 /**
  * Returns all basicLTI tools configured by the administrator
  *
+ * @param int $course
+ *
+ * @return array
  */
 function lti_filter_get_types($course) {
     global $DB;
@@ -1698,7 +1724,7 @@ function lti_delete_type($id) {
 function lti_set_state_for_type($id, $state) {
     global $DB;
 
-    $DB->update_record('lti_types', array('id' => $id, 'state' => $state));
+    $DB->update_record('lti_types', (object)array('id' => $id, 'state' => $state));
 }
 
 /**
@@ -1709,7 +1735,6 @@ function lti_set_state_for_type($id, $state) {
  * @return array Basic LTI configuration details
  */
 function lti_get_config($ltiobject) {
-    $typeconfig = array();
     $typeconfig = (array)$ltiobject;
     $additionalconfig = lti_get_type_config($ltiobject->typeid);
     $typeconfig = array_merge($typeconfig, $additionalconfig);
@@ -1722,7 +1747,7 @@ function lti_get_config($ltiobject) {
  *
  * @param int $id
  *
- * @return Instance configuration
+ * @return object configuration
  *
  */
 function lti_get_type_config_from_instance($id) {
@@ -1760,7 +1785,7 @@ function lti_get_type_config_from_instance($id) {
  *
  * @param int $id
  *
- * @return Configuration details
+ * @return stdClass Configuration details
  */
 function lti_get_type_type_config($id) {
     global $DB;
@@ -1847,6 +1872,10 @@ function lti_get_type_type_config($id) {
         $type->lti_contentitem = $config['contentitem'];
     }
 
+    if (isset($config['toolurl_ContentItemSelectionRequest'])) {
+        $type->lti_toolurl_ContentItemSelectionRequest = $config['toolurl_ContentItemSelectionRequest'];
+    }
+
     if (isset($config['debuglaunch'])) {
         $type->lti_debuglaunch = $config['debuglaunch'];
     }
@@ -1855,6 +1884,19 @@ function lti_get_type_type_config($id) {
         $type->lti_module_class_type = $config['module_class_type'];
     }
 
+    // Get the parameters from the LTI services.
+    $services = lti_get_services();
+    $ltiserviceprefixlength = 11;
+    foreach ($services as $service) {
+        $configurationparameters = $service->get_configuration_parameter_names();
+        foreach ($configurationparameters as $ltiserviceparameter) {
+            $shortltiserviceparameter = substr($ltiserviceparameter, $ltiserviceprefixlength);
+            if (isset($config[$shortltiserviceparameter])) {
+                $type->$ltiserviceparameter = $config[$shortltiserviceparameter];
+            }
+        }
+    }
+
     return $type;
 }
 
@@ -1886,6 +1928,14 @@ function lti_prepare_type_for_save($type, $config) {
         $type->contentitem = !empty($config->lti_contentitem) ? $config->lti_contentitem : 0;
         $config->lti_contentitem = $type->contentitem;
     }
+    if (isset($config->lti_toolurl_ContentItemSelectionRequest)) {
+        if (!empty($config->lti_toolurl_ContentItemSelectionRequest)) {
+            $type->toolurl_ContentItemSelectionRequest = $config->lti_toolurl_ContentItemSelectionRequest;
+        } else {
+            $type->toolurl_ContentItemSelectionRequest = '';
+        }
+        $config->lti_toolurl_ContentItemSelectionRequest = $type->toolurl_ContentItemSelectionRequest;
+    }
 
     $type->timemodified = time();
 
@@ -1901,7 +1951,6 @@ function lti_update_type($type, $config) {
 
     lti_prepare_type_for_save($type, $config);
 
-    $clearcache = false;
     if (lti_request_is_using_ssl() && !empty($type->secureicon)) {
         $clearcache = !isset($config->oldicon) || ($config->oldicon !== $type->secureicon);
     } else {
@@ -1918,6 +1967,13 @@ function lti_update_type($type, $config) {
                 $record->value = $value;
                 lti_update_config($record);
             }
+            if (substr($key, 0, 11) == 'ltiservice_' && !is_null($value)) {
+                $record = new \StdClass();
+                $record->typeid = $type->id;
+                $record->name = substr($key, 11);
+                $record->value = $value;
+                lti_update_config($record);
+            }
         }
         require_once($CFG->libdir.'/modinfolib.php');
         if ($clearcache) {
@@ -1964,10 +2020,17 @@ function lti_add_type($type, $config) {
 
     if ($id) {
         foreach ($config as $key => $value) {
-            if (substr($key, 0, 4) == 'lti_' && !is_null($value)) {
+            if (!is_null($value)) {
+                $fieldparts = preg_split("/(lti|ltiservice)_/i", $key);
+                // If array has only one element, it did not start with the pattern.
+                if (count($fieldparts) < 2) {
+                    continue;
+                }
+                $fieldname = $fieldparts[1];
+
                 $record = new \StdClass();
                 $record->typeid = $id;
-                $record->name = substr($key, 4);
+                $record->name = $fieldname;
                 $record->value = $value;
 
                 lti_add_config($record);
@@ -2033,7 +2096,7 @@ function lti_get_tool_proxies_from_registration_url($regurl) {
  *
  * @param int $id
  *
- * @return Tool Proxy details
+ * @return mixed Tool Proxy details
  */
 function lti_get_tool_proxy($id) {
     global $DB;
@@ -2052,7 +2115,6 @@ function lti_get_tool_proxies($orphanedonly) {
     global $DB;
 
     if ($orphanedonly) {
-        $tools = $DB->get_records('lti_types');
         $usedproxyids = array_values($DB->get_fieldset_select('lti_types', 'toolproxyid', 'toolproxyid IS NOT NULL'));
         $proxies = $DB->get_records('lti_tool_proxies', null, 'state DESC, timemodified DESC');
         foreach ($proxies as $key => $value) {
@@ -2071,7 +2133,7 @@ function lti_get_tool_proxies($orphanedonly) {
  *
  * @param int $id
  *
- * @return Tool Proxy details
+ * @return mixed  Tool Proxy details
  */
 function lti_get_tool_proxy_config($id) {
     $toolproxy = lti_get_tool_proxy($id);
@@ -2190,12 +2252,11 @@ function lti_add_config($config) {
  *
  * @param object  $config   Tool configuration
  *
- * @return Record id number
+ * @return mixed Record id number
  */
 function lti_update_config($config) {
     global $DB;
 
-    $return = true;
     $old = $DB->get_record('lti_types_config', array('typeid' => $config->typeid, 'name' => $config->name));
 
     if ($old) {
@@ -2243,7 +2304,7 @@ function lti_set_tool_settings($settings, $toolproxyid, $courseid = null, $insta
     $record = $DB->get_record('lti_tool_settings', array('toolproxyid' => $toolproxyid,
         'course' => $courseid, 'coursemoduleid' => $instanceid));
     if ($record !== false) {
-        $DB->update_record('lti_tool_settings', array('id' => $record->id, 'settings' => $json, 'timemodified' => time()));
+        $DB->update_record('lti_tool_settings', (object)array('id' => $record->id, 'settings' => $json, 'timemodified' => time()));
     } else {
         $record = new \stdClass();
         $record->toolproxyid = $toolproxyid;
@@ -2259,11 +2320,12 @@ function lti_set_tool_settings($settings, $toolproxyid, $courseid = null, $insta
 /**
  * Signs the petition to launch the external tool using OAuth
  *
- * @param $oldparms     Parameters to be passed for signing
- * @param $endpoint     url of the external tool
- * @param $method       Method for sending the parameters (e.g. POST)
- * @param $oauth_consumoer_key          Key
- * @param $oauth_consumoer_secret       Secret
+ * @param array  $oldparms     Parameters to be passed for signing
+ * @param string $endpoint     url of the external tool
+ * @param string $method       Method for sending the parameters (e.g. POST)
+ * @param string $oauthconsumerkey
+ * @param string $oauthconsumersecret
+ * @return array|null
  */
 function lti_sign_parameters($oldparms, $endpoint, $method, $oauthconsumerkey, $oauthconsumersecret) {
 
@@ -2285,9 +2347,10 @@ function lti_sign_parameters($oldparms, $endpoint, $method, $oauthconsumerkey, $
 /**
  * Posts the launch petition HTML
  *
- * @param $newparms     Signed parameters
- * @param $endpoint     URL of the external tool
- * @param $debug        Debug (true/false)
+ * @param array $newparms   Signed parameters
+ * @param string $endpoint  URL of the external tool
+ * @param bool $debug       Debug (true/false)
+ * @return string
  */
 function lti_post_launch_html($newparms, $endpoint, $debug=false) {
     $r = "<form action=\"" . $endpoint .
@@ -2607,7 +2670,7 @@ function lti_get_services() {
  *
  * @param string $servicename Name of service
  *
- * @return mod_lti\local\ltiservice\service_base Service
+ * @return bool|\mod_lti\local\ltiservice\service_base Service
  */
 function lti_get_service_by_name($servicename) {
 
@@ -2624,7 +2687,7 @@ function lti_get_service_by_name($servicename) {
 /**
  * Finds a service by id
  *
- * @param array  $services    Array of services
+ * @param \mod_lti\local\ltiservice\service_base[] $services Array of services
  * @param string $resourceid  ID of resource
  *
  * @return mod_lti\local\ltiservice\service_base Service
@@ -2742,15 +2805,14 @@ function get_tool_proxy_edit_url(stdClass $proxy) {
  *
  * @param stdClass $type The tool type
  *
- * @return string|void The url to the course of the tool type, void if it is a site wide type
+ * @return string The url to the course of the tool type, void if it is a site wide type
  */
 function get_tool_type_course_url(stdClass $type) {
-    if ($type->course == 1) {
-        return;
-    } else {
+    if ($type->course != 1) {
         $url = new moodle_url('/course/view.php', array('id' => $type->course));
         return $url->out();
     }
+    return null;
 }
 
 /**
@@ -2758,7 +2820,7 @@ function get_tool_type_course_url(stdClass $type) {
  *
  * @param stdClass $type The tool type
  *
- * @return string The urls of the tool type
+ * @return array The urls of the tool type
  */
 function get_tool_type_urls(stdClass $type) {
     $courseurl = get_tool_type_course_url($type);
@@ -2780,7 +2842,7 @@ function get_tool_type_urls(stdClass $type) {
  *
  * @param stdClass $proxy The tool proxy
  *
- * @return string The urls of the tool proxy
+ * @return array The urls of the tool proxy
  */
 function get_tool_proxy_urls(stdClass $proxy) {
     global $OUTPUT;
@@ -2802,7 +2864,6 @@ function get_tool_proxy_urls(stdClass $proxy) {
  * pending, configured, rejected, unknown
  */
 function get_tool_type_state_info(stdClass $type) {
-    $state = '';
     $isconfigured = false;
     $ispending = false;
     $isrejected = false;
diff --git a/mod/lti/service/gradebookservices/backup/moodle2/backup_ltiservice_gradebookservices_subplugin.class.php b/mod/lti/service/gradebookservices/backup/moodle2/backup_ltiservice_gradebookservices_subplugin.class.php
new file mode 100644 (file)
index 0000000..b53168f
--- /dev/null
@@ -0,0 +1,137 @@
+<?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/>.
+
+/**
+ * This file contains the class for restore of this gradebookservices plugin
+ *
+ * @package    ltiservice_gradebookservices
+ * @copyright  2017 Cengage Learning http://www.cengage.com
+ * @author     Dirk Singels, Diego del Blanco
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+require_once($CFG->dirroot.'/mod/lti/locallib.php');
+
+/**
+ * Provides the information to backup gradebookservices lineitems
+ *
+ * @package    ltiservice_gradebookservices
+ * @copyright  2017 Cengage Learning http://www.cengage.com
+ * @author     Dirk Singels, Diego del Blanco, Claude Vervoort
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class backup_ltiservice_gradebookservices_subplugin extends backup_subplugin {
+
+    /** TypeId contained in DB but is invalid */
+    const NONVALIDTYPEID = 0;
+
+    /**
+     * Returns the subplugin information to attach to submission element
+     * @return backup_subplugin_element
+     */
+    protected function define_lti_subplugin_structure() {
+        global $DB;
+
+        // Create XML elements.
+        $subplugin = $this->get_subplugin_element();
+        $subpluginwrapper = new backup_nested_element($this->get_recommended_name());
+        // The gbs entries related with this element.
+        $lineitems = new backup_nested_element('lineitems');
+        $lineitem = new backup_nested_element('lineitem', array('id'), array(
+                'gradeitemid',
+                'courseid',
+                'toolproxyid',
+                'typeid',
+                'baseurl',
+                'ltilinkid',
+                'tag',
+                'vendorcode',
+                'guid'
+                )
+        );
+
+        // Build the tree.
+        $subplugin->add_child($subpluginwrapper);
+        $subpluginwrapper->add_child($lineitems);
+        $lineitems->add_child($lineitem);
+
+        // We need to know the actual activity tool or toolproxy.
+        // If and activity is assigned to a type that doesn't exists we don't want to backup any related lineitems.``
+        // Default to invalid condition.
+        $typeid = 0;
+        $toolproxyid = '0';
+
+        /* cache parent property to account for missing PHPDoc type specification */
+        /** @var backup_activity_task $activitytask */
+        $activitytask = $this->task;
+        $activityid = $activitytask->get_activityid();
+        $activitycourseid = $activitytask->get_courseid();
+        $lti = $DB->get_record('lti', ['id' => $activityid], 'typeid, toolurl, securetoolurl');
+        $ltitype = $DB->get_record('lti_types', ['id' => $lti->typeid], 'toolproxyid, baseurl');
+        if ($ltitype) {
+            $typeid = $lti->typeid;
+            $toolproxyid = $ltitype->toolproxyid;
+        } else if ($lti->typeid == self::NONVALIDTYPEID) { // This activity comes from an old backup.
+            // 1. Let's check if the activity is coupled. If so, find the values in the GBS element.
+            $gbsrecord = $DB->get_record('ltiservice_gradebookservices',
+                    ['ltilinkid' => $activityid], 'typeid,toolproxyid,baseurl');
+            if ($gbsrecord) {
+                $typeid = $gbsrecord->typeid;
+                $toolproxyid = $gbsrecord->toolproxyid;
+            } else { // 2. If it is uncoupled... we will need to guess the right activity typeid
+                // Guess the typeid for the activity.
+                $tool = lti_get_tool_by_url_match($lti->toolurl, $activitycourseid);
+                if (!$tool) {
+                    $tool = lti_get_tool_by_url_match($lti->securetoolurl, $activitycourseid);
+                }
+                if ($tool) {
+                    $alttypeid = $tool->id;
+                    // If we have a valid typeid then get types again.
+                    if ($alttypeid != self::NONVALIDTYPEID) {
+                        $ltitype = $DB->get_record('lti_types', ['id' => $alttypeid], 'toolproxyid, baseurl');
+                        $toolproxyid = $ltitype->toolproxyid;
+                    }
+                }
+            }
+        }
+
+        // Define sources.
+        if ($toolproxyid != null) {
+            $lineitemssql = "SELECT l.*, t.vendorcode as vendorcode, t.guid as guid
+                               FROM {ltiservice_gradebookservices} l
+                         INNER JOIN {lti_tool_proxies} t ON (t.id = l.toolproxyid)
+                              WHERE l.courseid = ?
+                                AND l.toolproxyid = ?
+                                AND l.typeid is null";
+            $lineitemsparams = ['courseid' => backup::VAR_COURSEID, backup_helper::is_sqlparam($toolproxyid)];
+        } else {
+            $lineitemssql = "SELECT l.*, null as vendorcode, null as guid
+                               FROM {ltiservice_gradebookservices} l
+                              WHERE l.courseid = ?
+                                AND l.typeid = ?
+                                AND l.toolproxyid is null";
+            $lineitemsparams = ['courseid' => backup::VAR_COURSEID, backup_helper::is_sqlparam($typeid)];
+        }
+
+        $lineitem->set_source_sql($lineitemssql, $lineitemsparams);
+
+        return $subplugin;
+    }
+}
diff --git a/mod/lti/service/gradebookservices/backup/moodle2/restore_ltiservice_gradebookservices_subplugin.class.php b/mod/lti/service/gradebookservices/backup/moodle2/restore_ltiservice_gradebookservices_subplugin.class.php
new file mode 100644 (file)
index 0000000..35a16f6
--- /dev/null
@@ -0,0 +1,209 @@
+<?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/>.
+
+/**
+ * This file contains the class for restore of this gradebookservices plugin
+ *
+ * @package    ltiservice_gradebookservices
+ * @copyright  2017 Cengage Learning http://www.cengage.com
+ * @author     Dirk Singels, Diego del Blanco, Claude Vervoort
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot.'/mod/lti/locallib.php');
+
+/**
+ * Restore subplugin class.
+ *
+ * Provides the necessary information
+ * needed to restore the lineitems related with the lti activity (coupled),
+ * and all the uncoupled ones from the course.
+ *
+ * @package    ltiservice_gradebookservices
+ * @copyright  2017 Cengage Learning http://www.cengage.com
+ * @author     Dirk Singels, Diego del Blanco, Claude Vervoort
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class restore_ltiservice_gradebookservices_subplugin extends restore_subplugin {
+
+    /**
+     * Returns the subplugin structure to attach to the XML element.
+     *
+     * @return restore_path_element[] array of elements to be processed on restore.
+     */
+    protected function define_lti_subplugin_structure() {
+
+        $paths = array();
+        $elename = $this->get_namefor('lineitem');
+        $elepath = $this->get_pathfor('/lineitems/lineitem');
+        $paths[] = new restore_path_element($elename, $elepath);
+        return $paths;
+    }
+
+    /**
+     * Processes one lineitem
+     *
+     * @param mixed $data
+     * @return void
+     */
+    public function process_ltiservice_gradebookservices_lineitem($data) {
+        global $DB;
+        $data = (object)$data;
+        // The coupled lineitems are restored as any other grade item
+        // so we will only create the entry in the ltiservice_gradebookservices table.
+        // We will try to find a valid toolproxy in the system.
+        // If it has been found before... we use it.
+        /* cache parent property to account for missing PHPDoc type specification */
+        /** @var backup_activity_task $activitytask */
+        $activitytask = $this->task;
+        $courseid = $activitytask->get_courseid();
+        if ($data->typeid != null) {
+            if ($ltitypeid = $this->get_mappingid('ltitype', $data->typeid)) {
+                $newtypeid = $ltitypeid;
+            } else { // If not, then we will call our own function to find it.
+                $newtypeid = $this->find_typeid($data, $courseid);
+            }
+        } else {
+            $newtypeid = null;
+        }
+        if ($data->toolproxyid != null) {
+            $ltitoolproxy = $this->get_mappingid('ltitoolproxy', $data->toolproxyid);
+            if ($ltitoolproxy && $ltitoolproxy != 0) {
+                $newtoolproxyid = $ltitoolproxy;
+            } else { // If not, then we will call our own function to find it.
+                $newtoolproxyid = $this->find_proxy_id($data);
+            }
+        } else {
+            $newtoolproxyid = null;
+        }
+        if ($data->ltilinkid != null) {
+            $ltilinkid = $this->get_new_parentid('lti');
+        } else {
+            $ltilinkid = null;
+        }
+        // If this has not been restored before.
+        if ($this->get_mappingid('gbsgradeitemrestored',  $data->id, 0) == 0) {
+            $newgbsid = $DB->insert_record('ltiservice_gradebookservices', (object) array(
+                    'gradeitemid' => 0,
+                    'courseid' => $courseid,
+                    'toolproxyid' => $newtoolproxyid,
+                    'ltilinkid' => $ltilinkid,
+                    'typeid' => $newtypeid,
+                    'baseurl' => $data->baseurl,
+                    'tag' => $data->tag
+            ));
+            $this->set_mapping('gbsgradeitemoldid', $newgbsid, $data->gradeitemid);
+            $this->set_mapping('gbsgradeitemrestored', $data->id, $data->id);
+        }
+    }
+
+    /**
+     * If the toolproxy is not in the mapping (or it is 0)
+     * we try to find the toolproxyid.
+     * If none is found, then we set it to 0.
+     *
+     * @param mixed $data
+     * @return integer $newtoolproxyid
+     */
+    private function find_proxy_id($data) {
+        global $DB;
+        $newtoolproxyid = 0;
+        $oldtoolproxyguid = $data->guid;
+        $oldtoolproxyvendor = $data->vendorcode;
+
+        $dbtoolproxyjsonparams = array('guid' => $oldtoolproxyguid, 'vendorcode' => $oldtoolproxyvendor);
+        $dbtoolproxy = $DB->get_field('lti_tool_proxies', 'id', $dbtoolproxyjsonparams, IGNORE_MISSING);
+        if ($dbtoolproxy) {
+            $newtoolproxyid = $dbtoolproxy;
+        }
+        return $newtoolproxyid;
+    }
+
+    /**
+     * If the typeid is not in the mapping or it is 0, (it should be most of the times)
+     * we will try to find the better typeid that matches with the lineitem.
+     * If none is found, then we set it to 0.
+     *
+     * @param stdClass $data
+     * @param int $courseid
+     * @return int The item type id
+     */
+    private function find_typeid($data, $courseid) {
+        global $DB;
+        $newtypeid = 0;
+        $oldtypeid = $data->typeid;
+
+        // 1. Find a type with the same id in the same course.
+        $dbtypeidparameter = array('id' => $oldtypeid, 'course' => $courseid, 'baseurl' => $data->baseurl);
+        $dbtype = $DB->get_field_select('lti_types', 'id', "id=:id
+                AND course=:course AND ".$DB->sql_compare_text('baseurl')."=:baseurl",
+                $dbtypeidparameter);
+        if ($dbtype) {
+            $newtypeid = $dbtype;
+        } else {
+            // 2. Find a site type for all the courses (course == 1), but with the same id.
+            $dbtypeidparameter = array('id' => $oldtypeid, 'baseurl' => $data->baseurl);
+            $dbtype = $DB->get_field_select('lti_types', 'id', "id=:id
+                    AND course=1 AND ".$DB->sql_compare_text('baseurl')."=:baseurl",
+                    $dbtypeidparameter);
+            if ($dbtype) {
+                $newtypeid = $dbtype;
+            } else {
+                // 3. Find a type with the same baseurl in the actual site.
+                $dbtypeidparameter = array('course' => $courseid, 'baseurl' => $data->baseurl);
+                $dbtype = $DB->get_field_select('lti_types', 'id', "course=:course
+                        AND ".$DB->sql_compare_text('baseurl')."=:baseurl",
+                        $dbtypeidparameter);
+                if ($dbtype) {
+                    $newtypeid = $dbtype;
+                } else {
+                    // 4. Find a site type for all the courses (course == 1) with the same baseurl.
+                    $dbtypeidparameter = array('course' => 1, 'baseurl' => $data->baseurl);
+                    $dbtype = $DB->get_field_select('lti_types', 'id', "course=1
+                            AND ".$DB->sql_compare_text('baseurl')."=:baseurl",
+                            $dbtypeidparameter);
+                    if ($dbtype) {
+                        $newtypeid = $dbtype;
+                    }
+                }
+            }
+        }
+        return $newtypeid;
+    }
+
+    /**
+     * We call the after_restore_lti to update the grade_items id's that we didn't know in the moment of creating
+     * the gradebookservices rows.
+     */
+    protected function after_restore_lti() {
+        global $DB;
+        $activitytask = $this->task;
+        $courseid = $activitytask->get_courseid();
+        $gbstoupdate = $DB->get_records('ltiservice_gradebookservices', array('gradeitemid' => 0, 'courseid' => $courseid));
+        foreach ($gbstoupdate as $gbs) {
+            $oldgradeitemid = $this->get_mappingid('gbsgradeitemoldid', $gbs->id, 0);
+            $newgradeitemid = $this->get_mappingid('grade_item', $oldgradeitemid, 0);
+            if ($newgradeitemid > 0) {
+                $gbs->gradeitemid = $newgradeitemid;
+                $DB->update_record('ltiservice_gradebookservices', $gbs);
+            }
+        }
+    }
+
+}
diff --git a/mod/lti/service/gradebookservices/classes/local/resources/lineitem.php b/mod/lti/service/gradebookservices/classes/local/resources/lineitem.php
new file mode 100644 (file)
index 0000000..16a3620
--- /dev/null
@@ -0,0 +1,365 @@
+<?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/>.
+
+/**
+ * This file contains a class definition for the LineItem resource
+ *
+ * @package    ltiservice_gradebookservices
+ * @copyright  2017 Cengage Learning http://www.cengage.com
+ * @author     Dirk Singels, Diego del Blanco, Claude Vervoort
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace ltiservice_gradebookservices\local\resources;
+
+use ltiservice_gradebookservices\local\service\gradebookservices;
+use mod_lti\local\ltiservice\resource_base;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * A resource implementing LineItem.
+ *
+ * @package    ltiservice_gradebookservices
+ * @copyright  2017 Cengage Learning http://www.cengage.com
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class lineitem extends resource_base {
+
+    /**
+     * Class constructor.
+     *
+     * @param gradebookservices $service Service instance
+     */
+    public function __construct($service) {
+
+        parent::__construct($service);
+        $this->id = 'LineItem.item';
+        $this->template = '/{context_id}/lineitems/{item_id}/lineitem';
+        $this->variables[] = 'LineItem.url';
+        $this->formats[] = 'application/vnd.ims.lis.v2.lineitem+json';
+        $this->methods[] = self::HTTP_GET;
+        $this->methods[] = self::HTTP_PUT;
+        $this->methods[] = self::HTTP_DELETE;
+
+    }
+
+    /**
+     * Execute the request for this resource.
+     *
+     * @param \mod_lti\local\ltiservice\response $response  Response object for this request.
+     */
+    public function execute($response) {
+        global $CFG, $DB;
+
+        $params = $this->parse_template();
+        $contextid = $params['context_id'];
+        $itemid = $params['item_id'];
+        if ($response->get_request_method() === 'GET') {
+            $contenttype = $response->get_accept();
+        } else {
+            $contenttype = $response->get_content_type();
+        }
+        // We will receive typeid when working with LTI 1.x, if not then we are in LTI 2.
+        $typeid = optional_param('type_id', null, PARAM_ALPHANUM);
+        if (is_null($typeid)) {
+            if (!$this->check_tool_proxy(null, $response->get_request_data())) {
+                $response->set_code(403);
+                $response->set_reason("Invalid tool proxy specified.");
+                return;
+            }
+        } else {
+            switch ($response->get_request_method()) {
+                case self::HTTP_GET:
+                    if (!$this->check_type($typeid, $contextid, 'LineItem.item:get', $response->get_request_data())) {
+                        $response->set_code(403);
+                        $response->set_reason("This resource does not support GET requests.");
+                        return;
+                    }
+                    break;
+                case self::HTTP_PUT:
+                    if (!$this->check_type($typeid, $contextid, 'LineItem.item:put', $response->get_request_data())) {
+                        $response->set_code(403);
+                        $response->set_reason("This resource does not support PUT requests.");
+                        return;
+                    }
+                    break;
+                case self::HTTP_DELETE:
+                    if (!$this->check_type($typeid, $contextid, 'LineItem.item:delete', $response->get_request_data())) {
+                        $response->set_code(403);
+                        $response->set_reason("This resource does not support DELETE requests.");
+                        return;
+                    }
+                    break;
+                default:  // Should not be possible.
+                    $response->set_code(405);
+                    return;
+            }
+        }
+        if (empty($contextid) || (!empty($contenttype) && !in_array($contenttype, $this->formats))) {
+            $response->set_code(400);
+            $response->set_reason("Invalid request made.");
+            return;
+        }
+        if (!$DB->record_exists('course', array('id' => $contextid))) {
+            $response->set_code(404);
+            $response->set_reason("Not Found: Course $contextid doesn't exist.");
+            return;
+        }
+        if (!$DB->record_exists('grade_items', array('id' => $itemid))) {
+            $response->set_code(404);
+            $response->set_reason("Not Found: Grade item $itemid doesn't exist.");
+            return;
+        }
+        $item = $this->get_service()->get_lineitem($contextid, $itemid, $typeid);
+        if ($item === false) {
+            $response->set_code(403);
+            $response->set_reason("Line item does not exist.");
+            return;
+        }
+        require_once($CFG->libdir.'/gradelib.php');
+        switch ($response->get_request_method()) {
+            case 'GET':
+                $this->get_request($response, $item, $typeid);
+                break;
+            case 'PUT':
+                $json = $this->process_put_request($response->get_request_data(), $item, $typeid);
+                $response->set_body($json);
+                $response->set_code(200);
+                break;
+            case 'DELETE':
+                $this->process_delete_request($item);
+                $response->set_code(204);
+                break;
+            default:  // Should not be possible.
+                $response->set_code(405);
+                $response->set_reason("Invalid request method specified.");
+                return;
+        }
+    }
+
+    /**
+     * Process a GET request.
+     *
+     * @param \mod_lti\local\ltiservice\response $response Response object for this request.
+     * @param object $item Grade item instance.
+     * @param string $typeid Tool Type Id
+     */
+    private function get_request($response, $item, $typeid) {
+
+        $response->set_content_type($this->formats[0]);
+        $lineitem = gradebookservices::item_for_json($item, substr(parent::get_endpoint(),
+                0, strrpos(parent::get_endpoint(), "/", -10)), $typeid);
+        $response->set_body(json_encode($lineitem));
+
+    }
+
+    /**
+     * Process a PUT request.
+     *
+     * @param string $body PUT body
+     * @param \ltiservice_gradebookservices\local\resources\lineitem $olditem Grade item instance
+     * @param string $typeid Tool Type Id
+     *
+     * @return string
+     * @throws \Exception
+     */
+    private function process_put_request($body, $olditem, $typeid) {
+        global $DB;
+        $json = json_decode($body);
+        if (empty($json) ||
+                !isset($json->scoreMaximum) ||
+                !isset($json->label)) {
+            throw new \Exception(null, 400);
+        }
+        $item = \grade_item::fetch(array('id' => $olditem->id, 'courseid' => $olditem->courseid));
+        $gbs = gradebookservices::find_ltiservice_gradebookservice_for_lineitem($olditem->id);
+        $updategradeitem = false;
+        $rescalegrades = false;
+        $oldgrademax = grade_floatval($item->grademax);
+        $upgradegradebookservices = false;
+        if ($item->itemname !== $json->label) {
+            $updategradeitem = true;
+        }
+        $item->itemname = $json->label;
+        if (!is_numeric($json->scoreMaximum)) {
+            throw new \Exception(null, 400);
+        } else {
+            if (grade_floats_different($oldgrademax,
+                    grade_floatval($json->scoreMaximum))) {
+                $updategradeitem = true;
+                $rescalegrades = true;
+            }
+            $item->grademax = grade_floatval($json->scoreMaximum);
+        }
+        $resourceid = (isset($json->resourceId)) ? $json->resourceId : '';
+        if ($item->idnumber !== $resourceid) {
+            $updategradeitem = true;
+        }
+        $item->idnumber = $resourceid;
+        if ($gbs) {
+            $tag = (isset($json->tag)) ? $json->tag : null;
+            if ($gbs->tag !== $tag) {
+                $upgradegradebookservices = true;
+            }
+            $gbs->tag = $tag;
+        }
+        $ltilinkid = null;
+        if (isset($json->ltiLinkId)) {
+            if (is_numeric($json->ltiLinkId)) {
+                $ltilinkid = $json->ltiLinkId;
+                if ($gbs) {
+                    if (intval($gbs->ltilinkid) !== intval($json->ltiLinkId)) {
+                        $gbs->ltilinkid = $json->ltiLinkId;
+                        $upgradegradebookservices = true;
+                    }
+                } else {
+                    if (intval($item->iteminstance) !== intval($json->ltiLinkId)) {
+                        $item->iteminstance = intval($json->ltiLinkId);
+                        $updategradeitem = true;
+                    }
+                }
+            } else {
+                throw new \Exception(null, 400);
+            }
+        }
+        if ($ltilinkid != null) {
+            if (is_null($typeid)) {
+                if (!gradebookservices::check_lti_id($ltilinkid, $item->courseid,
+                        $this->get_service()->get_tool_proxy()->id)) {
+                            throw new \Exception(null, 403);
+                }
+            } else {
+                if (!gradebookservices::check_lti_1x_id($ltilinkid, $item->courseid,
+                        $typeid)) {
+                            throw new \Exception(null, 403);
+                }
+            }
+        }
+        if ($updategradeitem) {
+            if (!$item->update('mod/ltiservice_gradebookservices')) {
+                throw new \Exception(null, 500);
+            }
+            if ($rescalegrades) {
+                $item->rescale_grades_keep_percentage(0, $oldgrademax, 0, $item->grademax);
+            }
+        }
+
+        $lineitem = new lineitem($this->get_service());
+        $endpoint = $lineitem->get_endpoint();
+
+        if ($upgradegradebookservices) {
+            if (is_null($typeid)) {
+                $toolproxyid = $this->get_service()->get_tool_proxy()->id;
+                $baseurl = null;
+            } else {
+                $toolproxyid = null;
+                $baseurl = lti_get_type_type_config($typeid)->lti_toolurl;
+            }
+            $DB->update_record('ltiservice_gradebookservices', (object)array(
+                    'id' => $gbs->id,
+                    'gradeitemid' => $gbs->gradeitemid,
+                    'courseid' => $gbs->courseid,
+                    'toolproxyid' => $toolproxyid,
+                    'typeid' => $typeid,
+                    'baseurl' => $baseurl,
+                    'ltilinkid' => $ltilinkid,
+                    'tag' => $gbs->tag
+            ));
+        }
+
+        if (is_null($typeid)) {
+            $id = "{$endpoint}";
+            $json->id = $id;
+        } else {
+            $id = "{$endpoint}?type_id={$typeid}";
+            $json->id = $id;
+        }
+        return json_encode($json, JSON_UNESCAPED_SLASHES);
+
+    }
+
+    /**
+     * Process a DELETE request.
+     *
+     * @param \ltiservice_gradebookservices\local\resources\lineitem $item Grade item instance
+     * @throws \Exception
+     */
+    private function process_delete_request($item) {
+        global $DB;
+
+        $gradeitem = \grade_item::fetch(array('id' => $item->id));
+        if (($gbs = gradebookservices::find_ltiservice_gradebookservice_for_lineitem($item->id)) == false) {
+            throw new \Exception(null, 403);
+        }
+        if (!$gradeitem->delete('mod/ltiservice_gradebookservices')) {
+            throw new \Exception(null, 500);
+        } else {
+            $sqlparams = array();
+            $sqlparams['id'] = $gbs->id;
+            if (!$DB->delete_records('ltiservice_gradebookservices', $sqlparams)) {
+                throw new \Exception(null, 500);
+            }
+        }
+    }
+
+    /**
+     * Get permissions from the config of the tool for that resource
+     *
+     * @param int $typeid
+     *
+     * @return array with the permissions related to this resource by the $lti_type or null if none.
+     */
+    public function get_permissions($typeid) {
+        $tool = lti_get_type_type_config($typeid);
+        if ($tool->ltiservice_gradesynchronization == '1') {
+            return array('LineItem.item:get');
+        } else if ($tool->ltiservice_gradesynchronization == '2') {
+            return array('LineItem.item:get', 'LineItem.item:put', 'LineItem.item:delete');
+        } else {
+            return array();
+        }
+    }
+
+    /**
+     * Parse a value for custom parameter substitution variables.
+     *
+     * @param string $value String to be parsed
+     *
+     * @return string
+     */
+    public function parse_value($value) {
+        global $COURSE, $CFG;
+        if (strpos($value, '$LineItem.url') !== false) {
+            $resolved = '';
+            require_once($CFG->libdir . '/gradelib.php');
+
+            $this->params['context_id'] = $COURSE->id;
+            $id = optional_param('id', 0, PARAM_INT); // Course Module ID.
+            if (!empty($id)) {
+                $cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST);
+                $id = $cm->instance;
+                $item = grade_get_grades($COURSE->id, 'mod', 'lti', $id);
+                if ($item && $item->items) {
+                    $this->params['item_id'] = $item->items[0]->id;
+                    $resolved = parent::get_endpoint();
+                }
+            }
+            $value = str_replace('$LineItem.url', $resolved, $value);
+        }
+        return $value;
+    }
+}
diff --git a/mod/lti/service/gradebookservices/classes/local/resources/lineitems.php b/mod/lti/service/gradebookservices/classes/local/resources/lineitems.php
new file mode 100644 (file)
index 0000000..cd70180
--- /dev/null
@@ -0,0 +1,352 @@
+<?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/>.
+
+/**
+ * This file contains a class definition for the LineItem container resource
+ *
+ * @package    ltiservice_gradebookservices
+ * @copyright  2017 Cengage Learning http://www.cengage.com
+ * @author     Dirk Singels, Diego del Blanco, Claude Vervoort
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace ltiservice_gradebookservices\local\resources;
+
+use ltiservice_gradebookservices\local\service\gradebookservices;
+use mod_lti\local\ltiservice\resource_base;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * A resource implementing LineItem container.
+ *
+ * @package    ltiservice_gradebookservices
+ * @copyright  2017 Cengage Learning http://www.cengage.com
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class lineitems extends resource_base {
+
+    /**
+     * Class constructor.
+     *
+     * @param \ltiservice_gradebookservices\local\service\gradebookservices $service Service instance
+     */
+    public function __construct($service) {
+
+        parent::__construct($service);
+        $this->id = 'LineItem.collection';
+        $this->template = '/{context_id}/lineitems';
+        $this->variables[] = 'LineItems.url';
+        $this->formats[] = 'application/vnd.ims.lis.v2.lineitemcontainer+json';
+        $this->formats[] = 'application/vnd.ims.lis.v2.lineitem+json';
+        $this->methods[] = self::HTTP_GET;
+        $this->methods[] = self::HTTP_POST;
+
+    }
+
+    /**
+     * Execute the request for this resource.
+     *
+     * @param \mod_lti\local\ltiservice\response $response  Response object for this request.
+     */
+    public function execute($response) {
+        global $DB;
+
+        $params = $this->parse_template();
+        $contextid = $params['context_id'];
+        $isget = $response->get_request_method() === self::HTTP_GET;
+        if ($isget) {
+            $contenttype = $response->get_accept();
+        } else {
+            $contenttype = $response->get_content_type();
+        }
+        $container = empty($contenttype) || ($contenttype === $this->formats[0]);
+        // We will receive typeid when working with LTI 1.x, if not the we are in LTI 2.
+        $typeid = optional_param('type_id', null, PARAM_ALPHANUM);
+        if (is_null($typeid)) {
+            if (!$this->check_tool_proxy(null, $response->get_request_data())) {
+                $response->set_code(403);
+                $response->set_reason("Invalid tool proxy specified.");
+                return;
+            }
+        } else {
+            switch ($response->get_request_method()) {
+                case self::HTTP_GET:
+                    if (!$this->check_type($typeid, $contextid, 'LineItem.collection:get', $response->get_request_data())) {
+                        $response->set_code(403);
+                        $response->set_reason("This resource does not support GET requests.");
+                        return;
+                    }
+                    break;
+                case self::HTTP_POST:
+                    if (!$this->check_type($typeid, $contextid, 'LineItem.collection:post', $response->get_request_data())) {
+                        $response->set_code(403);
+                        $response->set_reason("This resource does not support POST requests.");
+                        return;
+                    }
+                    break;
+                default:  // Should not be possible.
+                    $response->set_code(405);
+                    $response->set_reason("Invalid request method specified.");
+                    return;
+            }
+        }
+        if (empty($contextid) || !($container ^ ($response->get_request_method() === self::HTTP_POST)) ||
+                (!empty($contenttype) && !in_array($contenttype, $this->formats))) {
+            $response->set_code(400);
+            $response->set_reason("Invalid request made.");
+            return;
+        }
+        if (!$DB->record_exists('course', array('id' => $contextid))) {
+            $response->set_code(404);
+            $response->set_reason("Not Found: Course $contextid doesn't exist.");
+            return;
+        }
+        switch ($response->get_request_method()) {
+            case self::HTTP_GET:
+                $resourceid = optional_param('resource_id', null, PARAM_TEXT);
+                $ltilinkid = optional_param('lti_link_id', null, PARAM_TEXT);
+                $tag = optional_param('tag', null, PARAM_TEXT);
+                $limitnum = optional_param('limit', 0, PARAM_INT);
+                $limitfrom = optional_param('from', 0, PARAM_INT);
+                $itemsandcount = $this->get_service()->get_lineitems($contextid, $resourceid, $ltilinkid, $tag, $limitfrom,
+                        $limitnum, $typeid);
+                $items = $itemsandcount[1];
+                $totalcount = $itemsandcount[0];
+                $json = $this->get_json_for_get_request($items, $resourceid, $ltilinkid, $tag, $limitfrom,
+                        $limitnum, $totalcount, $typeid, $response);
+                $response->set_content_type($this->formats[0]);
+                break;
+            case self::HTTP_POST:
+                try {
+                    $json = $this->get_json_for_post_request($response->get_request_data(), $contextid, $typeid);
+                    $response->set_code(201);
+                    $response->set_content_type($this->formats[1]);
+                } catch (\Exception $e) {
+                    $response->set_code($e->getCode());
+                    $response->set_reason($e->getMessage());
+                }
+                break;
+            default:  // Should not be possible.
+                $response->set_code(405);
+                $response->set_reason("Invalid request method specified.");
+                return;
+        }
+        $response->set_body($json);
+    }
+
+    /**
+     * Generate the JSON for a GET request.
+     *
+     * @param array $items Array of lineitems
+     * @param string $resourceid Resource identifier used for filtering, may be null
+     * @param string $ltilinkid Resource Link identifier used for filtering, may be null
+     * @param string $tag Tag identifier used for filtering, may be null
+     * @param int $limitfrom Offset of the first line item to return
+     * @param int $limitnum Maximum number of line items to return, ignored if zero or less
+     * @param int $totalcount Number of total lineitems before filtering for paging
+     * @param int $typeid Maximum number of line items to return, ignored if zero or less
+     * @param \mod_lti\local\ltiservice\response $response
+
+     * @return string
+     */
+    private function get_json_for_get_request($items, $resourceid, $ltilinkid,
+            $tag, $limitfrom, $limitnum, $totalcount, $typeid, $response) {
+
+        $firstpage = null;
+        $nextpage = null;
+        $prevpage = null;
+        $lastpage = null;
+        if (isset($limitnum) && $limitnum > 0) {
+            if ($limitfrom >= $totalcount || $limitfrom < 0) {
+                $outofrange = true;
+            } else {
+                $outofrange = false;
+            }
+            $limitprev = $limitfrom - $limitnum >= 0 ? $limitfrom - $limitnum : 0;
+            $limitcurrent = $limitfrom;
+            $limitlast = $totalcount - $limitnum + 1 >= 0 ? $totalcount - $limitnum + 1 : 0;
+            $limitfrom += $limitnum;
+
+            $baseurl = new \moodle_url($this->get_endpoint());
+            if (isset($resourceid)) {
+                $baseurl->param('resource_id', $resourceid);
+            }
+            if (isset($ltilinkid)) {
+                $baseurl->param('lti_link_id', $ltilinkid);
+            }
+            if (isset($tag)) {
+                $baseurl->param('tag', $tag);
+            }
+
+            if (is_null($typeid)) {
+                $baseurl->param('limit', $limitnum);
+                if (($limitfrom <= $totalcount - 1) && (!$outofrange)) {
+                    $nextpage = new \moodle_url($baseurl, ['from' => $limitfrom]);
+                }
+                $firstpage = new \moodle_url($baseurl, ['from' => 0]);
+                $canonicalpage = new \moodle_url($baseurl, ['from' => $limitcurrent]);
+                $lastpage = new \moodle_url($baseurl, ['from' > $limitlast]);
+                if (($limitcurrent > 0) && (!$outofrange)) {
+                    $prevpage = new \moodle_url($baseurl, ['from' => $limitprev]);
+                }
+            } else {
+                $baseurl->params(['type_id' => $typeid, 'limit' => $limitnum]);
+                if (($limitfrom <= $totalcount - 1) && (!$outofrange)) {
+                    $nextpage = new \moodle_url($baseurl, ['from' => $limitfrom]);
+                }
+                $firstpage = new \moodle_url($baseurl, ['from' => 0]);
+                $canonicalpage = new \moodle_url($baseurl, ['from' => $limitcurrent]);
+                $lastpage = new \moodle_url($baseurl, ['from' => $limitlast]);
+                if (($limitcurrent > 0) && (!$outofrange)) {
+                    $prevpage = new \moodle_url($baseurl, ['from' => $limitprev]);
+                }
+            }
+        }
+
+        $jsonitems=[];
+        $endpoint = parent::get_endpoint();
+        foreach ($items as $item) {
+            array_push($jsonitems, gradebookservices::item_for_json($item, $endpoint, $typeid));
+        }
+
+        if (isset($canonicalpage) && ($canonicalpage)) {
+            $links = 'Link: <' . $firstpage->out() . '>; rel=“first”';
+            if (!is_null($prevpage)) {
+                $links .= ', <' . $prevpage->out() . '>; rel=“prev”';
+            }
+            $links .= ', <' . $canonicalpage->out(). '>; rel=“canonical”';
+            if (!is_null($nextpage)) {
+                $links .= ', <' . $nextpage->out() . '>; rel=“next”';
+            }
+            $links .= ', <' . $lastpage->out() . '>; rel=“last”';
+            $response->add_additional_header($links);
+        }
+        return json_encode($jsonitems);
+    }
+
+    /**
+     * Generate the JSON for a POST request.
+     *
+     * @param string $body POST body
+     * @param string $contextid Course ID
+     * @param string $typeid
+     *
+     * @return string
+     * @throws \Exception
+     */
+    private function get_json_for_post_request($body, $contextid, $typeid) {
+        global $CFG, $DB;
+
+        $json = json_decode($body);
+        if (empty($json) ||
+                !isset($json->scoreMaximum) ||
+                !isset($json->label)) {
+            throw new \Exception(null, 400);
+        }
+        if (is_numeric($json->scoreMaximum)) {
+            $max = $json->scoreMaximum;
+        } else {
+            throw new \Exception(null, 400);
+        }
+        require_once($CFG->libdir.'/gradelib.php');
+        $resourceid = (isset($json->resourceId)) ? $json->resourceId : '';
+        $ltilinkid = (isset($json->ltiLinkId)) ? $json->ltiLinkId : null;
+        if ($ltilinkid != null) {
+            if (is_null($typeid)) {
+                if (!gradebookservices::check_lti_id($ltilinkid, $contextid, $this->get_service()->get_tool_proxy()->id)) {
+                    throw new \Exception(null, 403);
+                }
+            } else {
+                if (!gradebookservices::check_lti_1x_id($ltilinkid, $contextid, $typeid)) {
+                    throw new \Exception(null, 403);
+                }
+            }
+        }
+        $tag = (isset($json->tag)) ? $json->tag : '';
+        if (is_null($typeid)) {
+            $toolproxyid = $this->get_service()->get_tool_proxy()->id;
+            $baseurl = null;
+        } else {
+            $toolproxyid = null;
+            $baseurl = lti_get_type_type_config($typeid)->lti_toolurl;
+        }
+        $params = array();
+        $params['itemname'] = $json->label;
+        $params['gradetype'] = GRADE_TYPE_VALUE;
+        $params['grademax']  = $max;
+        $params['grademin']  = 0;
+        $item = new \grade_item(array('id' => 0, 'courseid' => $contextid));
+        \grade_item::set_properties($item, $params);
+        $item->itemtype = 'manual';
+        $item->idnumber = $resourceid;
+        $item->grademax = $max;
+        $id = $item->insert('mod/ltiservice_gradebookservices');
+        $DB->insert_record('ltiservice_gradebookservices', (object)array(
+                'gradeitemid' => $id,
+                'courseid' => $contextid,
+                'toolproxyid' => $toolproxyid,
+                'typeid' => $typeid,
+                'baseurl' => $baseurl,
+                'ltilinkid' => $ltilinkid,
+                'tag' => $tag
+        ));
+        if (is_null($typeid)) {
+            $json->id = parent::get_endpoint() . "/{$id}/lineitem";
+        } else {
+            $json->id = parent::get_endpoint() . "/{$id}/lineitem?type_id={$typeid}";
+        }
+        return json_encode($json, JSON_UNESCAPED_SLASHES);
+
+    }
+
+    /**
+     * get permissions from the config of the tool for that resource
+     *
+     * @param string $typeid
+     *
+     * @return array with the permissions related to this resource by the lti type or null if none.
+     */
+    public function get_permissions($typeid) {
+        $tool = lti_get_type_type_config($typeid);
+        if ($tool->ltiservice_gradesynchronization == '1') {
+            return array('LineItem.collection:get');
+        } else if ($tool->ltiservice_gradesynchronization == '2') {
+            return array('LineItem.collection:get', 'LineItem.collection:post');
+        } else {
+            return array();
+        }
+    }
+
+    /**
+     * Parse a value for custom parameter substitution variables.
+     *
+     * @param string $value String to be parsed
+     *
+     * @return string
+     */
+    public function parse_value($value) {
+        global $COURSE;
+
+        if (strpos($value, '$LineItems.url') !== false) {
+            $this->params['context_id'] = $COURSE->id;
+            $value = str_replace('$LineItems.url', parent::get_endpoint(), $value);
+        }
+
+        return $value;
+
+    }
+}
diff --git a/mod/lti/service/gradebookservices/classes/local/resources/results.php b/mod/lti/service/gradebookservices/classes/local/resources/results.php
new file mode 100644 (file)
index 0000000..445c1dc
--- /dev/null
@@ -0,0 +1,304 @@
+<?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/>.
+
+/**
+ * This file contains a class definition for the LISResults container resource
+ *
+ * @package    ltiservice_gradebookservices
+ * @copyright  2017 Cengage Learning http://www.cengage.com
+ * @author     Dirk Singels, Diego del Blanco, Claude Vervoort
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace ltiservice_gradebookservices\local\resources;
+
+use ltiservice_gradebookservices\local\service\gradebookservices;
+use mod_lti\local\ltiservice\resource_base;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * A resource implementing LISResults container.
+ *
+ * @package    ltiservice_gradebookservices
+ * @copyright  2017 Cengage Learning http://www.cengage.com
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class results extends resource_base {
+
+    /**
+     * Class constructor.
+     *
+     * @param \ltiservice_gradebookservices\local\service\gradebookservices $service Service instance
+     */
+    public function __construct($service) {
+
+        parent::__construct($service);
+        $this->id = 'Result.collection';
+        $this->template = '/{context_id}/lineitems/{item_id}/lineitem/results';
+        $this->variables[] = 'Results.url';
+        $this->formats[] = 'application/vnd.ims.lis.v2.resultcontainer+json';
+        $this->methods[] = 'GET';
+    }
+
+    /**
+     * Execute the request for this resource.
+     *
+     * @param \mod_lti\local\ltiservice\response $response  Response object for this request.
+     */
+    public function execute($response) {
+        global $CFG, $DB;
+
+        $params = $this->parse_template();
+        $contextid = $params['context_id'];
+        $itemid = $params['item_id'];
+
+        $isget = $response->get_request_method() === 'GET';
+        if ($isget) {
+            $contenttype = $response->get_accept();
+        } else {
+            $contenttype = $response->get_content_type();
+        }
+        // We will receive typeid when working with LTI 1.x, if not the we are in LTI 2.
+        $typeid = optional_param('type_id', null, PARAM_ALPHANUM);
+        if (is_null($typeid)) {
+            if (!$this->check_tool_proxy(null, $response->get_request_data())) {
+                $response->set_code(403);
+                $response->set_reason("Invalid tool proxy specified.");
+                return;
+            }
+        } else {
+            if (!$this->check_type($typeid, $contextid, 'Result.collection:get', $response->get_request_data())) {
+                $response->set_code(403);
+                $response->set_reason("This resource does not support GET requests.");
+                return;
+            }
+        }
+        if (empty($contextid) || (!empty($contenttype) && !in_array($contenttype, $this->formats))) {
+            $response->set_code(400);
+            $response->set_reason("Invalid request made.");
+            return;
+        }
+        if (!$DB->record_exists('course', array('id' => $contextid))) {
+            $response->set_code(404);
+            $response->set_reason("Not Found: Course $contextid doesn't exist.");
+            return;
+        }
+        if (!$DB->record_exists('grade_items', array('id' => $itemid))) {
+            $response->set_code(404);
+            $response->set_reason("Not Found: Grade item $itemid doesn't exist.");
+            return;
+        }
+        $item = $this->get_service()->get_lineitem($contextid, $itemid, $typeid);
+        if ($item === false) {
+            $response->set_code(403);
+            $response->set_reason("Line item does not exist.");
+            return;
+        }
+        $gbs = gradebookservices::find_ltiservice_gradebookservice_for_lineitem($itemid);
+        $ltilinkid = null;
+        if (isset($item->iteminstance)) {
+            $ltilinkid = $item->iteminstance;
+        } else if ($gbs && isset($gbs->ltilinkid)) {
+            $ltilinkid = $gbs->ltilinkid;
+        }
+        if ($ltilinkid != null) {
+            if (is_null($typeid)) {
+                if (isset($item->iteminstance) && (!gradebookservices::check_lti_id($ltilinkid, $item->courseid,
+                        $this->get_service()->get_tool_proxy()->id))) {
+                    $response->set_code(403);
+                    $response->set_reason("Invalid LTI id supplied.");
+                    return;
+                }
+            } else {
+                if (isset($item->iteminstance) && (!gradebookservices::check_lti_1x_id($ltilinkid, $item->courseid,
+                        $typeid))) {
+                    $response->set_code(403);
+                    $response->set_reason("Invalid LTI id supplied.");
+                    return;
+                }
+            }
+        }
+        require_once($CFG->libdir.'/gradelib.php');
+        switch ($response->get_request_method()) {
+            case 'GET':
+                $useridfilter = optional_param('user_id', 0, PARAM_INT);
+                $limitnum = optional_param('limit', 0, PARAM_INT);
+                $limitfrom = optional_param('from', 0, PARAM_INT);
+                $typeid = optional_param('type_id', null, PARAM_TEXT);
+                $json = $this->get_json_for_get_request($item->id, $limitfrom, $limitnum,
+                        $useridfilter, $typeid, $response);
+                $response->set_content_type($this->formats[0]);
+                $response->set_body($json);
+                break;
+            default:  // Should not be possible.
+                $response->set_code(405);
+                $response->set_reason("Invalid request method specified.");
+                return;
+        }
+        $response->set_body($json);
+    }
+
+    /**
+     * Generate the JSON for a GET request.
+     *
+     * @param int    $itemid     Grade item instance