Merge branch 'install_master' of https://git.in.moodle.com/amosbot/moodle-install
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Thu, 10 Mar 2016 12:30:51 +0000 (13:30 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Thu, 10 Mar 2016 12:30:51 +0000 (13:30 +0100)
143 files changed:
admin/roles/classes/define_role_table_advanced.php
admin/roles/define.php
admin/tool/monitor/lib.php
auth/db/auth.php
auth/db/tests/db_test.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_stepslib.php
blocks/community/communitycourse.php
calendar/externallib.php
calendar/tests/externallib_test.php
calendar/view.php
course/externallib.php
course/lib.php
course/modedit.php
course/modlib.php
course/moodleform_mod.php
course/tests/courselib_test.php
enrol/meta/lib.php
enrol/meta/tests/behat/enrol_meta.feature
grade/edit/tree/category.php
grade/edit/tree/category_form.php
grade/grading/form/guide/guideeditor.php
grade/grading/form/rubric/rubriceditor.php
grade/report/singleview/classes/local/screen/screen.php
grade/report/singleview/classes/local/screen/tablelike.php
grade/report/singleview/classes/local/ui/exclude.php
grade/report/singleview/tests/behat/singleview.feature
grade/tests/behat/grade_category_validation.feature [new file with mode: 0644]
lang/en/admin.php
lang/en/grades.php
lang/en/plugin.php
lang/en/tag.php
lib/adminlib.php
lib/ajax/getnavbranch.php
lib/amd/build/ajax.min.js
lib/amd/build/form-autocomplete.min.js
lib/amd/build/form-course-selector.min.js [new file with mode: 0644]
lib/amd/src/ajax.js
lib/amd/src/form-autocomplete.js
lib/amd/src/form-course-selector.js [new file with mode: 0644]
lib/behat/form_field/behat_form_autocomplete.php
lib/classes/task/search_index_task.php [moved from lib/classes/task/search_task.php with 90% similarity]
lib/classes/task/search_optimize_task.php [new file with mode: 0644]
lib/classes/text.php
lib/classes/user.php
lib/coursecatlib.php
lib/datalib.php
lib/db/install.xml
lib/db/services.php
lib/db/tag.php
lib/db/tasks.php
lib/db/upgrade.php
lib/form/course.php [new file with mode: 0644]
lib/form/modgrade.php
lib/formslib.php
lib/grade/grade_item.php
lib/htmlpurifier/locallib.php
lib/phpunit/classes/util.php
lib/testing/generator/module_generator.php
lib/testing/tests/generator_test.php
lib/tests/coursecatlib_test.php
lib/tests/htmlpurifier_test.php
lib/tests/text_test.php
lib/tests/user_test.php
lib/upgrade.txt
lib/upgradelib.php
lib/weblib.php
login/index.php
login/index_form.html
mod/assign/adminmanageplugins.php
mod/assign/db/services.php
mod/assign/externallib.php
mod/assign/tests/externallib_test.php
mod/assign/version.php
mod/book/db/services.php
mod/book/version.php
mod/chat/db/services.php
mod/chat/version.php
mod/choice/db/services.php
mod/choice/version.php
mod/data/db/services.php
mod/data/field/file/field.class.php
mod/data/field/number/field.class.php
mod/data/field/picture/field.class.php
mod/data/field/text/field.class.php
mod/data/field/textarea/field.class.php
mod/data/field/url/field.class.php
mod/data/lib.php
mod/data/version.php
mod/feedback/lang/en/feedback.php
mod/feedback/lib.php
mod/feedback/view.php
mod/folder/db/services.php
mod/folder/version.php
mod/forum/db/services.php
mod/forum/version.php
mod/glossary/db/services.php
mod/glossary/version.php
mod/imscp/db/services.php
mod/imscp/version.php
mod/lti/classes/service_exception_handler.php [new file with mode: 0644]
mod/lti/db/services.php
mod/lti/locallib.php
mod/lti/service.php
mod/lti/servicelib.php
mod/lti/tests/service_exception_handler_test.php [new file with mode: 0644]
mod/lti/tests/servicelib_test.php [new file with mode: 0644]
mod/lti/upgrade.txt
mod/lti/version.php
mod/page/db/services.php
mod/page/version.php
mod/quiz/classes/external.php
mod/quiz/db/services.php
mod/quiz/locallib.php
mod/quiz/tests/external_test.php
mod/quiz/version.php
mod/resource/db/services.php
mod/resource/version.php
mod/scorm/db/services.php
mod/scorm/version.php
mod/survey/db/services.php
mod/survey/version.php
mod/url/db/services.php
mod/url/version.php
mod/wiki/db/services.php
mod/wiki/version.php
question/type/ddmarker/styles.css
question/type/ddwtos/styles.css
question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd-debug.js
question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd-min.js
question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd.js
question/type/ddwtos/yui/src/ddwtos/js/ddwtos.js
report/search/index.php
repository/repository_callback.php
search/classes/engine.php
search/classes/manager.php
search/cli/indexer.php [new file with mode: 0644]
search/engine/solr/classes/document.php
search/engine/solr/classes/engine.php
search/engine/solr/setup_schema.php
search/tests/fixtures/mock_search_engine.php
user/index.php
version.php

index d18c144..eb46c76 100644 (file)
@@ -413,7 +413,7 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
     }
 
     public function save_changes() {
-        global $DB;
+        global $DB, $CFG;
 
         if (!$this->roleid) {
             // Creating role.
@@ -427,6 +427,7 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
             // the UI. It would be better to do this only when we know that fields affected are
             // updated. But thats getting into the weeds of the coursecat cache and role edits
             // should not be that frequent, so here is the ugly brutal approach.
+            require_once($CFG->libdir . '/coursecatlib.php');
             coursecat::role_assignment_changed($this->role->id, context_system::instance());
         }
 
index d30ad3e..8e98399 100644 (file)
@@ -201,7 +201,7 @@ if (optional_param('savechanges', false, PARAM_BOOL) && confirm_sesskey() && $de
     $event = \core\event\role_capabilities_updated::create(
         array(
             'context' => $systemcontext,
-            'objectid' => $roleid
+            'objectid' => $tableroleid
         )
     );
     $event->set_legacy_logdata(array(SITEID, 'role', $action, 'admin/roles/define.php?action=view&roleid=' . $tableroleid,
index 7e91f0b..52543d4 100644 (file)
@@ -103,10 +103,12 @@ function tool_monitor_get_user_courses() {
     if (has_capability('tool/monitor:subscribe', context_system::instance())) {
         $options[0] = get_string('site');
     }
-    if ($courses = get_user_capability_course('tool/monitor:subscribe', null, true, 'fullname', $orderby)) {
+    if ($courses = get_user_capability_course('tool/monitor:subscribe', null, true, 'fullname, visible', $orderby)) {
         foreach ($courses as $course) {
-            $options[$course->id] = format_string($course->fullname, true,
-                array('context' => context_course::instance($course->id)));
+            $coursectx = context_course::instance($course->id);
+            if ($course->visible || has_capability('moodle/course:viewhiddencourses', $coursectx)) {
+                $options[$course->id] = format_string($course->fullname, true, array('context' => $coursectx));
+            }
         }
     }
     // If there are no courses and there is no site permission then return false.
index 147ef60..8c2428d 100644 (file)
@@ -328,6 +328,7 @@ class auth_plugin_db extends auth_plugin_base {
                         $updateuser = new stdClass();
                         $updateuser->id   = $user->id;
                         $updateuser->suspended = 1;
+                        $updateuser = $this->clean_data($updateuser);
                         user_update_user($updateuser, false);
                         $trace->output(get_string('auth_dbsuspenduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)), 1);
                     }
@@ -414,6 +415,7 @@ class auth_plugin_db extends auth_plugin_base {
                         $updateuser = new stdClass();
                         $updateuser->id = $olduser->id;
                         $updateuser->suspended = 0;
+                        $updateuser = $this->clean_data($updateuser);
                         user_update_user($updateuser);
                         $trace->output(get_string('auth_dbreviveduser', 'auth_db', array('name' => $username,
                             'id' => $olduser->id)), 1);
@@ -436,6 +438,7 @@ class auth_plugin_db extends auth_plugin_base {
                     $trace->output(get_string('auth_dbinsertuserduplicate', 'auth_db', array('username'=>$user->username, 'auth'=>$collision->auth)), 1);
                     continue;
                 }
+                $user = $this->clean_data($user);
                 try {
                     $id = user_create_user($user, false); // It is truly a new user.
                     $trace->output(get_string('auth_dbinsertuser', 'auth_db', array('name'=>$user->username, 'id'=>$id)), 1);
@@ -577,6 +580,7 @@ class auth_plugin_db extends auth_plugin_base {
         }
         if ($needsupdate) {
             require_once($CFG->dirroot . '/user/lib.php');
+            $updateuser = $this->clean_data($updateuser);
             user_update_user($updateuser);
         }
         return $DB->get_record('user', array('id'=>$userid, 'deleted'=>0));
@@ -906,6 +910,30 @@ class auth_plugin_db extends auth_plugin_base {
         error_reporting($CFG->debug);
         ob_end_flush();
     }
+
+    /**
+     * Clean the user data that comes from an external database.
+     *
+     * @param array $user the user data to be validated against properties definition.
+     * @return stdClass $user the cleaned user data.
+     */
+    public function clean_data($user) {
+        if (empty($user)) {
+            return $user;
+        }
+
+        foreach ($user as $field => $value) {
+            // Get the property parameter type and do the cleaning.
+            try {
+                $property = core_user::get_property_definition($field);
+                $user->$field = clean_param($value, $property['type']);
+            } catch (coding_exception $e) {
+                debugging("The property '$field' could not be cleaned.", DEBUG_DEVELOPER);
+            }
+        }
+
+        return $user;
+    }
 }
 
 
index 03c186d..e0d68d3 100644 (file)
@@ -399,4 +399,77 @@ class auth_db_testcase extends advanced_testcase {
         $this->assertEquals("select * from table WHERE column=? AND anothercolumn > ?", $sqlout);
         $this->assertEquals(array(1, 'b'), $arrout);
     }
+
+    /**
+     * Testing the clean_data() method.
+     */
+    public function test_clean_data() {
+        global $DB;
+
+        $this->resetAfterTest(false);
+        $this->preventResetByRollback();
+        $this->init_auth_database();
+        $auth = get_auth_plugin('db');
+        $auth->db_init();
+
+        // Create users on external table.
+        $extdbuser1 = (object)array('name'=>'u1', 'pass'=>'heslo', 'email'=>'u1@example.com');
+        $extdbuser1->id = $DB->insert_record('auth_db_users', $extdbuser1);
+
+        // User with malicious data on the name.
+        $extdbuser2 = (object)array('name'=>'user<script>alert(1);</script>xss', 'pass'=>'heslo', 'email'=>'xssuser@example.com');
+        $extdbuser2->id = $DB->insert_record('auth_db_users', $extdbuser2);
+
+        $trace = new null_progress_trace();
+
+        // Let's test user sync make sure still works as expected..
+        $auth->sync_users($trace, true);
+
+        // Get the user on moodle user table.
+        $user2 = $DB->get_record('user', array('email'=> $extdbuser2->email, 'auth'=>'db'));
+
+        // The malicious code should be sanitized.
+        $this->assertEquals($user2->username, 'userscriptalert1scriptxss');
+        $this->assertNotEquals($user2->username, $extdbuser2->name);
+
+        // User with correct data, should be equal to external db.
+        $user1 = $DB->get_record('user', array('email'=> $extdbuser1->email, 'auth'=>'db'));
+        $this->assertEquals($extdbuser1->name, $user1->username);
+        $this->assertEquals($extdbuser1->email, $user1->email);
+
+        // Now, let's update the name.
+        $extdbuser2->name = 'user no xss anymore';
+        $DB->update_record('auth_db_users', $extdbuser2);
+
+        // Run sync again to update the user data.
+        $auth->sync_users($trace, true);
+
+        // The user information should be updated.
+        $user2 = $DB->get_record('user', array('username' => 'usernoxssanymore', 'auth' => 'db'));
+        // The spaces should be removed, as it's the username.
+        $this->assertEquals($user2->username, 'usernoxssanymore');
+
+        // Now let's test just the clean_data() method isolated.
+        // Testing PARAM_USERNAME, PARAM_NOTAGS, PARAM_RAW_TRIMMED and others.
+        $user3 = new stdClass();
+        $user3->firstname = 'John <script>alert(1)</script> Doe';
+        $user3->username = 'john%#&~%*_doe';
+        $user3->email = ' john@testing.com ';
+        $user3->deleted = 'no';
+        $user3->description = '<b>A description <script>alert(123)</script>about myself.</b>';
+        $user3cleaned = $auth->clean_data($user3);
+
+        // Expected results.
+        $this->assertEquals($user3cleaned->firstname, 'John alert(1) Doe');
+        $this->assertEquals($user3cleaned->email, 'john@testing.com');
+        $this->assertEquals($user3cleaned->deleted, 0);
+        $this->assertEquals($user3->description, '<b>A description about myself.</b>');
+        $this->assertEquals($user3->username, 'john_doe');
+
+        // Try to clean an invalid property (fullname).
+        $user3->fullname = 'John Doe';
+        $auth->clean_data($user3);
+        $this->assertDebuggingCalled("The property 'fullname' could not be cleaned.");
+        $this->cleanup_auth_database();
+    }
 }
index 436f19f..b088b1a 100644 (file)
@@ -269,6 +269,9 @@ class backup_module_structure_step extends backup_structure_step {
             'completion', 'completiongradeitemnumber', 'completionview', 'completionexpected',
             'availability', 'showdescription'));
 
+        $tags = new backup_nested_element('tags');
+        $tag = new backup_nested_element('tag', array('id'), array('name', 'rawname'));
+
         // attach format plugin structure to $module element, only one allowed
         $this->add_plugin_structure('format', $module, false);
 
@@ -279,6 +282,9 @@ class backup_module_structure_step extends backup_structure_step {
         // attach local plugin structure to $module, multiple allowed
         $this->add_plugin_structure('local', $module, true);
 
+        $module->add_child($tags);
+        $tags->add_child($tag);
+
         // Set the sources
         $concat = $DB->sql_concat("'mod_'", 'm.name');
         $module->set_source_sql("
@@ -289,6 +295,13 @@ class backup_module_structure_step extends backup_structure_step {
               JOIN {course_sections} s ON s.id = cm.section
              WHERE cm.id = ?", array(backup::VAR_MODID));
 
+        $tag->set_source_sql("SELECT t.id, t.name, t.rawname
+                                FROM {tag} t
+                                JOIN {tag_instance} ti ON ti.tagid = t.id
+                               WHERE ti.itemtype = 'course_modules'
+                                 AND ti.component = 'core'
+                                 AND ti.itemid = ?", array(backup::VAR_MODID));
+
         // Define annotations
         $module->annotate_ids('grouping', 'groupingid');
 
index caf5f07..ec316b6 100644 (file)
@@ -3584,6 +3584,8 @@ class restore_module_structure_step extends restore_structure_step {
             $paths[] = new restore_path_element('availability_field', '/module/availability_info/availability_field');
         }
 
+        $paths[] = new restore_path_element('tag', '/module/tags/tag');
+
         // Apply for 'format' plugins optional paths at module level
         $this->add_plugin_structure('format', $module);
 
@@ -3691,6 +3693,25 @@ class restore_module_structure_step extends restore_structure_step {
         }
     }
 
+    /**
+     * Fetch all the existing because tag_set() deletes them
+     * so everything must be reinserted on each call.
+     *
+     * @param stdClass $data Record data
+     */
+    protected function process_tag($data) {
+        global $CFG;
+
+        $data = (object)$data;
+
+        if (core_tag_tag::is_enabled('core', 'course_modules')) {
+            $modcontext = context::instance_by_id($this->task->get_contextid());
+            $instanceid = $this->task->get_moduleid();
+
+            core_tag_tag::add_item_tag('core', 'course_modules', $instanceid, $modcontext, $data->rawname);
+        }
+    }
+
     /**
      * Process the legacy availability table record. This table does not exist
      * in Moodle 2.7+ but we still support restore.
@@ -3759,6 +3780,20 @@ class restore_module_structure_step extends restore_structure_step {
                     array('id' => $availfield->coursemoduleid));
         }
     }
+    /**
+     * This method will be executed after the rest of the restore has been processed.
+     *
+     * Update old tag instance itemid(s).
+     */
+    protected function after_restore() {
+        global $DB;
+
+        $contextid = $this->task->get_contextid();
+        $instanceid = $this->task->get_activityid();
+        $olditemid = $this->task->get_old_activityid();
+
+        $DB->set_field('tag_instance', 'itemid', $instanceid, array('contextid' => $contextid, 'itemid' => $olditemid));
+    }
 }
 
 /**
index 9eb544b..d27cb07 100644 (file)
@@ -198,7 +198,7 @@ if (optional_param('executesearch', 0, PARAM_INT) and confirm_sesskey()) {
     require_once($CFG->dirroot . "/webservice/xmlrpc/lib.php");
     $xmlrpcclient = new webservice_xmlrpc_client($serverurl, $token);
     try {
-        $result = $xmlrpcclient->call($function, $params);
+        $result = $xmlrpcclient->call($function, array_values($params));
         $courses = $result['courses'];
         $coursetotal = $result['coursetotal'];
     } catch (Exception $e) {
index c2053fa..da95699 100644 (file)
@@ -227,21 +227,21 @@ class core_calendar_external extends external_api {
             $params['options']['timeend'] = PHP_INT_MAX;
         }
 
+        // Event list does not check visibility and permissions, we'll check that later.
         $eventlist = calendar_get_events($params['options']['timestart'], $params['options']['timeend'], $funcparam['users'], $funcparam['groups'],
                 $funcparam['courses'], true, $params['options']['ignorehidden']);
+
         // WS expects arrays.
         $events = array();
-        foreach ($eventlist as $id => $event) {
-            $events[$id] = (array) $event;
-        }
 
         // We need to get events asked for eventids.
-        $eventsbyid = calendar_get_events_by_id($params['events']['eventids']);
-        foreach ($eventsbyid as $eventid => $eventobj) {
+        if ($eventsbyid = calendar_get_events_by_id($params['events']['eventids'])) {
+            $eventlist += $eventsbyid;
+        }
+
+        foreach ($eventlist as $eventid => $eventobj) {
             $event = (array) $eventobj;
-            if (isset($events[$eventid])) {
-                   continue;
-            }
+
             if ($hassystemcap) {
                 // User can see everything, no further check is needed.
                 $events[$eventid] = $event;
index 8a5b444..7619b2a 100644 (file)
@@ -370,6 +370,30 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
         $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
         $this->assertEquals(1, count($events['events']));
         $this->assertEquals(0, count($events['warnings']));
+
+        // Now, create an activity event.
+        $this->setAdminUser();
+        $nexttime = time() + DAYSECS;
+        $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id, 'duedate' => $nexttime));
+
+        $this->setUser($user);
+        $paramevents = array ('courseids' => array($course->id));
+        $options = array ('siteevents' => true, 'userevents' => true, 'timeend' => time() + WEEKSECS);
+        $events = core_calendar_external::get_calendar_events($paramevents, $options);
+        $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
+
+        $this->assertCount(5, $events['events']);
+
+        // Hide the assignment.
+        set_coursemodule_visible($assign->cmid, 0);
+        // Empty all the caches that may be affected  by this change.
+        accesslib_clear_all_caches_for_unit_testing();
+        course_modinfo::clear_instance_cache();
+
+        $events = core_calendar_external::get_calendar_events($paramevents, $options);
+        $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
+        // Expect one less.
+        $this->assertCount(4, $events['events']);
     }
 
     /**
index 5121a98..7902359 100644 (file)
@@ -68,10 +68,10 @@ if (!empty($day) && !empty($mon) && !empty($year)) {
     if (checkdate($mon, $day, $year)) {
         $time = make_timestamp($year, $mon, $day);
     } else {
-        $time = time();
+        $time = usergetmidnight(time());
     }
 } else if (empty($time)) {
-    $time = time();
+    $time = usergetmidnight(time());
 }
 
 $url->param('time', $time);
index da480bb..6214828 100644 (file)
@@ -2135,7 +2135,11 @@ class core_course_external extends external_api {
                                                         (search, modulelist (only admins), blocklist (only admins), tagid)'),
                 'criteriavalue' => new external_value(PARAM_RAW, 'criteria value'),
                 'page'          => new external_value(PARAM_INT, 'page number (0 based)', VALUE_DEFAULT, 0),
-                'perpage'       => new external_value(PARAM_INT, 'items per page', VALUE_DEFAULT, 0)
+                'perpage'       => new external_value(PARAM_INT, 'items per page', VALUE_DEFAULT, 0),
+                'requiredcapabilities' => new external_multiple_structure(
+                    new external_value(PARAM_CAPABILITY, 'Capability string used to filter courses by permission'),
+                    VALUE_OPTIONAL
+                )
             )
         );
     }
@@ -2147,11 +2151,16 @@ class core_course_external extends external_api {
      * @param string $criteriavalue Criteria value
      * @param int $page             Page number (for pagination)
      * @param int $perpage          Items per page
+     * @param array $requiredcapabilities Optional list of required capabilities (used to filter the list).
      * @return array of course objects and warnings
      * @since Moodle 3.0
      * @throws moodle_exception
      */
-    public static function search_courses($criterianame, $criteriavalue, $page=0, $perpage=0) {
+    public static function search_courses($criterianame,
+                                          $criteriavalue,
+                                          $page=0,
+                                          $perpage=0,
+                                          $requiredcapabilities=array()) {
         global $CFG;
         require_once($CFG->libdir . '/coursecatlib.php');
 
@@ -2161,7 +2170,8 @@ class core_course_external extends external_api {
             'criterianame'  => $criterianame,
             'criteriavalue' => $criteriavalue,
             'page'          => $page,
-            'perpage'       => $perpage
+            'perpage'       => $perpage,
+            'requiredcapabilities' => $requiredcapabilities
         );
         $params = self::validate_parameters(self::search_courses_parameters(), $parameters);
 
@@ -2194,8 +2204,8 @@ class core_course_external extends external_api {
         }
 
         // Search the courses.
-        $courses = coursecat::search_courses($searchcriteria, $options);
-        $totalcount = coursecat::search_courses_count($searchcriteria);
+        $courses = coursecat::search_courses($searchcriteria, $options, $params['requiredcapabilities']);
+        $totalcount = coursecat::search_courses_count($searchcriteria, $options, $params['requiredcapabilities']);
 
         $finalcourses = array();
         $categoriescache = array();
@@ -2244,10 +2254,12 @@ class core_course_external extends external_api {
             list($summary, $summaryformat) =
                 external_format_text($course->summary, $course->summaryformat, $coursecontext->id, 'course', 'summary', null);
 
+            $displayname = get_course_display_name_for_list($course);
             $coursereturns = array();
             $coursereturns['id']                = $course->id;
-            $coursereturns['fullname']          = $course->get_formatted_fullname();
-            $coursereturns['shortname']         = $course->get_formatted_shortname();
+            $coursereturns['fullname']          = external_format_string($course->fullname, $coursecontext->id);
+            $coursereturns['displayname']       = external_format_string($displayname, $coursecontext->id);
+            $coursereturns['shortname']         = external_format_string($course->shortname, $coursecontext->id);
             $coursereturns['categoryid']        = $course->category;
             $coursereturns['categoryname']      = $category->name;
             $coursereturns['summary']           = $summary;
@@ -2281,6 +2293,7 @@ class core_course_external extends external_api {
                         array(
                             'id' => new external_value(PARAM_INT, 'course id'),
                             'fullname' => new external_value(PARAM_TEXT, 'course full name'),
+                            'displayname' => new external_value(PARAM_TEXT, 'course display name'),
                             'shortname' => new external_value(PARAM_TEXT, 'course short name'),
                             'categoryid' => new external_value(PARAM_INT, 'category id'),
                             'categoryname' => new external_value(PARAM_TEXT, 'category name'),
index 4cb1e52..6afaae0 100644 (file)
@@ -1690,6 +1690,7 @@ function course_delete_module($cmid) {
 
     // Delete all tag instances associated with the instance of this module.
     core_tag_tag::delete_instances('mod_' . $modulename, null, $modcontext->id);
+    core_tag_tag::remove_all_item_tags('core', 'course_modules', $cm->id);
 
     // Delete the context.
     context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
index 65880f9..27b982b 100644 (file)
@@ -156,6 +156,7 @@ if (!empty($add)) {
     $data->completionexpected = $cm->completionexpected;
     $data->completionusegrade = is_null($cm->completiongradeitemnumber) ? 0 : 1;
     $data->showdescription    = $cm->showdescription;
+    $data->tags               = core_tag_tag::get_item_tags_array('core', 'course_modules', $cm->id);
     if (!empty($CFG->enableavailability)) {
         $data->availabilityconditionsjson = $cm->availability;
     }
index b878ca1..56814ad 100644 (file)
@@ -151,6 +151,11 @@ function add_moduleinfo($moduleinfo, $course, $mform = null) {
         $DB->set_field($moduleinfo->modulename, 'intro', $moduleinfo->intro, array('id'=>$moduleinfo->instance));
     }
 
+    // Add module tags.
+    if (core_tag_tag::is_enabled('core', 'course_modules') && isset($moduleinfo->tags)) {
+        core_tag_tag::set_item_tags('core', 'course_modules', $moduleinfo->coursemodule, $modcontext, $moduleinfo->tags);
+    }
+
     // Course_modules and course_sections each contain a reference to each other.
     // So we have to update one of them twice.
     $sectionid = course_add_cm_to_section($course, $moduleinfo->coursemodule, $moduleinfo->section);
@@ -578,6 +583,11 @@ function update_moduleinfo($cm, $moduleinfo, $course, $mform = null) {
         set_coursemodule_idnumber($moduleinfo->coursemodule, $moduleinfo->cmidnumber);
     }
 
+    // Update module tags.
+    if (core_tag_tag::is_enabled('core', 'course_modules') && isset($moduleinfo->tags)) {
+        core_tag_tag::set_item_tags('core', 'course_modules', $moduleinfo->coursemodule, $modcontext, $moduleinfo->tags);
+    }
+
     // Now that module is fully updated, also update completion data if required.
     // (this will wipe all user completion data and recalculate it)
     if ($completion->is_enabled() && !empty($moduleinfo->completionunlocked)) {
index 1f493d0..841a261 100644 (file)
@@ -597,6 +597,16 @@ abstract class moodleform_mod extends moodleform {
             $mform->disabledIf('completionexpected', 'completion', 'eq', COMPLETION_TRACKING_NONE);
         }
 
+        // Populate module tags.
+        if (core_tag_tag::is_enabled('core', 'course_modules')) {
+            $mform->addElement('header', 'tagshdr', get_string('tags', 'tag'));
+            $mform->addElement('tags', 'tags', get_string('tags'), array('itemtype' => 'course_modules', 'component' => 'core'));
+            if ($this->_cm) {
+                $tags = core_tag_tag::get_item_tags_array('core', 'course_modules', $this->_cm->id);
+                $mform->setDefault('tags', $tags);
+            }
+        }
+
         $this->standard_hidden_coursemodule_elements();
     }
 
index de74065..0e4e9de 100644 (file)
@@ -1522,9 +1522,12 @@ class core_course_courselib_testcase extends advanced_testcase {
             case 'assign':
                 // Add some tags to this assignment.
                 core_tag_tag::set_item_tags('mod_assign', 'assign', $module->id, $modcontext, array('Tag 1', 'Tag 2', 'Tag 3'));
+                core_tag_tag::set_item_tags('core', 'course_modules', $module->cmid, $modcontext, array('Tag 3', 'Tag 4', 'Tag 5'));
 
                 // Confirm the tag instances were added.
-                $criteria = array('component' => 'mod_assign', 'contextid' => $modcontext->id);
+                $criteria = array('component' => 'mod_assign', 'itemtype' => 'assign', 'contextid' => $modcontext->id);
+                $this->assertEquals(3, $DB->count_records('tag_instance', $criteria));
+                $criteria = array('component' => 'core', 'itemtype' => 'course_modules', 'contextid' => $modcontext->id);
                 $this->assertEquals(3, $DB->count_records('tag_instance', $criteria));
 
                 // Verify event assignment event has been generated.
@@ -1565,6 +1568,9 @@ class core_course_courselib_testcase extends advanced_testcase {
                 // Verify the tag instances were deleted.
                 $criteria = array('component' => 'mod_assign', 'contextid' => $modcontext->id);
                 $this->assertEquals(0, $DB->count_records('tag_instance', $criteria));
+
+                $criteria = array('component' => 'core', 'itemtype' => 'course_modules', 'contextid' => $modcontext->id);
+                $this->assertEquals(0, $DB->count_records('tag_instance', $criteria));
                 break;
             case 'quiz':
                 // Verify category deleted.
index 5659d9f..e563b60 100644 (file)
@@ -132,21 +132,30 @@ class enrol_meta_plugin extends enrol_plugin {
      * Add new instance of enrol plugin.
      * @param object $course
      * @param array $fields instance fields
-     * @return int id of new instance, null if can not be created
+     * @return int id of last instance, null if can not be created
      */
     public function add_instance($course, array $fields = null) {
         global $CFG;
 
         require_once("$CFG->dirroot/enrol/meta/locallib.php");
 
-        if (!empty($fields['customint2']) && $fields['customint2'] == ENROL_META_CREATE_GROUP) {
-            $context = context_course::instance($course->id);
-            require_capability('moodle/course:managegroups', $context);
-            $groupid = enrol_meta_create_new_group($course->id, $fields['customint1']);
-            $fields['customint2'] = $groupid;
+        // Support creating multiple at once.
+        if (is_array($fields['customint1'])) {
+            $courses = array_unique($fields['customint1']);
+        } else {
+            $courses = array($fields['customint1']);
         }
+        foreach ($courses as $courseid) {
+            if (!empty($fields['customint2']) && $fields['customint2'] == ENROL_META_CREATE_GROUP) {
+                $context = context_course::instance($course->id);
+                require_capability('moodle/course:managegroups', $context);
+                $groupid = enrol_meta_create_new_group($course->id, $courseid);
+                $fields['customint2'] = $groupid;
+            }
 
-        $result = parent::add_instance($course, $fields);
+            $fields['customint1'] = $courseid;
+            $result = parent::add_instance($course, $fields);
+        }
 
         enrol_meta_sync($course->id);
 
@@ -258,7 +267,7 @@ class enrol_meta_plugin extends enrol_plugin {
         }
 
         // TODO: this has to be done via ajax or else it will fail very badly on large sites!
-        $courses = array('' => get_string('choosedots'));
+        $courses = array();
         $select = ', ' . context_helper::get_preload_record_columns_sql('ctx');
         $join = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
 
@@ -313,10 +322,20 @@ class enrol_meta_plugin extends enrol_plugin {
     public function edit_instance_form($instance, MoodleQuickForm $mform, $coursecontext) {
         global $DB;
 
-        $courses = $this->get_course_options($instance, $coursecontext);
         $groups = $this->get_group_options($coursecontext);
+        $existing = $DB->get_records('enrol', array('enrol' => 'meta', 'courseid' => $coursecontext->instanceid), '', 'customint1, id');
 
-        $mform->addElement('select', 'customint1', get_string('linkedcourse', 'enrol_meta'), $courses);
+        $excludelist = array($coursecontext->instanceid);
+        foreach ($existing as $existinginstance) {
+            $excludelist[] = $existinginstance->customint1;
+        }
+
+        $options = array(
+            'requiredcapabilities' => array('enrol/meta:selectaslinked'),
+            'multiple' => true,
+            'exclude' => $excludelist
+        );
+        $mform->addElement('course', 'customint1', get_string('linkedcourse', 'enrol_meta'), $options);
         $mform->addRule('customint1', get_string('required'), 'required', null, 'client');
         if (!empty($instance->id)) {
             $mform->freeze('customint1');
@@ -343,28 +362,25 @@ class enrol_meta_plugin extends enrol_plugin {
         $c = false;
 
         if (!empty($data['customint1'])) {
-            $c = $DB->get_record('course', array('id' => $data['customint1']));
-        }
-
-        if (!$c) {
-            $errors['customint1'] = get_string('required');
-        } else {
-            $coursecontext = context_course::instance($c->id);
-            $existing = $DB->get_records('enrol', array('enrol' => 'meta', 'courseid' => $thiscourseid), '', 'customint1, id');
-            if (!$c->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
-                $errors['customint1'] = get_string('error');
-            } else if (!has_capability('enrol/meta:selectaslinked', $coursecontext)) {
-                $errors['customint1'] = get_string('error');
-            } else if ($c->id == SITEID or $c->id == $thiscourseid or isset($existing[$c->id])) {
-                $errors['customint1'] = get_string('error');
+            foreach ($data['customint1'] as $courseid) {
+                $c = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
+                $coursecontext = context_course::instance($c->id);
+                $existing = $DB->get_records('enrol', array('enrol' => 'meta', 'courseid' => $thiscourseid), '', 'customint1, id');
+                if (!$c->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
+                    $errors['customint1'] = get_string('error');
+                } else if (!has_capability('enrol/meta:selectaslinked', $coursecontext)) {
+                    $errors['customint1'] = get_string('error');
+                } else if ($c->id == SITEID or $c->id == $thiscourseid or isset($existing[$c->id])) {
+                    $errors['customint1'] = get_string('error');
+                }
             }
+        } else {
+            $errors['customint1'] = get_string('required');
         }
 
-        $validcourses = array_keys($this->get_course_options($instance, $context));
         $validgroups = array_keys($this->get_group_options($context));
 
         $tovalidate = array(
-            'customint1' => $validcourses,
             'customint2' => $validgroups
         );
         $typeerrors = $this->validate_param_types($data, $tovalidate);
index 9a07371..c5df107 100644 (file)
@@ -1,4 +1,4 @@
-@enrol @enrol_meta
+@enrol @enrol_meta @javascript
 Feature: Enrolments are synchronised with meta courses
   In order to simplify enrolments in parent courses
   As a teacher
@@ -13,21 +13,21 @@ Feature: Enrolments are synchronised with meta courses
       | student4 | Student | 4 | student4@asd.com |
     And the following "courses" exist:
       | fullname | shortname |
-      | Course 1 | C1 |
-      | Course 2 | C2 |
-      | Course 3 | C3 |
+      | Course 1 | C1C1 |
+      | Course 2 | C2C2 |
+      | Course 3 | C3C3 |
     And the following "groups" exist:
       | name | course | idnumber |
-      | Groupcourse 1 | C3 | G1 |
-      | Groupcourse 2 | C3 | G2 |
+      | Groupcourse 1 | C3C3 | G1 |
+      | Groupcourse 2 | C3C3 | G2 |
     And the following "course enrolments" exist:
       | user | course | role |
-      | student1 | C1 | student |
-      | student2 | C1 | student |
-      | student3 | C1 | student |
-      | student4 | C1 | student |
-      | student1 | C2 | student |
-      | student2 | C2 | student |
+      | student1 | C1C1 | student |
+      | student2 | C1C1 | student |
+      | student3 | C1C1 | student |
+      | student4 | C1C1 | student |
+      | student1 | C2C2 | student |
+      | student2 | C2C2 | student |
     And I log in as "admin"
     And I navigate to "Manage enrol plugins" node in "Site administration > Plugins > Enrolments"
     And I click on "Enable" "link" in the "Course meta link" "table_row"
@@ -37,7 +37,7 @@ Feature: Enrolments are synchronised with meta courses
   Scenario: Add meta enrolment instance without groups
     When I follow "Course 3"
     And I add "Course meta link" enrolment method with:
-      | Link course  | Course 1 |
+      | Link course  | C1C1 |
     And I navigate to "Enrolled users" node in "Course administration > Users"
     Then I should see "Student 1"
     And I should see "Student 4"
@@ -45,18 +45,13 @@ Feature: Enrolments are synchronised with meta courses
 
   Scenario: Add meta enrolment instance with groups
     When I follow "Course 3"
-    And I navigate to "Enrolment methods" node in "Course administration > Users"
-    And I select "Course meta link" from the "Add method" singleselect
-    And I set the following fields to these values:
-      | Link course  | Course 1      |
+    And I add "Course meta link" enrolment method with:
+      | Link course  | C1C1      |
       | Add to group | Groupcourse 1 |
-    And I press "Add method"
-    And I set the field "Add method" to "Course meta link"
-    And I press "Go"
-    And I set the following fields to these values:
-      | Link course  | Course 2      |
+    And I follow "Course 3"
+    And I add "Course meta link" enrolment method with:
+      | Link course  | C2C2      |
       | Add to group | Groupcourse 2 |
-    And I press "Add method"
     And I navigate to "Enrolled users" node in "Course administration > Users"
     Then I should see "Groupcourse 1" in the "Student 1" "table_row"
     And I should see "Groupcourse 1" in the "Student 2" "table_row"
@@ -69,13 +64,9 @@ Feature: Enrolments are synchronised with meta courses
 
   Scenario: Add meta enrolment instance with auto-created groups
     When I follow "Course 3"
-    And I navigate to "Enrolment methods" node in "Course administration > Users"
-    And I set the field "Add method" to "Course meta link"
-    And I press "Go"
-    And I set the following fields to these values:
-      | Link course  | Course 1      |
+    And I add "Course meta link" enrolment method with:
+      | Link course  | C1C1      |
       | Add to group | Create new group |
-    And I press "Add method"
     And I navigate to "Enrolled users" node in "Course administration > Users"
     Then I should see "Course 1 course" in the "Student 1" "table_row"
     And I should see "Course 1 course" in the "Student 2" "table_row"
@@ -86,17 +77,12 @@ Feature: Enrolments are synchronised with meta courses
 
   Scenario: Backup and restore of meta enrolment instance
     When I follow "Course 3"
-    And I navigate to "Enrolment methods" node in "Course administration > Users"
-    And I set the field "Add method" to "Course meta link"
-    And I press "Go"
-    And I set the following fields to these values:
-      | Link course  | Course 1      |
+    And I add "Course meta link" enrolment method with:
+      | Link course  | C1C1      |
       | Add to group | Groupcourse 1 |
-    And I press "Add method"
-    And I select "Course meta link" from the "Add method" singleselect
-    And I set the following fields to these values:
-      | Link course  | Course 2      |
-    And I press "Add method"
+    And I follow "Course 3"
+    And I add "Course meta link" enrolment method with:
+      | Link course  | C2C2      |
     When I backup "Course 3" course using this options:
       | Confirmation | Filename | test_backup.mbz |
     And I click on "Restore" "link" in the "test_backup.mbz" "table_row"
index 9695c9c..9886fe2 100644 (file)
@@ -237,6 +237,11 @@ if ($mform->is_cancelled()) {
 
     $grade_item->outcomeid = null;
 
+    if (!empty($data->grade_item_rescalegrades) && $data->grade_item_rescalegrades == 'yes') {
+        $grade_item->rescale_grades_keep_percentage($grade_item_copy->grademin, $grade_item_copy->grademax, $grade_item->grademin,
+                $grade_item->grademax, 'gradebook');
+    }
+
     // update hiding flag
     if ($hiddenuntil) {
         $grade_item->set_hidden($hiddenuntil, false);
index b090740..98a18a4 100644 (file)
@@ -32,7 +32,7 @@ class edit_category_form extends moodleform {
     private $aggregation_options = array();
 
     function definition() {
-        global $CFG, $COURSE, $DB;
+        global $CFG, $COURSE, $DB, $OUTPUT;
         $mform =& $this->_form;
 
         $category = $this->_customdata['current'];
@@ -104,6 +104,25 @@ class edit_category_form extends moodleform {
         $mform->addHelpButton('grade_item_idnumber', 'idnumbermod');
         $mform->setType('grade_item_idnumber', PARAM_RAW);
 
+        if (!empty($category->id)) {
+            $gradecategory = grade_category::fetch(array('id' => $category->id));
+            $gradeitem = $gradecategory->load_grade_item();
+
+            // If grades exist set a message so the user knows why they can not alter the grade type or scale.
+            // We could never change the grade type for external items, so only need to show this for manual grade items.
+            if ($gradeitem->has_overridden_grades()) {
+                // Set a message so the user knows why the can not alter the grade type or scale.
+                if ($gradeitem->gradetype == GRADE_TYPE_SCALE) {
+                    $gradesexistmsg = get_string('modgradecategorycantchangegradetyporscalemsg', 'grades');
+                } else {
+                    $gradesexistmsg = get_string('modgradecategorycantchangegradetypemsg', 'grades');
+                }
+                $notification = new \core\output\notification($gradesexistmsg, \core\output\notification::NOTIFY_INFO);
+                $notification->set_show_closebutton(false);
+                $mform->addElement('static', 'gradesexistmsg', '', $OUTPUT->render($notification));
+            }
+        }
+
         $options = array(GRADE_TYPE_NONE=>get_string('typenone', 'grades'),
                          GRADE_TYPE_VALUE=>get_string('typevalue', 'grades'),
                          GRADE_TYPE_SCALE=>get_string('typescale', 'grades'),
@@ -140,6 +159,14 @@ class edit_category_form extends moodleform {
         $mform->disabledIf('grade_item_scaleid', 'grade_item_gradetype', 'noteq', GRADE_TYPE_SCALE);
         $mform->disabledIf('grade_item_scaleid', 'aggregation', 'eq', GRADE_AGGREGATE_SUM);
 
+        $choices = array();
+        $choices[''] = get_string('choose');
+        $choices['no'] = get_string('no');
+        $choices['yes'] = get_string('yes');
+        $mform->addElement('select', 'grade_item_rescalegrades', get_string('modgradecategoryrescalegrades', 'grades'), $choices);
+        $mform->addHelpButton('grade_item_rescalegrades', 'modgradecategoryrescalegrades', 'grades');
+        $mform->disabledIf('grade_item_rescalegrades', 'grade_item_gradetype', 'noteq', GRADE_TYPE_VALUE);
+
         $mform->addElement('text', 'grade_item_grademax', get_string('grademax', 'grades'));
         $mform->setType('grade_item_grademax', PARAM_RAW);
         $mform->addHelpButton('grade_item_grademax', 'grademax', 'grades');
@@ -398,6 +425,8 @@ class edit_category_form extends moodleform {
                     }
                 }
             }
+
+            $mform->removeElement('grade_item_rescalegrades');
         }
 
 
@@ -423,6 +452,25 @@ class edit_category_form extends moodleform {
                 $mform->removeElement('grade_item_display');
                 $mform->removeElement('grade_item_decimals');
                 $mform->hardFreeze('grade_item_scaleid');
+            // Only show the option to rescale grades on a category if its corresponding grade_item has overridden grade_grades.
+            } else if ($grade_item->has_overridden_grades()) {
+                // Can't change the grade type or the scale if there are grades.
+                $mform->hardFreeze('grade_item_gradetype, grade_item_scaleid');
+
+                // If we are using scles then remove the unnecessary rescale and grade fields.
+                if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
+                    $mform->removeElement('grade_item_rescalegrades');
+                    $mform->removeElement('grade_item_grademax');
+                    if ($mform->elementExists('grade_item_grademin')) {
+                        $mform->removeElement('grade_item_grademin');
+                    }
+                } else { // Not using scale, so remove it.
+                    $mform->removeElement('grade_item_scaleid');
+                    $mform->disabledIf('grade_item_grademax', 'grade_item_rescalegrades', 'eq', '');
+                    $mform->disabledIf('grade_item_grademin', 'grade_item_rescalegrades', 'eq', '');
+                }
+            } else { // Remove the rescale element if there are no grades.
+                $mform->removeElement('grade_item_rescalegrades');
             }
 
             //remove the aggregation coef element if not needed
@@ -484,6 +532,11 @@ class edit_category_form extends moodleform {
 /// perform extra validation before submission
     function validation($data, $files) {
         global $COURSE;
+        $gradeitem = false;
+        if ($data['id']) {
+            $gradecategory = grade_category::fetch(array('id' => $data['id']));
+            $gradeitem = $gradecategory->load_grade_item();
+        }
 
         $errors = parent::validation($data, $files);
 
@@ -501,6 +554,16 @@ class edit_category_form extends moodleform {
              }
         }
 
+        if ($data['id'] && $gradeitem->has_overridden_grades()) {
+            if ($gradeitem->gradetype == GRADE_TYPE_VALUE) {
+                if (grade_floats_different($data['grade_item_grademin'], $gradeitem->grademin) ||
+                    grade_floats_different($data['grade_item_grademax'], $gradeitem->grademax)) {
+                    if (empty($data['grade_item_rescalegrades'])) {
+                        $errors['grade_item_rescalegrades'] = get_string('mustchooserescaleyesorno', 'grades');
+                    }
+                }
+            }
+        }
         return $errors;
     }
 }
index 153a770..88763ec 100644 (file)
@@ -137,7 +137,7 @@ class moodlequickform_guideeditor extends HTML_QuickForm_input {
             $html .= $renderer->display_regrade_confirmation($this->getName(), $this->regradeconfirmation, $data['regrade']);
         }
         if ($this->validationerrors) {
-            $html .= html_writer::div($renderer->notification($this->validationerrors, 'error'), '', array('role' => 'alert'));
+            $html .= html_writer::div($renderer->notification($this->validationerrors));
         }
         $html .= $renderer->display_guide($data['criteria'], $data['comments'], $data['options'], $mode, $this->getName());
         return $html;
index 533e81e..54601b5 100644 (file)
@@ -141,7 +141,7 @@ class MoodleQuickForm_rubriceditor extends HTML_QuickForm_input {
             $html .= $renderer->display_regrade_confirmation($this->getName(), $this->regradeconfirmation, $data['regrade']);
         }
         if ($this->validationerrors) {
-            $html .= html_writer::div($renderer->notification($this->validationerrors, 'error'), '', array('role' => 'alert'));
+            $html .= html_writer::div($renderer->notification($this->validationerrors));
         }
         $html .= $renderer->display_rubric($data['criteria'], $data['options'], $mode, $this->getName());
         return $html;
@@ -380,4 +380,4 @@ class MoodleQuickForm_rubriceditor extends HTML_QuickForm_input {
         $value =  $this->prepare_data($this->_findValue($submitValues));
         return $this->_prepareValue($value, $assoc);
     }
-}
\ No newline at end of file
+}
index 8a43bd5..7c97600 100644 (file)
@@ -329,6 +329,13 @@ abstract class screen {
                 continue;
             }
 
+            // If the user submits Exclude grade elements without the proper.
+            // permissions then we should refuse to update.
+            if ($matches[1] === 'exclude' && !has_capability('moodle/grade:manage', $this->context)){
+                $warnings[] = get_string('nopermissions', 'error', get_string('grade:manage', 'role'));
+                continue;
+            }
+
             $msg = $element->set($posted);
 
             // Optional type.
index ecb6f90..5e5054f 100644 (file)
@@ -141,6 +141,12 @@ abstract class tablelike extends screen {
                 $html .= $this->structure->get_grade_analysis_icon($grade);
             }
 
+            // Singleview users without proper permissions should be presented
+            // disabled checkboxes for the Exclude grade attribute.
+            if ($field == 'exclude' && !has_capability('moodle/grade:manage', $this->context)){
+                $html->disabled = true;
+            }
+
             $line[] = $html;
         }
         return $line;
index caddb1e..62f7780 100644 (file)
@@ -35,11 +35,14 @@ use grade_grade;
  * @copyright 2014 Moodle Pty Ltd (http://moodle.com)
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class exclude extends grade_attribute_format implements be_checked {
+class exclude extends grade_attribute_format implements be_checked, be_disabled {
 
     /** @var string $name The name of the input */
     public $name = 'exclude';
 
+    /** @var bool $disabled Is the checkbox disabled? */
+    public $disabled = false;
+
     /**
      * Is it checked?
      *
@@ -49,6 +52,15 @@ class exclude extends grade_attribute_format implements be_checked {
         return $this->grade->is_excluded();
     }
 
+    /**
+     * Is it disabled?
+     *
+     * @return bool
+     */
+    public function is_disabled() {
+        return $this->disabled;
+    }
+
     /**
      * Generate the element used to render the UI
      *
@@ -58,7 +70,8 @@ class exclude extends grade_attribute_format implements be_checked {
         return new checkbox_attribute(
             $this->get_name(),
             $this->get_label(),
-            $this->is_checked()
+            $this->is_checked(),
+            $this->is_disabled()
         );
     }
 
index f12404d..22aba02 100644 (file)
@@ -11,6 +11,7 @@ Feature: We can use Single view
     And the following "users" exist:
       | username | firstname | lastname | email | idnumber | alternatename |
       | teacher1 | Teacher | 1 | teacher1@example.com | t1 | fred |
+      | teacher2 | No edit | 1 | teacher2@example.com | t2 | nick |
       | student1 | Student | 1 | student1@example.com | s1 | james |
       | student2 | Student | 2 | student1@example.com | s2 | holly |
       | student3 | Student | 3 | student1@example.com | s3 | anna |
@@ -27,6 +28,7 @@ Feature: We can use Single view
     And the following "course enrolments" exist:
       | user | course | role |
       | teacher1 | C1 | editingteacher |
+      | teacher2 | C1 | teacher |
       | student1 | C1 | student |
       | student2 | C1 | student |
       | student3 | C1 | student |
@@ -44,6 +46,10 @@ Feature: We can use Single view
     And the following "grade items" exist:
       | itemname | course | gradetype |
       | Test grade item | C1 | Scale |
+    And the following "permission overrides" exist:
+      | capability                  | permission | role     | contextlevel  | reference |
+      | moodle/grade:edit           | Allow      | teacher  | Course        | C1        |
+      | gradereport/singleview:view | Allow      | teacher  | Course        | C1        |
     And I log in as "teacher1"
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
@@ -88,6 +94,14 @@ Feature: We can use Single view
     And the following should exist in the "generaltable" table:
         | First name (Alternate name) Surname | Grade |
         | james (Student) 1 | Very good |
+    And I log out
+    And I log in as "teacher2"
+    And I follow "Course 1"
+    And I navigate to "Grades" node in "Course administration"
+    And I click on "Single view" "option"
+    And I click on "Student 4" "option"
+    And the "Exclude for Test assignment one" "checkbox" should be disabled
+    And the "Override for Test assignment one" "checkbox" should be enabled
 
   Scenario: Single view links work on grade report.
     Given I follow "Single view for Test assignment one"
diff --git a/grade/tests/behat/grade_category_validation.feature b/grade/tests/behat/grade_category_validation.feature
new file mode 100644 (file)
index 0000000..0388b88
--- /dev/null
@@ -0,0 +1,108 @@
+@core_grades
+Feature: Editing a grade item
+  In order to ensure validation is provided to the teacher
+  As a teacher
+  I need to know why I can not add/edit values on the grade category form
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | student1 | Student | 1 | student1@example.com |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 1 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And I log in as "admin"
+    And I navigate to "Scales" node in "Site administration > Grades"
+    And I press "Add a new scale"
+    And I set the following fields to these values:
+      | Name  | ABCDEF |
+      | Scale | F,E,D,C,B,A |
+    And I press "Save changes"
+    And I press "Add a new scale"
+    And I set the following fields to these values:
+      | Name  | Letter scale |
+      | Scale | Disappointing, Good, Very good, Excellent |
+    And I press "Save changes"
+    And I set the following administration settings values:
+      | grade_aggregations_visible | Mean of grades,Weighted mean of grades,Simple weighted mean of grades,Mean of grades (with extra credits),Median of grades,Lowest grade,Highest grade,Mode of grades,Natural |
+    And I log out
+    And I log in as "teacher1"
+    And I am on site homepage
+    And I follow "Course 1"
+    And I navigate to "Gradebook setup" node in "Course administration"
+    And I press "Add category"
+    And I set the following fields to these values:
+      | Category name | Cat 1 |
+      | Aggregation   | Highest grade |
+    And I press "Save changes"
+    And I press "Add grade item"
+    And I set the following fields to these values:
+      | Item name | Item 1 |
+      | Grade category | Cat 1 |
+    And I press "Save changes"
+    And I press "Add grade item"
+    And I set the following fields to these values:
+      | Item name | Item 2 |
+      | Grade category | Cat 1 |
+    And I press "Save changes"
+
+  Scenario: Being able to change the grade type, scale and maximum grade for a grade category when there are no overridden grades
+    Given I click on "Edit" "link" in the "Cat 1" "table_row"
+    When I click on "Edit settings" "link" in the "Cat 1" "table_row"
+    Then I should not see "This category has associated grade items which have been overridden. Therefore some grades have already been awarded"
+    And I set the field "Grade type" to "Scale"
+    And I press "Save changes"
+    And I should see "Scale must be selected"
+    And I set the field "Scale" to "ABCDEF"
+    And I press "Save changes"
+    And I should not see "You cannot change the type, as grades already exist for this item"
+    And I click on "Edit" "link" in the "Cat 1" "table_row"
+    And I click on "Edit settings" "link" in the "Cat 1" "table_row"
+    And I should not see "This category has associated grade items which have been overridden. Therefore some grades have already been awarded"
+    And I set the field "Scale" to "Letter scale"
+    And I press "Save changes"
+    And I should not see "You cannot change the scale, as grades already exist for this item"
+
+  Scenario: Attempting to change a category item's grade type when overridden grades already exist
+    Given I navigate to "Grader report" node in "Grade administration"
+    And I turn editing mode on
+    And I give the grade "20.00" to the user "Student 1" for the grade item "Cat 1 total"
+    And I press "Save changes"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
+    And I click on "Edit" "link" in the "Cat 1" "table_row"
+    When I click on "Edit settings" "link" in the "Cat 1" "table_row"
+    Then I should see "This category has associated grade items which have been overridden. Therefore some grades have already been awarded, so the grade type cannot be changed. If you wish to change the maximum grade, you must first choose whether or not to rescale existing grades."
+    And "//div[contains(concat(' ', normalize-space(@class), ' '), 'fstatic') and contains(text(), 'Value')]" "xpath_element" should exist
+
+  Scenario: Attempting to change a category item's scale when overridden grades already exist
+    Given I click on "Edit" "link" in the "Cat 1" "table_row"
+    And I click on "Edit settings" "link" in the "Cat 1" "table_row"
+    And I set the field "Grade type" to "Scale"
+    And I set the field "Scale" to "ABCDEF"
+    And I press "Save changes"
+    And I navigate to "Grader report" node in "Grade administration"
+    And I turn editing mode on
+    And I give the grade "C" to the user "Student 1" for the grade item "Cat 1 total"
+    And I press "Save changes"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
+    And I click on "Edit" "link" in the "Cat 1" "table_row"
+    When I click on "Edit settings" "link" in the "Cat 1" "table_row"
+    Then I should see "This category has associated grade items which have been overridden. Therefore some grades have already been awarded, so the grade type and scale cannot be changed."
+    And "//div[contains(concat(' ', normalize-space(@class), ' '), 'fstatic') and contains(text(), 'ABCDEF')]" "xpath_element" should exist
+
+  Scenario: Attempting to change a category item's maximum grade when no rescaling option has been chosen
+    Given I navigate to "Grader report" node in "Grade administration"
+    And I turn editing mode on
+    And I give the grade "20.00" to the user "Student 1" for the grade item "Cat 1 total"
+    And I press "Save changes"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
+    And I click on "Edit" "link" in the "Cat 1" "table_row"
+    And I click on "Edit settings" "link" in the "Cat 1" "table_row"
+    And I set the field "Maximum grade" to "50"
+    When I press "Save changes"
+    Then I should see "You must choose whether to rescale existing grades or not."
index a38048b..1ed26d1 100644 (file)
@@ -1028,7 +1028,8 @@ $string['taskdeleteincompleteusers'] = 'Delete incomplete users';
 $string['taskdeleteunconfirmedusers'] = 'Delete unconfirmed users';
 $string['taskeventscron'] = 'Background processing for events';
 $string['taskfiletrashcleanup'] = 'Cleanup files in trash';
-$string['taskglobalsearch'] = 'Global search indexing';
+$string['taskglobalsearchindex'] = 'Global search indexing';
+$string['taskglobalsearchoptimize'] = 'Global search index optimization';
 $string['taskgradecron'] = 'Background processing for gradebook';
 $string['tasklegacycron'] = 'Legacy cron processing for plugins';
 $string['taskmessagingcleanup'] = 'Background processing for messaging';
index f37a5fe..c5e9dab 100644 (file)
@@ -477,6 +477,8 @@ $string['modgrade_help'] = 'Select the type of grading used for this activity. I
 $string['modgradecantchangegradetype'] = 'You cannot change the type, as grades already exist for this item.';
 $string['modgradecantchangegradetypemsg'] = 'Some grades have already been awarded, so the grade type cannot be changed. If you wish to change the maximum grade, you must first choose whether or not to rescale existing grades.';
 $string['modgradecantchangegradetyporscalemsg'] = 'Some grades have already been awarded, so the grade type and scale cannot be changed.';
+$string['modgradecategorycantchangegradetypemsg'] = 'This category has associated grade items which have been overridden. Therefore some grades have already been awarded, so the grade type cannot be changed. If you wish to change the maximum grade, you must first choose whether or not to rescale existing grades.';
+$string['modgradecategorycantchangegradetyporscalemsg'] = 'This category has associated grade items which have been overridden. Therefore some grades have already been awarded, so the grade type and scale cannot be changed.';
 $string['modgradecantchangescale'] = 'You cannot change the scale, as grades already exist for this item.';
 $string['modgradecantchangeratingmaxgrade'] = 'You cannot change the maximum grade when grades already exist for an activity with ratings.';
 $string['modgradedonotmodify'] = 'Do not modify existing grades';
@@ -489,6 +491,12 @@ $string['modgraderescalegrades_help'] = 'When changing the maximum grades on a g
 If this is set to \'Yes\', any existing grades will be rescaled so that the percentage grade remains the same.
 
 For example, if this option is set to \'Yes\', changing the maximum grade on an item from 10 to 20 would cause a grade of 6/10 (60%) to be rescaled to 12/20 (60%). With this option set to \'No\', the grade would change from 6/10 (60%) to 6/20 (30%), requiring manual adjustment of the grade items to ensure correct scores.';
+$string['modgradecategoryrescalegrades'] = 'Rescale overridden grades';
+$string['modgradecategoryrescalegrades_help'] = 'When changing the maximum grades on a gradebook item you need to specify whether or not this will cause existing percentage grades to change as well.
+
+If this is set to \'Yes\', any existing overridden grades will be rescaled so that the percentage grade remains the same.
+
+For example, if this option is set to \'Yes\', changing the maximum grade on an item from 10 to 20 would cause a grade of 6/10 (60%) to be rescaled to 12/20 (60%). With this option set to \'No\', the grade will remain unchanged, requiring manual adjustment of the grade items to ensure correct scores.';
 $string['modgradetype'] = 'Type';
 $string['modgradetypenone'] = 'None';
 $string['modgradetypepoint'] = 'Point';
index 335cc75..b3d0a5d 100644 (file)
@@ -161,6 +161,8 @@ $string['type_report'] = 'Site report';
 $string['type_report_plural'] = 'Reports';
 $string['type_repository'] = 'Repository';
 $string['type_repository_plural'] = 'Repositories';
+$string['type_search'] = 'Search engine';
+$string['type_search_plural'] = 'Search engines';
 $string['type_theme'] = 'Theme';
 $string['type_theme_plural'] = 'Themes';
 $string['type_tool'] = 'Admin tool';
index 552cdcc..af4bd3f 100644 (file)
@@ -116,6 +116,7 @@ $string['tagarea_blog_external'] = 'External blog posts';
 $string['tagarea_post'] = 'Blog posts';
 $string['tagarea_user'] = 'User interests';
 $string['tagarea_course'] = 'Courses';
+$string['tagarea_course_modules'] = 'Course modules';
 $string['tagareaenabled'] = 'Enabled';
 $string['tagareaname'] = 'Name';
 $string['tagareas'] = 'Tag areas';
index 5deb194..3df2cc3 100644 (file)
@@ -9538,22 +9538,10 @@ class admin_setting_searchsetupinfo extends admin_setting {
 
         $return .= $brtag . get_string('searchsetupdescription', 'search') . $brtag . $brtag;
 
-        // Enable global search.
-        $row = array();
-        $url = new moodle_url("/admin/search.php?query=enableglobalsearch");
-        $row[0] = '1. ' . html_writer::tag('a', get_string('enableglobalsearch', 'admin'),
-                        array('href' => $url));
-        $status = html_writer::tag('span', get_string('no'), array('class' => 'statuscritical'));
-        if (\core_search\manager::is_global_search_enabled()) {
-            $status = html_writer::tag('span', get_string('yes'), array('class' => 'statusok'));
-        }
-        $row[1] = $status;
-        $table->data[] = $row;
-
         // Select a search engine.
         $row = array();
         $url = new moodle_url('/admin/settings.php?section=manageglobalsearch#admin-searchengine');
-        $row[0] = '2. ' . html_writer::tag('a', get_string('selectsearchengine', 'admin'),
+        $row[0] = '1. ' . html_writer::tag('a', get_string('selectsearchengine', 'admin'),
                         array('href' => $url));
 
         $status = html_writer::tag('span', get_string('no'), array('class' => 'statuscritical'));
@@ -9568,7 +9556,7 @@ class admin_setting_searchsetupinfo extends admin_setting {
         // Available areas.
         $row = array();
         $url = new moodle_url('/admin/settings.php?section=manageglobalsearch#admin-searchengine');
-        $row[0] = '3. ' . html_writer::tag('a', get_string('enablesearchareas', 'admin'),
+        $row[0] = '2. ' . html_writer::tag('a', get_string('enablesearchareas', 'admin'),
                         array('href' => $url));
 
         $status = html_writer::tag('span', get_string('no'), array('class' => 'statuscritical'));
@@ -9582,11 +9570,11 @@ class admin_setting_searchsetupinfo extends admin_setting {
         // Setup search engine.
         $row = array();
         if (empty($CFG->searchengine)) {
-            $row[0] = '4. ' . get_string('setupsearchengine', 'admin');
+            $row[0] = '3. ' . get_string('setupsearchengine', 'admin');
             $row[1] = html_writer::tag('span', get_string('no'), array('class' => 'statuscritical'));
         } else {
             $url = new moodle_url('/admin/settings.php?section=search' . $CFG->searchengine);
-            $row[0] = '4. ' . html_writer::tag('a', get_string('setupsearchengine', 'admin'),
+            $row[0] = '3. ' . html_writer::tag('a', get_string('setupsearchengine', 'admin'),
                             array('href' => $url));
             // Check the engine status.
             $searchengine = \core_search\manager::search_engine_instance();
@@ -9603,7 +9591,7 @@ class admin_setting_searchsetupinfo extends admin_setting {
         // Indexed data.
         $row = array();
         $url = new moodle_url('/report/search/index.php#searchindexform');
-        $row[0] = '5. ' . html_writer::tag('a', get_string('indexdata', 'admin'), array('href' => $url));
+        $row[0] = '4. ' . html_writer::tag('a', get_string('indexdata', 'admin'), array('href' => $url));
         if ($anyindexed) {
             $status = html_writer::tag('span', get_string('yes'), array('class' => 'statusok'));
         } else {
@@ -9612,6 +9600,18 @@ class admin_setting_searchsetupinfo extends admin_setting {
         $row[1] = $status;
         $table->data[] = $row;
 
+        // Enable global search.
+        $row = array();
+        $url = new moodle_url("/admin/search.php?query=enableglobalsearch");
+        $row[0] = '5. ' . html_writer::tag('a', get_string('enableglobalsearch', 'admin'),
+                        array('href' => $url));
+        $status = html_writer::tag('span', get_string('no'), array('class' => 'statuscritical'));
+        if (\core_search\manager::is_global_search_enabled()) {
+            $status = html_writer::tag('span', get_string('yes'), array('class' => 'statusok'));
+        }
+        $row[1] = $status;
+        $table->data[] = $row;
+
         $return .= html_writer::table($table);
 
         return highlight($query, $return);
index e2e319a..d87f4ea 100644 (file)
@@ -32,6 +32,10 @@ require_once(dirname(__FILE__) . '/../../config.php');
 /** Include course lib for its functions */
 require_once($CFG->dirroot.'/course/lib.php');
 
+if (!empty($CFG->forcelogin)) {
+    require_login();
+}
+
 try {
     // Start buffer capture so that we can `remove` any errors
     ob_start();
index c7c80e1..3af51d6 100644 (file)
Binary files a/lib/amd/build/ajax.min.js and b/lib/amd/build/ajax.min.js differ
index 1adc2b0..d024ddb 100644 (file)
Binary files a/lib/amd/build/form-autocomplete.min.js and b/lib/amd/build/form-autocomplete.min.js differ
diff --git a/lib/amd/build/form-course-selector.min.js b/lib/amd/build/form-course-selector.min.js
new file mode 100644 (file)
index 0000000..6eaaee6
Binary files /dev/null and b/lib/amd/build/form-course-selector.min.js differ
index 38a393b..0ba0d5d 100644 (file)
@@ -148,7 +148,8 @@ define(['jquery', 'core/config'], function($, config) {
                 context: requests,
                 dataType: 'json',
                 processData: false,
-                async: async
+                async: async,
+                contentType: "application/json"
             };
 
             var script = config.wwwroot + '/lib/ajax/service.php?sesskey=' + config.sesskey;
index 24d32c4..474708e 100644 (file)
@@ -469,12 +469,20 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                 if (!option.prop('selected')) {
                     option.remove();
                 } else {
-                    existingValues.push(option.attr('value'));
+                    existingValues.push(String(option.attr('value')));
                 }
             });
+
+            if (!options.multiple && originalSelect.children('option').length === 0) {
+                // If this is a single select - and there are no current options
+                // the first option added will be selected by the browser. This causes a bug!
+                // We need to insert an empty option so that none of the real options are selected.
+                var option = $('<option>');
+                originalSelect.append(option);
+            }
             // And add all the new ones returned from ajax.
             $.each(processedResults, function(resultIndex, result) {
-                if (existingValues.indexOf(result.value) === -1) {
+                if (existingValues.indexOf(String(result.value)) === -1) {
                     var option = $('<option>');
                     option.append(result.label);
                     option.attr('value', result.value);
@@ -562,7 +570,13 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
         });
         // Handler used to force set the value from behat.
         inputElement.on('behat:set-value', function() {
-            if (options.tags) {
+            var suggestionsElement = $(document.getElementById(state.suggestionsId));
+            if ((inputElement.attr('aria-expanded') === "true") &&
+                    (suggestionsElement.children('[aria-selected=true]').length > 0)) {
+                // If the suggestion list has an active item, select it.
+                selectCurrentItem(options, state, originalSelect);
+            } else if (options.tags) {
+                // If tags are enabled, create a tag.
                 createItem(options, state, originalSelect);
             }
         });
@@ -747,11 +761,22 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                 // If this field uses ajax, set it up.
                 if (options.ajax) {
                     require([options.ajax], function(ajaxHandler) {
+                        var throttleTimeout = null;
                         var handler = function(e) {
                             updateAjax(e, options, state, originalSelect, ajaxHandler);
                         };
+
+                        // For input events, we do not want to trigger many, many updates.
+                        var throttledHandler = function(e) {
+                            if (throttleTimeout !== null) {
+                                window.clearTimeout(throttleTimeout);
+                                throttleTimeout = null;
+                            }
+                            throttleTimeout = window.setTimeout(handler.bind(this, e), 300);
+                        };
                         // Trigger an ajax update after the text field value changes.
-                        inputElement.on("input keypress", handler);
+                        inputElement.on("input keypress", throttledHandler);
+
                         var arrowElement = $(document.getElementById(state.downArrowId));
                         arrowElement.on("click", handler);
                     });
diff --git a/lib/amd/src/form-course-selector.js b/lib/amd/src/form-course-selector.js
new file mode 100644 (file)
index 0000000..2bf7aed
--- /dev/null
@@ -0,0 +1,76 @@
+// 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/>.
+
+/**
+ * Course selector adaptor for auto-complete form element.
+ *
+ * @module     core/form-course-selector
+ * @class      form-course-selector
+ * @package    core
+ * @copyright  2016 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      3.1
+ */
+define(['core/ajax', 'jquery'], function(ajax, $) {
+
+    return /** @alias module:core/form-course-selector */ {
+        // Public variables and functions.
+        processResults: function(selector, data) {
+            // Mangle the results into an array of objects.
+            var results = [], i = 0;
+            var excludelist = String($(selector).data('exclude')).split(',');
+
+            for (i = 0; i < data.courses.length; i++) {
+                if (excludelist.indexOf(String(data.courses[i].id)) === -1) {
+                    results.push({ value: data.courses[i].id, label: data.courses[i].displayname });
+                }
+            }
+            return results;
+        },
+
+        transport: function(selector, query, success, failure) {
+            // Parse some data-attributes from the form element.
+            var requiredcapabilities = $(selector).data('requiredcapabilities');
+            if (requiredcapabilities.trim() !== "") {
+                requiredcapabilities = requiredcapabilities.split(',');
+            } else {
+                requiredcapabilities = [];
+            }
+            // Build the query.
+            var promise = null;
+
+            if (typeof query === "undefined") {
+                query = '';
+            }
+
+            var searchargs = {
+                criterianame: 'search',
+                criteriavalue: query,
+                page: 0,
+                perpage: 100,
+                requiredcapabilities: requiredcapabilities
+            };
+            // Go go go!
+            promise = ajax.call([{
+                methodname: 'core_course_search_courses', args: searchargs
+            }]);
+
+            promise[0].done(success);
+            promise[0].fail(failure);
+
+            return promise;
+        }
+    };
+});
index 3bd7249..aadfbc1 100644 (file)
@@ -48,6 +48,9 @@ class behat_form_autocomplete extends behat_form_text {
             throw new coding_exception('Setting the valid of an autocomplete field requires javascript.');
         }
         $this->field->setValue($value);
+        // After the value is set, there is a 400ms throttle and then search. So adding 2 sec. delay to ensure both
+        // throttle + search finishes.
+        sleep(2);
         $id = $this->field->getAttribute('id');
         $js = ' require(["jquery"], function($) { $(document.getElementById("'.$id.'")).trigger("behat:set-value"); }); ';
         $this->session->executeScript($js);
similarity index 90%
rename from lib/classes/task/search_task.php
rename to lib/classes/task/search_index_task.php
index f22224a..bdeedf1 100644 (file)
@@ -30,7 +30,7 @@ namespace core\task;
  * @copyright  2015 David Monllao {@link http://www.davidmonllao.com}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class search_task extends scheduled_task {
+class search_index_task extends scheduled_task {
 
     /**
      * Get a descriptive name for this task (shown to admins).
@@ -38,7 +38,7 @@ class search_task extends scheduled_task {
      * @return string
      */
     public function get_name() {
-        return get_string('taskglobalsearch', 'admin');
+        return get_string('taskglobalsearchindex', 'admin');
     }
 
     /**
@@ -53,8 +53,5 @@ class search_task extends scheduled_task {
 
         // Indexing database records for modules + rich documents of forum.
         $globalsearch->index();
-
-        // Optimize index at last.
-        $globalsearch->optimize_index();
     }
 }
diff --git a/lib/classes/task/search_optimize_task.php b/lib/classes/task/search_optimize_task.php
new file mode 100644 (file)
index 0000000..9f4b8fe
--- /dev/null
@@ -0,0 +1,61 @@
+<?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/>.
+
+/**
+ * A scheduled task for global search.
+ *
+ * @package    core
+ * @copyright  2016 Eric Merrill {@link https://www.merrilldigital.com}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\task;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Runs search index optimization.
+ *
+ * @package    core
+ * @copyright  2016 Eric Merrill {@link https://www.merrilldigital.com}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class search_optimize_task extends scheduled_task {
+
+    /**
+     * Get a descriptive name for this task (shown to admins).
+     *
+     * @return string
+     */
+    public function get_name() {
+        return get_string('taskglobalsearchoptimize', 'admin');
+    }
+
+    /**
+     * Do the job.
+     * Throw exceptions on errors (the job will be retried).
+     */
+    public function execute() {
+        if (!\core_search\manager::is_global_search_enabled()) {
+            return;
+        }
+
+        $globalsearch = \core_search\manager::instance();
+
+        // Optimize index at last.
+        $globalsearch->optimize_index();
+    }
+}
index b99b5d6..7a44b1b 100644 (file)
@@ -248,6 +248,30 @@ class core_text {
         return $result;
     }
 
+    /**
+     * Truncates a string to no more than a certain number of bytes in a multi-byte safe manner.
+     * UTF-8 only!
+     *
+     * Many of the other charsets we test for (like ISO-2022-JP and EUC-JP) are not supported
+     * by typo3, and will give invalid results, so we are supporting UTF-8 only.
+     *
+     * @param string $string String to truncate
+     * @param int $bytes Maximum length of bytes in the result
+     * @return string Portion of string specified by $bytes
+     * @since Moodle 3.1
+     */
+    public static function str_max_bytes($string, $bytes) {
+        if (function_exists('mb_strcut')) {
+            return mb_strcut($string, 0, $bytes, 'UTF-8');
+        }
+
+        $oldlevel = error_reporting(E_PARSE);
+        $result = self::typo3()->strtrunc('utf-8', $string, $bytes);
+        error_reporting($oldlevel);
+
+        return $result;
+    }
+
     /**
      * Finds the last occurrence of a character in a string within another.
      * UTF-8 ONLY safe mb_strrchr().
@@ -707,4 +731,4 @@ class core_text {
         }
         return implode(' ', $words);
     }
-}
\ No newline at end of file
+}
index f11cb75..07c1faf 100644 (file)
@@ -49,6 +49,9 @@ class core_user {
     /** @var stdClass keep record of support user */
     public static $supportuser = false;
 
+    /** @var array store user fields properties cache. */
+    protected static $propertiescache = null;
+
     /**
      * Return user object from db or create noreply or support user,
      * if userid matches corse_user::NOREPLY_USER or corse_user::SUPPORT_USER
@@ -274,4 +277,104 @@ class core_user {
             throw new moodle_exception('suspended', 'auth');
         }
     }
+
+    /**
+     * Definition of user profile fields and the expected parameter type for data validation.
+     *
+     * @return void
+     */
+    protected static function fill_properties_cache() {
+
+        if (self::$propertiescache !== null) {
+            return;
+        }
+
+        // Array of user fields properties and expected parameters.
+        // Every new field on the user table should be added here otherwise it won't be validated.
+        $fields = array();
+        $fields['id'] = array('type' => PARAM_INT);
+        $fields['auth'] = array('type' => PARAM_NOTAGS);
+        $fields['confirmed'] = array('type' => PARAM_BOOL);
+        $fields['policyagreed'] = array('type' => PARAM_BOOL);
+        $fields['deleted'] = array('type' => PARAM_BOOL);
+        $fields['suspended'] = array('type' => PARAM_BOOL);
+        $fields['mnethostid'] = array('type' => PARAM_BOOL);
+        $fields['username'] = array('type' => PARAM_USERNAME);
+        $fields['password'] = array('type' => PARAM_NOTAGS);
+        $fields['idnumber'] = array('type' => PARAM_NOTAGS);
+        $fields['firstname'] = array('type' => PARAM_NOTAGS);
+        $fields['lastname'] = array('type' => PARAM_NOTAGS);
+        $fields['surname'] = array('type' => PARAM_NOTAGS);
+        $fields['email'] = array('type' => PARAM_RAW_TRIMMED);
+        $fields['emailstop'] = array('type' => PARAM_INT);
+        $fields['icq'] = array('type' => PARAM_NOTAGS);
+        $fields['skype'] = array('type' => PARAM_NOTAGS);
+        $fields['aim'] = array('type' => PARAM_NOTAGS);
+        $fields['yahoo'] = array('type' => PARAM_NOTAGS);
+        $fields['msn'] = array('type' => PARAM_NOTAGS);
+        $fields['phone1'] = array('type' => PARAM_NOTAGS);
+        $fields['phone2'] = array('type' => PARAM_NOTAGS);
+        $fields['institution'] = array('type' => PARAM_TEXT);
+        $fields['department'] = array('type' => PARAM_TEXT);
+        $fields['address'] = array('type' => PARAM_TEXT);
+        $fields['city'] = array('type' => PARAM_TEXT);
+        $fields['country'] = array('type' => PARAM_TEXT);
+        $fields['lang'] = array('type' => PARAM_TEXT);
+        $fields['calendartype'] = array('type' => PARAM_NOTAGS);
+        $fields['theme'] = array('type' => PARAM_NOTAGS);
+        $fields['timezones'] = array('type' => PARAM_TEXT);
+        $fields['firstaccess'] = array('type' => PARAM_INT);
+        $fields['lastaccess'] = array('type' => PARAM_INT);
+        $fields['lastlogin'] = array('type' => PARAM_INT);
+        $fields['currentlogin'] = array('type' => PARAM_INT);
+        $fields['lastip'] = array('type' => PARAM_NOTAGS);
+        $fields['secret'] = array('type' => PARAM_TEXT);
+        $fields['picture'] = array('type' => PARAM_INT);
+        $fields['url'] = array('type' => PARAM_URL);
+        $fields['description'] = array('type' => PARAM_CLEANHTML);
+        $fields['descriptionformat'] = array('type' => PARAM_INT);
+        $fields['mailformat'] = array('type' => PARAM_INT);
+        $fields['maildigest'] = array('type' => PARAM_INT);
+        $fields['maildisplay'] = array('type' => PARAM_INT);
+        $fields['autosubscribe'] = array('type' => PARAM_INT);
+        $fields['trackforums'] = array('type' => PARAM_INT);
+        $fields['timecreated'] = array('type' => PARAM_INT);
+        $fields['timemodified'] = array('type' => PARAM_INT);
+        $fields['trustbitmask'] = array('type' => PARAM_INT);
+        $fields['imagealt'] = array('type' => PARAM_TEXT);
+        $fields['lastnamephonetic'] = array('type' => PARAM_NOTAGS);
+        $fields['firstnamephonetic'] = array('type' => PARAM_NOTAGS);
+        $fields['middlename'] = array('type' => PARAM_NOTAGS);
+        $fields['alternatename'] = array('type' => PARAM_NOTAGS);
+
+        self::$propertiescache = $fields;
+    }
+
+    /**
+     * Get properties of a user field.
+     *
+     * @param string $property property name to be retrieved.
+     * @throws coding_exception if the requested property name is invalid.
+     * @return array the property definition.
+     */
+    public static function get_property_definition($property) {
+
+        self::fill_properties_cache();
+
+        if (!array_key_exists($property, self::$propertiescache)) {
+            throw new coding_exception('Invalid property requested.');
+        }
+
+        return self::$propertiescache[$property];
+    }
+
+    /**
+     * Clean the properties cache.
+     *
+     * During unit tests we need to be able to reset all caches so that each new test starts in a known state.
+     * Intended for use only for testing, phpunit calls this before every test.
+     */
+    public static function reset_caches() {
+        self::$propertiescache = null;
+    }
 }
index fc8811f..fb6a4ed 100644 (file)
@@ -1276,16 +1276,19 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
      *     - tagid - id of tag
      * @param array $options display options, same as in get_courses() except 'recursive' is ignored -
      *                       search is always category-independent
+     * @param array $requiredcapabilites List of capabilities required to see return course.
      * @return course_in_list[]
      */
-    public static function search_courses($search, $options = array()) {
+    public static function search_courses($search, $options = array(), $requiredcapabilities = array()) {
         global $DB;
         $offset = !empty($options['offset']) ? $options['offset'] : 0;
         $limit = !empty($options['limit']) ? $options['limit'] : null;
         $sortfields = !empty($options['sort']) ? $options['sort'] : array('sortorder' => 1);
 
         $coursecatcache = cache::make('core', 'coursecat');
-        $cachekey = 's-'. serialize($search + array('sort' => $sortfields));
+        $cachekey = 's-'. serialize(
+            $search + array('sort' => $sortfields) + array('requiredcapabilities' => $requiredcapabilities)
+        );
         $cntcachekey = 'scnt-'. serialize($search);
 
         $ids = $coursecatcache->get($cachekey);
@@ -1315,11 +1318,16 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
         $preloadcoursecontacts = !empty($options['coursecontacts']);
         unset($options['coursecontacts']);
 
-        if (!empty($search['search'])) {
+        // Empty search string will return all results.
+        if (!isset($search['search'])) {
+            $search['search'] = '';
+        }
+
+        if (empty($search['blocklist']) && empty($search['modulelist']) && empty($search['tagid'])) {
             // Search courses that have specified words in their names/summaries.
             $searchterms = preg_split('|\s+|', trim($search['search']), 0, PREG_SPLIT_NO_EMPTY);
-            $searchterms = array_filter($searchterms, create_function('$v', 'return strlen($v) > 1;'));
-            $courselist = get_courses_search($searchterms, 'c.sortorder ASC', 0, 9999999, $totalcount);
+
+            $courselist = get_courses_search($searchterms, 'c.sortorder ASC', 0, 9999999, $totalcount, $requiredcapabilities);
             self::sort_records($courselist, $sortfields);
             $coursecatcache->set($cachekey, array_keys($courselist));
             $coursecatcache->set($cntcachekey, $totalcount);
@@ -1365,6 +1373,15 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
                 return array();
             }
             $courselist = self::get_course_records($where, $params, $options, true);
+            if (!empty($requiredcapabilities)) {
+                foreach ($courselist as $key => $course) {
+                    context_helper::preload_from_record($course);
+                    $coursecontext = context_course::instance($course->id);
+                    if (!has_all_capabilities($requiredcapabilities, $coursecontext)) {
+                        unset($courselist[$key]);
+                    }
+                }
+            }
             self::sort_records($courselist, $sortfields);
             $coursecatcache->set($cachekey, array_keys($courselist));
             $coursecatcache->set($cntcachekey, count($courselist));
@@ -1397,11 +1414,12 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
      * @param array $search search criteria, see method search_courses() for more details
      * @param array $options display options. They do not affect the result but
      *     the 'sort' property is used in cache key for storing list of course ids
+     * @param array $requiredcapabilites List of capabilities required to see return course.
      * @return int
      */
-    public static function search_courses_count($search, $options = array()) {
+    public static function search_courses_count($search, $options = array(), $requiredcapabilities = array()) {
         $coursecatcache = cache::make('core', 'coursecat');
-        $cntcachekey = 'scnt-'. serialize($search);
+        $cntcachekey = 'scnt-'. serialize($search) . serialize($requiredcapabilities);
         if (($cnt = $coursecatcache->get($cntcachekey)) === false) {
             // Cached value not found. Retrieve ALL courses and return their count.
             unset($options['offset']);
@@ -1409,7 +1427,7 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
             unset($options['summary']);
             unset($options['coursecontacts']);
             $options['idonly'] = true;
-            $courses = self::search_courses($search, $options);
+            $courses = self::search_courses($search, $options, $requiredcapabilities);
             $cnt = count($courses);
         }
         return $cnt;
index 334253b..4301c53 100644 (file)
@@ -741,9 +741,11 @@ function get_courses_page($categoryid="all", $sort="c.sortorder ASC", $fields="c
  * @param int $page The page number to get
  * @param int $recordsperpage The number of records per page
  * @param int $totalcount Passed in by reference.
+ * @param array $requiredcapabilities Extra list of capabilities used to filter courses
  * @return object {@link $COURSE} records
  */
-function get_courses_search($searchterms, $sort, $page, $recordsperpage, &$totalcount) {
+function get_courses_search($searchterms, $sort, $page, $recordsperpage, &$totalcount,
+                            $requiredcapabilities = array()) {
     global $CFG, $DB;
 
     if ($DB->sql_regex_supported()) {
@@ -798,8 +800,7 @@ function get_courses_search($searchterms, $sort, $page, $recordsperpage, &$total
     }
 
     if (empty($searchcond)) {
-        $totalcount = 0;
-        return array();
+        $searchcond = array('1 = 1');
     }
 
     $searchcond = implode(" AND ", $searchcond);
@@ -823,11 +824,14 @@ function get_courses_search($searchterms, $sort, $page, $recordsperpage, &$total
 
     $rs = $DB->get_recordset_sql($sql, $params);
     foreach($rs as $course) {
-        if (!$course->visible) {
-            // preload contexts only for hidden courses or courses we need to return
-            context_helper::preload_from_record($course);
-            $coursecontext = context_course::instance($course->id);
-            if (!has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
+        // Preload contexts only for hidden courses or courses we need to return.
+        context_helper::preload_from_record($course);
+        $coursecontext = context_course::instance($course->id);
+        if (!$course->visible && !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
+            continue;
+        }
+        if (!empty($requiredcapabilities)) {
+            if (!has_all_capabilities($requiredcapabilities, $coursecontext)) {
                 continue;
             }
         }
index b29a3f8..ff92a46 100644 (file)
         <FIELD NAME="classpath" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false"/>
         <FIELD NAME="component" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="capabilities" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="all capabilities that are required to be run by the function (separated by comma)"/>
+        <FIELD NAME="services" TYPE="char" LENGTH="1333" NOTNULL="false" SEQUENCE="false" COMMENT="all the services (by shortname) where this function must be included"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
index 7d65f86..78c89f7 100644 (file)
@@ -669,6 +669,7 @@ $functions = array(
         'description' => 'Return course details',
         'type'        => 'read',
         'capabilities'=> 'moodle/course:view,moodle/course:update,moodle/course:viewhiddencourses',
+        'ajax'        => true,
     ),
 
     'core_course_search_courses' => array(
@@ -678,6 +679,7 @@ $functions = array(
         'description'   => 'Search courses by (name, module, block, tag)',
         'type'          => 'read',
         'capabilities'  => '',
+        'ajax'          => true,
     ),
 
     'moodle_course_create_courses' => array(
@@ -1213,29 +1215,9 @@ $services = array(
             'core_notes_delete_notes',
             'core_user_get_course_user_profiles',
             'core_message_send_instant_messages',
-            'mod_assign_get_grades',
-            'mod_assign_get_assignments',
-            'mod_assign_get_submissions',
-            'mod_assign_get_user_flags',
-            'mod_assign_set_user_flags',
-            'mod_assign_get_user_mappings',
-            'mod_assign_revert_submissions_to_draft',
-            'mod_assign_lock_submissions',
-            'mod_assign_unlock_submissions',
-            'mod_assign_save_submission',
-            'mod_assign_submit_for_grading',
-            'mod_assign_save_grade',
-            'mod_assign_save_user_extensions',
-            'mod_assign_reveal_identities',
             'message_airnotifier_is_system_configured',
             'message_airnotifier_are_notification_preferences_configured',
             'core_grades_update_grades',
-            'mod_forum_get_forums_by_courses',
-            'mod_forum_get_forum_discussions_paginated',
-            'mod_forum_get_forum_discussion_posts',
-            'mod_forum_add_discussion_post',
-            'mod_forum_add_discussion',
-            'mod_forum_can_add_discussion',
             'core_files_get_files',
             'core_message_get_messages',
             'core_message_create_contacts',
@@ -1253,9 +1235,7 @@ $services = array(
             'core_course_get_courses',
             'core_completion_update_activity_completion_status_manually',
             'core_completion_mark_course_self_completed',
-            'mod_data_get_databases_by_courses',
             'core_comment_get_comments',
-            'mod_forum_view_forum',
             'core_course_view_course',
             'core_course_search_courses',
             'core_course_get_course_module',
@@ -1267,69 +1247,11 @@ $services = array(
             'core_message_mark_message_read',
             'core_message_delete_message',
             'core_notes_view_notes',
-            'mod_forum_view_forum_discussion',
             'core_user_view_user_profile',
             'gradereport_user_view_grade_report',
             'core_rating_get_item_ratings',
-            'mod_url_view_url',
             'core_user_get_users_by_field',
             'core_user_add_user_private_files',
-            'mod_assign_view_grading_table',
-            'mod_assign_view_submission_status',
-            'mod_scorm_view_scorm',
-            'mod_scorm_get_scorm_scoes',
-            'mod_scorm_get_scorm_user_data',
-            'mod_scorm_insert_scorm_tracks',
-            'mod_scorm_get_scorm_sco_tracks',
-            'mod_scorm_get_scorm_attempt_count',
-            'mod_scorm_get_scorms_by_courses',
-            'mod_scorm_launch_sco',
-            'mod_survey_get_surveys_by_courses',
-            'mod_survey_view_survey',
-            'mod_survey_get_questions',
-            'mod_survey_submit_answers',
-            'mod_page_view_page',
-            'mod_resource_view_resource',
-            'mod_folder_view_folder',
-            'mod_chat_login_user',
-            'mod_chat_get_chat_users',
-            'mod_chat_send_chat_message',
-            'mod_chat_get_chat_latest_messages',
-            'mod_chat_view_chat',
-            'mod_chat_get_chats_by_courses',
-            'mod_book_view_book',
-            'mod_book_get_books_by_courses',
-            'mod_choice_get_choice_results',
-            'mod_choice_get_choice_options',
-            'mod_choice_submit_choice_response',
-            'mod_choice_view_choice',
-            'mod_choice_get_choices_by_courses',
-            'mod_choice_delete_choice_responses',
-            'mod_lti_get_tool_launch_data',
-            'mod_lti_get_ltis_by_courses',
-            'mod_lti_view_lti',
-            'mod_imscp_view_imscp',
-            'mod_imscp_get_imscps_by_courses',
-            'mod_quiz_get_quizzes_by_courses',
-            'mod_quiz_view_quiz',
-            'mod_glossary_get_glossaries_by_courses',
-            'mod_wiki_get_wikis_by_courses',
-            'mod_wiki_view_wiki',
-            'mod_wiki_view_page',
-            'mod_wiki_get_subwikis',
-            'mod_glossary_view_glossary',
-            'mod_glossary_view_entry',
-            'mod_glossary_get_entries_by_letter',
-            'mod_glossary_get_entries_by_date',
-            'mod_glossary_get_categories',
-            'mod_glossary_get_entries_by_category',
-            'mod_glossary_get_authors',
-            'mod_glossary_get_entries_by_author',
-            'mod_glossary_get_entries_by_author_id',
-            'mod_glossary_get_entries_by_search',
-            'mod_glossary_get_entries_by_term',
-            'mod_glossary_get_entries_to_approve',
-            'mod_glossary_get_entry_by_id',
             ),
         'enabled' => 0,
         'restrictedusers' => 0,
index 074b685..80bb379 100644 (file)
@@ -80,4 +80,8 @@ $tagareas = array(
         'itemtype' => 'blog_external', // External blogs.
         'component' => 'core',
     ),
+    array(
+        'itemtype' => 'course_modules', // Course modules.
+        'component' => 'core',
+    ),
 );
index 8abc3f6..1a56493 100644 (file)
@@ -285,7 +285,7 @@ $tasks = array(
         'month' => '*'
     ),
     array(
-        'classname' => 'core\task\search_task',
+        'classname' => 'core\task\search_index_task',
         'blocking' => 0,
         'minute' => '*/30',
         'hour' => '*',
@@ -293,6 +293,15 @@ $tasks = array(
         'dayofweek' => '*',
         'month' => '*'
     ),
+    array(
+        'classname' => 'core\task\search_optimize_task',
+        'blocking' => 0,
+        'minute' => '15',
+        'hour' => '*/12',
+        'day' => '*',
+        'dayofweek' => '*',
+        'month' => '*'
+    ),
     array(
         'classname' => 'core\task\stats_cron_task',
         'blocking' => 0,
index ea198d8..fc8ba43 100644 (file)
@@ -1451,5 +1451,18 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2016030103.00);
     }
 
+    if ($oldversion < 2016030400.01) {
+        // Add the new services field.
+        $table = new xmldb_table('external_functions');
+        $field = new xmldb_field('services', XMLDB_TYPE_CHAR, '1333', null, null, null, null, 'capabilities');
+
+        // Conditionally launch add field services.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2016030400.01);
+    }
+
     return true;
 }
diff --git a/lib/form/course.php b/lib/form/course.php
new file mode 100644 (file)
index 0000000..9121447
--- /dev/null
@@ -0,0 +1,142 @@
+<?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/>.
+
+
+/**
+ * Course selector field.
+ *
+ * Allows auto-complete ajax searching for courses and can restrict by enrolment, permissions, viewhidden...
+ *
+ * @package   core_form
+ * @copyright 2015 Damyon Wiese <damyon@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+global $CFG;
+require_once($CFG->libdir . '/form/autocomplete.php');
+
+/**
+ * Form field type for choosing a course.
+ *
+ * Allows auto-complete ajax searching for courses and can restrict by enrolment, permissions, viewhidden...
+ *
+ * @package   core_form
+ * @copyright 2015 Damyon Wiese <damyon@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class MoodleQuickForm_course extends MoodleQuickForm_autocomplete {
+
+    /**
+     * @var array $exclude Exclude a list of courses from the list (e.g. the current course).
+     */
+    protected $exclude = array();
+
+    /**
+     * @var boolean $allowmultiple Allow selecting more than one course.
+     */
+    protected $multiple = false;
+
+    /**
+     * @var array $requiredcapabilities Array of extra capabilities to check at the course context.
+     */
+    protected $requiredcapabilities = array();
+
+    /**
+     * Constructor
+     *
+     * @param string $elementname Element name
+     * @param mixed $elementlabel Label(s) for an element
+     * @param array $options Options to control the element's display
+     *                       Valid options are:
+     *                       'multiple' - boolean multi select
+     *                       'exclude' - array or int, list of course ids to never show
+     *                       'requiredcapabilities' - array of capabilities. Uses ANY to combine them.
+     */
+    public function __construct($elementname = null, $elementlabel = null, $options = array()) {
+        if (isset($options['multiple'])) {
+            $this->multiple = $options['multiple'];
+        }
+        if (isset($options['exclude'])) {
+            $this->exclude = $options['exclude'];
+            if (!is_array($this->exclude)) {
+                $this->exclude = array($this->exclude);
+            }
+        }
+        if (isset($options['requiredcapabilities'])) {
+            $this->requiredcapabilities = $options['requiredcapabilities'];
+        }
+
+        $validattributes = array(
+            'ajax' => 'core/form-course-selector',
+            'data-requiredcapabilities' => implode(',', $this->requiredcapabilities),
+            'data-exclude' => implode(',', $this->exclude)
+        );
+        if ($this->multiple) {
+            $validattributes['multiple'] = 'multiple';
+        }
+
+        parent::__construct($elementname, $elementlabel, array(), $validattributes);
+    }
+
+    /**
+     * Set the value of this element. If values can be added or are unknown, we will
+     * make sure they exist in the options array.
+     * @param string|array $value The value to set.
+     * @return boolean
+     */
+    public function setValue($value) {
+        global $DB;
+        $values = (array) $value;
+        $coursestofetch = array();
+
+        foreach ($values as $onevalue) {
+            if ((!$this->optionExists($onevalue)) &&
+                    ($onevalue !== '_qf__force_multiselect_submission')) {
+                array_push($coursestofetch, $onevalue);
+            }
+        }
+
+        if (empty($coursestofetch)) {
+            return $this->setSelected(array());
+        }
+
+        // There is no API function to load a list of course from a list of ids.
+        $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
+        $fields = array('c.id', 'c.category', 'c.sortorder',
+                        'c.shortname', 'c.fullname', 'c.idnumber',
+                        'c.startdate', 'c.visible', 'c.cacherev');
+        list($whereclause, $params) = $DB->get_in_or_equal($coursestofetch, SQL_PARAMS_NAMED, 'id');
+
+        $sql = "SELECT ". join(',', $fields). ", $ctxselect
+                FROM {course} c
+                JOIN {context} ctx ON c.id = ctx.instanceid AND ctx.contextlevel = :contextcourse
+                WHERE c.id ". $whereclause." ORDER BY c.sortorder";
+        $list = $DB->get_records_sql($sql, array('contextcourse' => CONTEXT_COURSE) + $params);
+
+        $coursestoselect = array();
+        foreach ($list as $course) {
+            context_helper::preload_from_record($course);
+            // Make sure we can see the course.
+            if (!$course->visible && !has_capability('moodle/course:viewhiddencourses', context_course::instance($course->id))) {
+                continue;
+            }
+            $label = get_course_display_name_for_list($course);
+            $this->addOption($label, $course->id);
+            array_push($coursestoselect, $course->id);
+        }
+        return $this->setSelected($coursestoselect);
+    }
+}
index b1c5eb6..e6cdc73 100644 (file)
@@ -364,7 +364,9 @@ class MoodleQuickForm_modgrade extends MoodleQuickForm_group {
                         return true;
                     }
                     // Check if we are changing the scale type when grades are present.
-                    if (isset($val['modgrade_type']) && $val['modgrade_type'] === 'scale') {
+                    // If modgrade_type is empty then use currentgradetype.
+                    $gradetype = isset($val['modgrade_type']) ? $val['modgrade_type'] : $this->currentgradetype;
+                    if ($gradetype === 'scale') {
                         if (isset($val['modgrade_scale']) && ($val['modgrade_scale'] !== $this->currentscaleid)) {
                             return false;
                         }
@@ -381,7 +383,9 @@ class MoodleQuickForm_modgrade extends MoodleQuickForm_group {
                         return true;
                     }
                     // Check if we are changing the max grade if we are using ratings and there is a grade.
-                    if (isset($val['modgrade_type']) && $val['modgrade_type'] === 'point') {
+                    // If modgrade_type is empty then use currentgradetype.
+                    $gradetype = isset($val['modgrade_type']) ? $val['modgrade_type'] : $this->currentgradetype;
+                    if ($gradetype === 'point') {
                         if (isset($val['modgrade_point']) &&
                             grade_floats_different($this->currentgrade, $val['modgrade_point'])) {
                             return false;
@@ -391,21 +395,23 @@ class MoodleQuickForm_modgrade extends MoodleQuickForm_group {
                 };
                 $checkmaxgrade = function($val) {
                     // Closure to validate a max points value. See the note above about scope if this confuses you.
-                    if (isset($val['modgrade_type']) && $val['modgrade_type'] === 'point') {
-                        if (!isset($val['modgrade_point'])) {
-                            return false;
+                    // If modgrade_type is empty then use currentgradetype.
+                    $gradetype = isset($val['modgrade_type']) ? $val['modgrade_type'] : $this->currentgradetype;
+                    if ($gradetype === 'point') {
+                        if (isset($val['modgrade_point'])) {
+                            return $this->validate_point($val['modgrade_point']);
                         }
-                        return $this->validate_point($val['modgrade_point']);
                     }
                     return true;
                 };
                 $checkvalidscale = function($val) {
                     // Closure to validate a scale value. See the note above about scope if this confuses you.
-                    if (isset($val['modgrade_type']) && $val['modgrade_type'] === 'scale') {
-                        if (!isset($val['modgrade_scale'])) {
-                            return false;
+                    // If modgrade_type is empty then use currentgradetype.
+                    $gradetype = isset($val['modgrade_type']) ? $val['modgrade_type'] : $this->currentgradetype;
+                    if ($gradetype === 'scale') {
+                        if (isset($val['modgrade_scale'])) {
+                            return $this->validate_scale($val['modgrade_scale']);
                         }
-                        return $this->validate_scale($val['modgrade_scale']);
                     }
                     return true;
                 };
@@ -416,7 +422,9 @@ class MoodleQuickForm_modgrade extends MoodleQuickForm_group {
                         return true;
                     }
                     // Closure to validate a scale value. See the note above about scope if this confuses you.
-                    if (isset($val['modgrade_type']) && $val['modgrade_type'] === 'point') {
+                    // If modgrade_type is empty then use currentgradetype.
+                    $gradetype = isset($val['modgrade_type']) ? $val['modgrade_type'] : $this->currentgradetype;
+                    if ($gradetype === 'point' && isset($val['modgrade_point'])) {
                         // Work out if the value was actually changed in the form.
                         if (grade_floats_different($this->currentgrade, $val['modgrade_point'])) {
                             if (empty($val['modgrade_rescalegrades'])) {
index e0639a2..6307a4c 100644 (file)
@@ -2995,6 +2995,7 @@ MoodleQuickForm::registerElementType('advcheckbox', "$CFG->libdir/form/advcheckb
 MoodleQuickForm::registerElementType('autocomplete', "$CFG->libdir/form/autocomplete.php", 'MoodleQuickForm_autocomplete');
 MoodleQuickForm::registerElementType('button', "$CFG->libdir/form/button.php", 'MoodleQuickForm_button');
 MoodleQuickForm::registerElementType('cancel', "$CFG->libdir/form/cancel.php", 'MoodleQuickForm_cancel');
+MoodleQuickForm::registerElementType('course', "$CFG->libdir/form/course.php", 'MoodleQuickForm_course');
 MoodleQuickForm::registerElementType('searchableselector', "$CFG->libdir/form/searchableselector.php", 'MoodleQuickForm_searchableselector');
 MoodleQuickForm::registerElementType('checkbox', "$CFG->libdir/form/checkbox.php", 'MoodleQuickForm_checkbox');
 MoodleQuickForm::registerElementType('date_selector', "$CFG->libdir/form/dateselector.php", 'MoodleQuickForm_date_selector');
index 50daf71..40df5cc 100644 (file)
@@ -368,6 +368,20 @@ class grade_item extends grade_object {
         return $count > 0;
     }
 
+    /**
+     * Check to see if there are existing overridden grades for this grade_item.
+     *
+     * @return boolean - true if there are overridden grades for this grade_item.
+     */
+    public function has_overridden_grades() {
+        global $DB;
+
+        $count = $DB->count_records_select('grade_grades',
+                                           'itemid = :gradeitemid AND finalgrade IS NOT NULL AND overridden > 0',
+                                           array('gradeitemid' => $this->id));
+        return $count > 0;
+    }
+
     /**
      * Finds and returns all grade_item instances based on params.
      *
@@ -872,13 +886,15 @@ class grade_item extends grade_object {
             // Set this object in the item so it doesn't re-fetch it.
             $grade->grade_item = $this;
 
-            // Updating the raw grade automatically updates the min/max.
-            if ($this->is_raw_used()) {
-                $rawgrade = (($grade->rawgrade - $oldgrademin) * $scale) + $newgrademin;
-                $this->update_raw_grade(false, $rawgrade, $source, false, FORMAT_MOODLE, null, null, null, $grade);
-            } else {
-                $finalgrade = (($grade->finalgrade - $oldgrademin) * $scale) + $newgrademin;
-                $this->update_final_grade($grade->userid, $finalgrade, $source);
+            if (!$this->is_category_item() || ($this->is_category_item() && $grade->is_overridden())) {
+                // Updating the raw grade automatically updates the min/max.
+                if ($this->is_raw_used()) {
+                    $rawgrade = (($grade->rawgrade - $oldgrademin) * $scale) + $newgrademin;
+                    $this->update_raw_grade(false, $rawgrade, $source, false, FORMAT_MOODLE, null, null, null, $grade);
+                } else {
+                    $finalgrade = (($grade->finalgrade - $oldgrademin) * $scale) + $newgrademin;
+                    $this->update_final_grade($grade->userid, $finalgrade, $source);
+                }
             }
         }
         $rs->close();
index b949b74..362ddd5 100644 (file)
@@ -119,3 +119,69 @@ class HTMLPurifier_URIScheme_teamspeak extends HTMLPurifier_URIScheme {
     }
 
 }
+
+/**
+ * A custom HTMLPurifier transformation. Adds rel="noreferrer" to all links with target="_blank".
+ *
+ * @package core
+ * @copyright Cameron Ball
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class HTMLPurifier_AttrTransform_Noreferrer extends HTMLPurifier_AttrTransform {
+    /** @var HTMLPurifier_URIParser $parser */
+    private $parser;
+
+    /**
+     * Constructor.
+     */
+    public function __construct() {
+        $this->parser = new HTMLPurifier_URIParser();
+    }
+
+    /**
+     * Transforms a tags such that when a target attribute is present, rel="noreferrer" is added.
+     *
+     * Note that this will not respect Attr.AllowedRel
+     *
+     * @param array $attr Assoc array of attributes, usually from
+     *              HTMLPurifier_Token_Tag::$attr
+     * @param HTMLPurifier_Config $config Mandatory HTMLPurifier_Config object.
+     * @param HTMLPurifier_Context $context Mandatory HTMLPurifier_Context object
+     * @return array Processed attribute array.
+     */
+    public function transform($attr, $config, $context) {
+        // Nothing to do If we already have noreferrer in the rel attribute
+        if (!empty($attr['rel']) && substr($attr['rel'], 'noreferrer') !== false) {
+            return $attr;
+        }
+
+        // If _blank target attribute exists, add rel=noreferrer
+        if (!empty($attr['target']) && $attr['target'] == '_blank') {
+            $attr['rel'] = !empty($attr['rel']) ? $attr['rel'] . ' noreferrer' : 'noreferrer';
+        }
+
+        return $attr;
+    }
+}
+
+/**
+ * A custom HTMLPurifier module to add rel="noreferrer" attributes a tags.
+ *
+ * @package    core
+ * @copyright  Cameron Ball
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class HTMLPurifier_HTMLModule_Noreferrer extends HTMLPurifier_HTMLModule {
+    /** @var string $name */
+    public $name = 'Noreferrer';
+
+    /**
+     * Module setup
+     *
+     * @param HTMLPurifier_Config $config
+     */
+    public function setup($config) {
+        $a = $this->addBlankElement('a');
+        $a->attr_transform_post[] = new HTMLPurifier_AttrTransform_Noreferrer();
+    }
+}
index 5cbe558..60704c8 100644 (file)
@@ -216,6 +216,7 @@ class phpunit_util extends testing_util {
         filter_manager::reset_caches();
         core_filetypes::reset_caches();
         \core_search\manager::clear_static();
+        core_user::reset_caches();
 
         // Reset static unit test options.
         if (class_exists('\availability_date\condition', false)) {
index e686f6a..81e68a5 100644 (file)
@@ -248,6 +248,10 @@ abstract class testing_module_generator extends component_generator_base {
             $record->introformat = FORMAT_MOODLE;
         }
 
+        if (isset($record->tags) && !is_array($record->tags)) {
+            $record->tags = preg_split('/\s*,\s*/', trim($record->tags), -1, PREG_SPLIT_NO_EMPTY);
+        }
+
         // Before Moodle 2.6 it was possible to create a module with completion tracking when
         // it is not setup for course and/or site-wide. Display debugging message so it is
         // easier to trace an error in unittests.
index 42e07fd..0c55fab 100644 (file)
@@ -199,6 +199,10 @@ class core_test_generator_testcase extends advanced_testcase {
         $cm = get_coursemodule_from_instance('page', $page->id, $SITE->id, true);
         $this->assertEquals(3, $cm->sectionnum);
 
+        $page = $generator->create_module('page', array('course' => $SITE->id, 'tags' => 'Cat, Dog'));
+        $this->assertEquals(array('Cat', 'Dog'),
+            array_values(core_tag_tag::get_item_tags_array('core', 'course_modules', $page->cmid)));
+
         // Prepare environment to generate modules with all possible options.
 
         // Enable advanced functionality.
index d930044..2c855dc 100644 (file)
@@ -511,6 +511,16 @@ class core_coursecatlib_testcase extends advanced_testcase {
         $res = coursecat::search_courses(array('search' => 'Математика'));
         $this->assertEquals(array($c3->id, $c6->id), array_keys($res));
         $this->assertEquals(2, coursecat::search_courses_count(array('search' => 'Математика'), array()));
+
+        $this->setUser($this->getDataGenerator()->create_user());
+
+        // Add necessary capabilities.
+        $this->assign_capability('moodle/course:create', CAP_ALLOW, context_coursecat::instance($cat2->id));
+        // Do another search with restricted capabilities.
+        $reqcaps = array('moodle/course:create');
+        $res = coursecat::search_courses(array('search' => 'test'), array(), $reqcaps);
+        $this->assertEquals(array($c8->id, $c5->id), array_keys($res));
+        $this->assertEquals(2, coursecat::search_courses_count(array('search' => 'test'), array(), $reqcaps));
     }
 
     public function test_course_contacts() {
index 51d22c8..ae22dae 100644 (file)
@@ -40,9 +40,13 @@ class core_htmlpurifier_testcase extends basic_testcase {
      * Verify _blank target is allowed.
      */
     public function test_allow_blank_target() {
+        // See MDL-52651 for an explanation as to why the rel="noreferrer" attribute is expected here.
+        // Also note we do not need to test links with an existing rel attribute as the HTML Purifier is configured to remove
+        // the rel attribute.
         $text = '<a href="http://moodle.org" target="_blank">Some link</a>';
+        $expected = '<a href="http://moodle.org" target="_blank" rel="noreferrer">Some link</a>';
         $result = format_text($text, FORMAT_HTML);
-        $this->assertSame($text, $result);
+        $this->assertSame($expected, $result);
 
         $result = format_text('<a href="http://moodle.org" target="some">Some link</a>', FORMAT_HTML);
         $this->assertSame('<a href="http://moodle.org">Some link</a>', $result);
index 8aeac33..53e9e57 100644 (file)
@@ -179,6 +179,56 @@ class core_text_testcase extends advanced_testcase {
         $this->assertSame(4, core_text::strlen($str, 'GB18030'));
     }
 
+    /**
+     * Test unicode safe string truncation.
+     */
+    public function test_str_max_bytes() {
+        // These are all 3 byte characters, so this is a 12-byte string.
+        $str = '言語設定';
+
+        $this->assertEquals(12, strlen($str));
+
+        // Step back, shortening the string 1 byte at a time. Should remove in 1 char chunks.
+        $conv = core_text::str_max_bytes($str, 12);
+        $this->assertEquals(12, strlen($conv));
+        $this->assertSame('言語設定', $conv);
+        $conv = core_text::str_max_bytes($str, 11);
+        $this->assertEquals(9, strlen($conv));
+        $this->assertSame('言語設', $conv);
+        $conv = core_text::str_max_bytes($str, 10);
+        $this->assertEquals(9, strlen($conv));
+        $this->assertSame('言語設', $conv);
+        $conv = core_text::str_max_bytes($str, 9);
+        $this->assertEquals(9, strlen($conv));
+        $this->assertSame('言語設', $conv);
+        $conv = core_text::str_max_bytes($str, 8);
+        $this->assertEquals(6, strlen($conv));
+        $this->assertSame('言語', $conv);
+
+        // Now try a mixed byte string.
+        $str = '言語設a定';
+
+        $this->assertEquals(13, strlen($str));
+
+        $conv = core_text::str_max_bytes($str, 11);
+        $this->assertEquals(10, strlen($conv));
+        $this->assertSame('言語設a', $conv);
+        $conv = core_text::str_max_bytes($str, 10);
+        $this->assertEquals(10, strlen($conv));
+        $this->assertSame('言語設a', $conv);
+        $conv = core_text::str_max_bytes($str, 9);
+        $this->assertEquals(9, strlen($conv));
+        $this->assertSame('言語設', $conv);
+        $conv = core_text::str_max_bytes($str, 8);
+        $this->assertEquals(6, strlen($conv));
+        $this->assertSame('言語', $conv);
+
+        // Test 0 byte case.
+        $conv = core_text::str_max_bytes($str, 0);
+        $this->assertEquals(0, strlen($conv));
+        $this->assertSame('', $conv);
+    }
+
     /**
      * Tests the static strtolower method.
      */
index 4f0f268..c6ddbac 100644 (file)
@@ -184,4 +184,29 @@ class core_user_testcase extends advanced_testcase {
         }
 
     }
+
+    /**
+     * Test get_property_definition() method.
+     */
+    public function test_get_property_definition() {
+        // Try to get a existing property.
+        $properties = core_user::get_property_definition('id');
+        $this->assertEquals($properties['type'], PARAM_INT);
+        $properties = core_user::get_property_definition('username');
+        $this->assertEquals($properties['type'], PARAM_USERNAME);
+
+        // Invalid property.
+        try {
+            core_user::get_property_definition('fullname');
+        } catch (coding_exception $e) {
+            $this->assertRegExp('/Invalid property requested./', $e->getMessage());
+        }
+
+        // Empty parameter.
+        try {
+            core_user::get_property_definition('');
+        } catch (coding_exception $e) {
+            $this->assertRegExp('/Invalid property requested./', $e->getMessage());
+        }
+    }
 }
index d23f702..72e47f2 100644 (file)
@@ -3,6 +3,16 @@ information provided here is intended especially for developers.
 
 === 3.1 ===
 
+* The core_user::fill_properties_cache() static method has been introduced to be a reference
+  and allow standard user fields data validation. Right now only type validation is supported
+  checking it against the parameter (PARAM_*) type of the target user field. MDL-52781 is
+  going to add support to null/not null and choices validation, replacing the existing code to
+  validate the user fields in different places in a common way.
+* Webservice function core_course_search_courses now returns results when the search string
+  is less than 2 chars long.
+* Webservice function core_course_search_courses accepts a new parameter 'requiredcapabilities' to filter the results
+  by the capabilities of the current user.
+* New mform element 'course' handles thousands of courses with good performance and usability.
 * The redirect() function will now redirect immediately if output has not
   already started. Messages will be displayed on the subsequent page using
   session notifications. The type of message output can be configured using the
@@ -70,6 +80,7 @@ information provided here is intended especially for developers.
   is now a part of \antivirus_clamav\scanner class methods.
 * \repository::antivir_scan_file() has been deprecated, \core\antivirus\manager::scan_file() that
   applies antivirus plugins is replacing its functionality.
+* Added core_text::str_max_bytes() which safely truncates multi-byte strings to a maximum number of bytes.
 
 === 3.0 ===
 
index 4f96d38..b3c92a2 100644 (file)
@@ -1080,6 +1080,28 @@ function external_update_descriptions($component) {
             $dbfunction->capabilities = $functioncapabilities;
             $update = true;
         }
+
+        if (isset($function['services']) and is_array($function['services'])) {
+            sort($function['services']);
+            $functionservices = implode(',', $function['services']);
+        } else {
+            // Force null values in the DB.
+            $functionservices = null;
+        }
+
+        if ($dbfunction->services != $functionservices) {
+            // Now, we need to check if services were removed, in that case we need to remove the function from them.
+            $servicesremoved = array_diff(explode(",", $dbfunction->services), explode(",", $functionservices));
+            foreach ($servicesremoved as $removedshortname) {
+                if ($externalserviceid = $DB->get_field('external_services', 'id', array("shortname" => $removedshortname))) {
+                    $DB->delete_records('external_services_functions', array('functionname' => $dbfunction->name,
+                                                                                'externalserviceid' => $externalserviceid));
+                }
+            }
+
+            $dbfunction->services = $functionservices;
+            $update = true;
+        }
         if ($update) {
             $DB->update_record('external_functions', $dbfunction);
         }
@@ -1092,6 +1114,15 @@ function external_update_descriptions($component) {
         $dbfunction->classpath  = empty($function['classpath']) ? null : $function['classpath'];
         $dbfunction->component  = $component;
         $dbfunction->capabilities = array_key_exists('capabilities', $function)?$function['capabilities']:'';
+
+        if (isset($function['services']) and is_array($function['services'])) {
+            sort($function['services']);
+            $dbfunction->services = implode(',', $function['services']);
+        } else {
+            // Force null values in the DB.
+            $dbfunction->services = null;
+        }
+
         $dbfunction->id = $DB->insert_record('external_functions', $dbfunction);
     }
     unset($functions);
@@ -1200,6 +1231,52 @@ function external_update_descriptions($component) {
     }
 }
 
+/**
+ * Allow plugins to add external functions to other plugins or core services.
+ * This function is executed just after all the plugins have been updated.
+ */
+function external_update_services() {
+    global $DB;
+
+    // Look for external functions that want to be added in existing services.
+    $functions = $DB->get_records_select('external_functions', 'services IS NOT NULL');
+
+    $servicescache = array();
+    foreach ($functions as $function) {
+        // Prevent edge cases.
+        if (empty($function->services)) {
+            continue;
+        }
+        $services = explode(',', $function->services);
+
+        foreach ($services as $serviceshortname) {
+            // Get the service id by shortname.
+            if (!empty($servicescache[$serviceshortname])) {
+                $serviceid = $servicescache[$serviceshortname];
+            } else if ($service = $DB->get_record('external_services', array('shortname' => $serviceshortname))) {
+                // If the component is empty, it means that is not a built-in service.
+                // We don't allow functions to inject themselves in services created by an user in Moodle.
+                if (empty($service->component)) {
+                    continue;
+                }
+                $serviceid = $service->id;
+                $servicescache[$serviceshortname] = $serviceid;
+            } else {
+                // Service not found.
+                continue;
+            }
+            // Finally add the function to the service.
+            $newf = new stdClass();
+            $newf->externalserviceid = $serviceid;
+            $newf->functionname      = $function->name;
+
+            if (!$DB->record_exists('external_services_functions', (array)$newf)) {
+                $DB->insert_record('external_services_functions', $newf);
+            }
+        }
+    }
+}
+
 /**
  * upgrade logging functions
  */
@@ -1657,6 +1734,9 @@ function upgrade_noncore($verbose) {
         foreach ($plugintypes as $type=>$location) {
             upgrade_plugins($type, 'print_upgrade_part_start', 'print_upgrade_part_end', $verbose);
         }
+        // Upgrade services. This function gives plugins a chance to add functions to existing core or non-core services.
+        external_update_services();
+
         // Update cache definitions. Involves scanning each plugin for any changes.
         cache_helper::update_definitions();
         // Mark the site as upgraded.
index 18ce243..b2e7eeb 100644 (file)
@@ -1754,7 +1754,7 @@ function purify_html($text, $options = array()) {
         $config = HTMLPurifier_Config::createDefault();
 
         $config->set('HTML.DefinitionID', 'moodlehtml');
-        $config->set('HTML.DefinitionRev', 3);
+        $config->set('HTML.DefinitionRev', 4);
         $config->set('Cache.SerializerPath', $cachedir);
         $config->set('Cache.SerializerPermissions', $CFG->directorypermissions);
         $config->set('Core.NormalizeNewlines', false);
@@ -1796,6 +1796,9 @@ function purify_html($text, $options = array()) {
 
             // Use the built-in Ruby module to add annotation support.
             $def->manager->addModule(new HTMLPurifier_HTMLModule_Ruby());
+
+            // Use the custom Noreferrer module.
+            $def->manager->addModule(new HTMLPurifier_HTMLModule_Noreferrer());
         }
 
         $purifier = new HTMLPurifier($config);
index 407ba79..b223b73 100644 (file)
@@ -37,6 +37,7 @@ redirect_if_major_upgrade_required();
 
 $testsession = optional_param('testsession', 0, PARAM_INT); // test session works properly
 $cancel      = optional_param('cancel', 0, PARAM_BOOL);      // redirect to frontpage, needed for loginhttps
+$anchor      = optional_param('anchor', '', PARAM_RAW);      // Used to restore hash anchor to wantsurl.
 
 if ($cancel) {
     redirect(new moodle_url('/'));
@@ -120,6 +121,15 @@ if ($user !== false or $frm !== false or $errormsg !== '') {
     $frm = data_submitted();
 }
 
+// Restore the #anchor to the original wantsurl. Note that this
+// will only work for internal auth plugins, SSO plugins such as
+// SAML / CAS / OIDC will have to handle this correctly directly.
+if ($anchor && isset($SESSION->wantsurl) && strpos($SESSION->wantsurl, '#') === false) {
+    $wantsurl = new moodle_url($SESSION->wantsurl);
+    $wantsurl->set_anchor(substr($anchor, 1));
+    $SESSION->wantsurl = $wantsurl->out();
+}
+
 /// Check if the user has actually submitted login data to us
 
 if ($frm and isset($frm->username)) {                             // Login WITH cookies
index 7f212c3..3b8ae58 100644 (file)
@@ -53,6 +53,8 @@ if (empty($CFG->authloginviaemail)) {
               </div>
               <?php } ?>
           <div class="clearer"><!-- --></div>
+          <input id="anchor" type="hidden" name="anchor" value="" />
+          <script>document.getElementById('anchor').value = location.hash</script>
           <input type="submit" id="loginbtn" value="<?php print_string("login") ?>" />
           <div class="forgetpass"><a href="forgot_password.php"><?php print_string("forgotten") ?></a></div>
         </form>
index 9c6ab63..0aaceb6 100644 (file)
 require_once(dirname(__FILE__) . '/../../config.php');
 require_once($CFG->dirroot.'/mod/assign/adminlib.php');
 
+$subtype = required_param('subtype', PARAM_PLUGIN);
+$action = optional_param('action', null, PARAM_PLUGIN);
+$plugin = optional_param('plugin', null, PARAM_PLUGIN);
+
+if (!empty($plugin)) {
+    require_sesskey();
+}
+
 // Create the class for this controller.
-$pluginmanager = new assign_plugin_manager(required_param('subtype', PARAM_PLUGIN));
+$pluginmanager = new assign_plugin_manager($subtype);
 
 $PAGE->set_context(context_system::instance());
 
 // Execute the controller.
-$pluginmanager->execute(optional_param('action', null, PARAM_PLUGIN),
-                        optional_param('plugin', null, PARAM_PLUGIN));
+$pluginmanager->execute($action, $plugin);
index 64dd216..cadf5f8 100644 (file)
@@ -30,7 +30,8 @@ $functions = array(
                 'methodname'  => 'get_grades',
                 'classpath'   => 'mod/assign/externallib.php',
                 'description' => 'Returns grades from the assignment',
-                'type'        => 'read'
+                'type'        => 'read',
+                'services'    => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
         ),
 
         'mod_assign_get_assignments' => array(
@@ -38,7 +39,8 @@ $functions = array(
                 'methodname'  => 'get_assignments',
                 'classpath'   => 'mod/assign/externallib.php',
                 'description' => 'Returns the courses and assignments for the users capability',
-                'type'        => 'read'
+                'type'        => 'read',
+                'services'    => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
         ),
 
         'mod_assign_get_submissions' => array(
@@ -46,7 +48,8 @@ $functions = array(
                 'methodname' => 'get_submissions',
                 'classpath' => 'mod/assign/externallib.php',
                 'description' => 'Returns the submissions for assignments',
-                'type' => 'read'
+                'type' => 'read',
+                'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
         ),
 
         'mod_assign_get_user_flags' => array(
@@ -54,7 +57,8 @@ $functions = array(
                 'methodname' => 'get_user_flags',
                 'classpath' => 'mod/assign/externallib.php',
                 'description' => 'Returns the user flags for assignments',
-                'type' => 'read'
+                'type' => 'read',
+                'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
         ),
 
         'mod_assign_set_user_flags' => array(
@@ -63,7 +67,8 @@ $functions = array(
                 'classpath'   => 'mod/assign/externallib.php',
                 'description' => 'Creates or updates user flags',
                 'type'        => 'write',
-                'capabilities'=> 'mod/assign:grade'
+                'capabilities'=> 'mod/assign:grade',
+                'services'    => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
         ),
 
         'mod_assign_get_user_mappings' => array(
@@ -71,7 +76,8 @@ $functions = array(
                 'methodname' => 'get_user_mappings',
                 'classpath' => 'mod/assign/externallib.php',
                 'description' => 'Returns the blind marking mappings for assignments',
-                'type' => 'read'
+                'type' => 'read',
+                'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
         ),
 
         'mod_assign_revert_submissions_to_draft' => array(
@@ -79,7 +85,8 @@ $functions = array(
                 'methodname' => 'revert_submissions_to_draft',
                 'classpath' => 'mod/assign/externallib.php',
                 'description' => 'Reverts the list of submissions to draft status',
-                'type' => 'write'
+                'type' => 'write',
+                'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
         ),
 
         'mod_assign_lock_submissions' => array(
@@ -87,7 +94,8 @@ $functions = array(
                 'methodname' => 'lock_submissions',
                 'classpath' => 'mod/assign/externallib.php',
                 'description' => 'Prevent students from making changes to a list of submissions',
-                'type' => 'write'
+                'type' => 'write',
+                'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
         ),
 
         'mod_assign_unlock_submissions' => array(
@@ -95,7 +103,8 @@ $functions = array(
                 'methodname' => 'unlock_submissions',
                 'classpath' => 'mod/assign/externallib.php',
                 'description' => 'Allow students to make changes to a list of submissions',
-                'type' => 'write'
+                'type' => 'write',
+                'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
         ),
 
         'mod_assign_save_submission' => array(
@@ -103,7 +112,8 @@ $functions = array(
                 'methodname' => 'save_submission',
                 'classpath' => 'mod/assign/externallib.php',
                 'description' => 'Update the current students submission',
-                'type' => 'write'
+                'type' => 'write',
+                'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
         ),
 
         'mod_assign_submit_for_grading' => array(
@@ -111,7 +121,8 @@ $functions = array(
                 'methodname' => 'submit_for_grading',
                 'classpath' => 'mod/assign/externallib.php',
                 'description' => 'Submit the current students assignment for grading',
-                'type' => 'write'
+                'type' => 'write',
+                'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
         ),
 
         'mod_assign_save_grade' => array(
@@ -119,7 +130,8 @@ $functions = array(
                 'methodname' => 'save_grade',
                 'classpath' => 'mod/assign/externallib.php',
                 'description' => 'Save a grade update for a single student.',
-                'type' => 'write'
+                'type' => 'write',
+                'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
         ),
 
         'mod_assign_save_grades' => array(
@@ -127,7 +139,8 @@ $functions = array(
                 'methodname' => 'save_grades',
                 'classpath' => 'mod/assign/externallib.php',
                 'description' => 'Save multiple grade updates for an assignment.',
-                'type' => 'write'
+                'type' => 'write',
+                'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
         ),
 
         'mod_assign_save_user_extensions' => array(
@@ -135,7 +148,8 @@ $functions = array(
                 'methodname' => 'save_user_extensions',
                 'classpath' => 'mod/assign/externallib.php',
                 'description' => 'Save a list of assignment extensions',
-                'type' => 'write'
+                'type' => 'write',
+                'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
         ),
 
         'mod_assign_reveal_identities' => array(
@@ -143,7 +157,8 @@ $functions = array(
                 'methodname' => 'reveal_identities',
                 'classpath' => 'mod/assign/externallib.php',
                 'description' => 'Reveal the identities for a blind marking assignment',
-                'type' => 'write'
+                'type' => 'write',
+                'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
         ),
 
         'mod_assign_view_grading_table' => array(
@@ -152,7 +167,8 @@ $functions = array(
                 'classpath'     => 'mod/assign/externallib.php',
                 'description'   => 'Trigger the grading_table_viewed event.',
                 'type'          => 'write',
-                'capabilities'  => 'mod/assign:view, mod/assign:viewgrades'
+                'capabilities'  => 'mod/assign:view, mod/assign:viewgrades',
+                'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
         ),
 
         'mod_assign_view_submission_status' => array(
@@ -161,6 +177,7 @@ $functions = array(
             'classpath'     => 'mod/assign/externallib.php',
             'description'   => 'Trigger the submission status viewed event.',
             'type'          => 'write',
-            'capabilities'  => 'mod/assign:view'
+            'capabilities'  => 'mod/assign:view',
+            'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
         ),
 );
index 2c65d15..5c7d862 100644 (file)
@@ -1681,9 +1681,12 @@ class mod_assign_external extends external_api {
 
         $notices = array();
 
-        $submissiondata = (object)$params['plugindata'];
-
-        $assignment->save_submission($submissiondata, $notices);
+        if (!$assignment->submissions_open($USER->id)) {
+            $notices[] = get_string('duedatereached', 'assign');
+        } else {
+            $submissiondata = (object)$params['plugindata'];
+            $assignment->save_submission($submissiondata, $notices);
+        }
 
         $warnings = array();
         foreach ($notices as $notice) {
index 01c5d4d..63e9c36 100644 (file)
@@ -946,6 +946,16 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
 
         $this->assertEquals(0, count($result));
 
+        // Set up a due and cutoff passed date.
+        $instance->duedate = time() - WEEKSECS;
+        $instance->cutoffdate = time() - WEEKSECS;
+        $DB->update_record('assign', $instance);
+
+        $result = mod_assign_external::save_submission($instance->id, $submissionpluginparams);
+        $result = external_api::clean_returnvalue(mod_assign_external::save_submission_returns(), $result);
+
+        $this->assertCount(1, $result);
+        $this->assertEquals(get_string('duedatereached', 'assign'), $result[0]['item']);
     }
 
     /**
index 7e1c4b0..210e4bc 100644 (file)
@@ -25,6 +25,6 @@
 defined('MOODLE_INTERNAL') || die();
 
 $plugin->component = 'mod_assign'; // Full name of the plugin (used for diagnostics).
-$plugin->version  = 2015111601;    // The current module version (Date: YYYYMMDDXX).
+$plugin->version  = 2015111602;    // The current module version (Date: YYYYMMDDXX).
 $plugin->requires = 2015111000;    // Requires this Moodle version.
 $plugin->cron     = 60;
index 0252270..97b8ce4 100644 (file)
@@ -33,7 +33,8 @@ $functions = array(
         'methodname'    => 'view_book',
         'description'   => 'Simulate the view.php web interface book: trigger events, completion, etc...',
         'type'          => 'write',
-        'capabilities'  => 'mod/book:read'
+        'capabilities'  => 'mod/book:read',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_book_get_books_by_courses' => array(
@@ -42,6 +43,7 @@ $functions = array(
         'description'   => 'Returns a list of book instances in a provided set of courses,
                             if no courses are provided then all the book instances the user has access to will be returned.',
         'type'          => 'read',
-        'capabilities'  => ''
+        'capabilities'  => '',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     )
 );
index 08c40c4..6260531 100644 (file)
@@ -25,6 +25,6 @@
 defined('MOODLE_INTERNAL') || die;
 
 $plugin->component = 'mod_book'; // Full name of the plugin (used for diagnostics)
-$plugin->version   = 2015111600; // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2015111601; // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2015111000; // Requires this Moodle version
 $plugin->cron      = 0;          // Period for cron to check this module (secs)
index 2361aac..2e0f946 100644 (file)
@@ -33,7 +33,8 @@ $functions = array(
         'methodname'    => 'login_user',
         'description'   => 'Log a user into a chat room in the given chat.',
         'type'          => 'write',
-        'capabilities'  => 'mod/chat:chat'
+        'capabilities'  => 'mod/chat:chat',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_chat_get_chat_users' => array(
@@ -41,7 +42,8 @@ $functions = array(
         'methodname'    => 'get_chat_users',
         'description'   => 'Get the list of users in the given chat session.',
         'type'          => 'read',
-        'capabilities'  => 'mod/chat:chat'
+        'capabilities'  => 'mod/chat:chat',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_chat_send_chat_message' => array(
@@ -49,7 +51,8 @@ $functions = array(
         'methodname'    => 'send_chat_message',
         'description'   => 'Send a message on the given chat session.',
         'type'          => 'write',
-        'capabilities'  => 'mod/chat:chat'
+        'capabilities'  => 'mod/chat:chat',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_chat_get_chat_latest_messages' => array(
@@ -57,7 +60,8 @@ $functions = array(
         'methodname'    => 'get_chat_latest_messages',
         'description'   => 'Get the latest messages from the given chat session.',
         'type'          => 'read',
-        'capabilities'  => 'mod/chat:chat'
+        'capabilities'  => 'mod/chat:chat',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_chat_view_chat' => array(
@@ -65,7 +69,8 @@ $functions = array(
         'methodname'    => 'view_chat',
         'description'   => 'Trigger the course module viewed event and update the module completion status.',
         'type'          => 'write',
-        'capabilities'  => 'mod/chat:chat'
+        'capabilities'  => 'mod/chat:chat',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_chat_get_chats_by_courses' => array(
@@ -74,6 +79,7 @@ $functions = array(
         'description'   => 'Returns a list of chat instances in a provided set of courses,
                             if no courses are provided then all the chat instances the user has access to will be returned.',
         'type'          => 'read',
-        'capabilities'  => ''
+        'capabilities'  => '',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     )
 );
index 223f84e..274f334 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015111600; // The current module version (Date: YYYYMMDDXX).
+$plugin->version   = 2015111601; // The current module version (Date: YYYYMMDDXX).
 $plugin->requires  = 2015111000; // Requires this Moodle version.
 $plugin->component = 'mod_chat'; // Full name of the plugin (used for diagnostics).
 $plugin->cron      = 300;
index 8670d27..3ccc8dd 100644 (file)
@@ -33,7 +33,8 @@ $functions = array(
         'methodname'    => 'get_choice_results',
         'description'   => 'Retrieve users results for a given choice.',
         'type'          => 'read',
-        'capabilities'  => ''
+        'capabilities'  => '',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_choice_get_choice_options' => array(
@@ -41,7 +42,8 @@ $functions = array(
         'methodname'    => 'get_choice_options',
         'description'   => 'Retrieve options for a specific choice.',
         'type'          => 'read',
-        'capabilities'  => 'mod/choice:choose'
+        'capabilities'  => 'mod/choice:choose',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_choice_submit_choice_response' => array(
@@ -49,7 +51,8 @@ $functions = array(
         'methodname'    => 'submit_choice_response',
         'description'   => 'Submit responses to a specific choice item.',
         'type'          => 'write',
-        'capabilities'  => 'mod/choice:choose'
+        'capabilities'  => 'mod/choice:choose',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_choice_view_choice' => array(
@@ -57,7 +60,8 @@ $functions = array(
         'methodname'    => 'view_choice',
         'description'   => 'Trigger the course module viewed event and update the module completion status.',
         'type'          => 'write',
-        'capabilities'  => ''
+        'capabilities'  => '',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_choice_get_choices_by_courses' => array(
@@ -66,7 +70,8 @@ $functions = array(
         'description'   => 'Returns a list of choice instances in a provided set of courses,
                             if no courses are provided then all the choice instances the user has access to will be returned.',
         'type'          => 'read',
-        'capabilities'  => ''
+        'capabilities'  => '',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_choice_delete_choice_responses' => array(
@@ -74,6 +79,7 @@ $functions = array(
         'methodname'    => 'delete_choice_responses',
         'description'   => 'Delete the given submitted responses in a choice',
         'type'          => 'write',
-        'capabilities'  => 'mod/choice:choose'
+        'capabilities'  => 'mod/choice:choose',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 );
index 2b62ca9..20e663d 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2016020100;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2016020101;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2015111000;    // Requires this Moodle version
 $plugin->component = 'mod_choice';     // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;
index 557e504..980188d 100644 (file)
@@ -32,6 +32,7 @@ $functions = array(
         'description' => 'Returns a list of database instances in a provided set of courses, if
             no courses are provided then all the database instances the user has access to will be returned.',
         'type' => 'read',
-        'capabilities' => 'mod/data:viewentry'
+        'capabilities' => 'mod/data:viewentry',
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     )
 );
index e29a7d6..705c3db 100644 (file)
@@ -38,7 +38,7 @@ class data_field_file extends data_field_base {
         // editing an existing database entry
         if ($formdata) {
             $fieldname = 'field_' . $this->field->id . '_file';
-            $itemid = $formdata->$fieldname;
+            $itemid = clean_param($formdata->$fieldname, PARAM_INT);
         } else if ($recordid) {
             if ($content = $DB->get_record('data_content', array('fieldid'=>$this->field->id, 'recordid'=>$recordid))) {
 
@@ -79,7 +79,7 @@ class data_field_file extends data_field_base {
         }
 
         // itemid element
-        $html .= '<input type="hidden" name="field_'.$this->field->id.'_file" value="'.$itemid.'" />';
+        $html .= '<input type="hidden" name="field_'.$this->field->id.'_file" value="'.s($itemid).'" />';
 
         $options = new stdClass();
         $options->maxbytes = $this->field->param3;
@@ -104,7 +104,7 @@ class data_field_file extends data_field_base {
 
     function display_search_field($value = '') {
         return '<label class="accesshide" for="f_' . $this->field->id . '">' . $this->field->name . '</label>' .
-               '<input type="text" size="16" id="f_'.$this->field->id.'" name="f_'.$this->field->id.'" value="'.$value.'" />';
+               '<input type="text" size="16" id="f_'.$this->field->id.'" name="f_'.$this->field->id.'" value="'.s($value).'" />';
     }
 
     function generate_sql($tablealias, $value) {
index d04be45..4035f50 100644 (file)
@@ -71,7 +71,7 @@ class data_field_number extends data_field_base {
 
     function display_search_field($value = '') {
         return '<label class="accesshide" for="f_'.$this->field->id.'">' . get_string('fieldname', 'data') . '</label>' .
-               '<input type="text" size="16" id="f_'.$this->field->id.'" name="f_'.$this->field->id.'" value="'.$value.'" />';
+               '<input type="text" size="16" id="f_'.$this->field->id.'" name="f_'.$this->field->id.'" value="'.s($value).'" />';
     }
 
     function parse_search_field() {
index 36bcd55..571fd73 100644 (file)
@@ -39,7 +39,7 @@ class data_field_picture extends data_field_base {
 
         if ($formdata) {
             $fieldname = 'field_' . $this->field->id . '_file';
-            $itemid = $formdata->$fieldname;
+            $itemid = clean_param($formdata->$fieldname, PARAM_INT);
             $fieldname = 'field_' . $this->field->id . '_alttext';
             if (isset($formdata->$fieldname)) {
                 $alttext = $formdata->$fieldname;
@@ -109,7 +109,7 @@ class data_field_picture extends data_field_base {
         $str .= $output->render($fm);
 
         $str .= '<div class="mdl-left">';
-        $str .= '<input type="hidden" name="field_'.$this->field->id.'_file" value="'.$itemid.'" />';
+        $str .= '<input type="hidden" name="field_'.$this->field->id.'_file" value="'.s($itemid).'" />';
         $str .= '<label for="field_'.$this->field->id.'_alttext">'.get_string('alttext','data') .'</label>&nbsp;<input type="text" name="field_'
                 .$this->field->id.'_alttext" id="field_'.$this->field->id.'_alttext" value="'.s($alttext).'" />';
         $str .= '</div>';
@@ -140,7 +140,7 @@ class data_field_picture extends data_field_base {
 
     function display_search_field($value = '') {
         return '<label class="accesshide" for="f_'.$this->field->id.'">' . get_string('fieldname', 'data') . '</label>' .
-               '<input type="text" size="16" id="f_'.$this->field->id.'" name="f_'.$this->field->id.'" value="'.$value.'" />';
+               '<input type="text" size="16" id="f_'.$this->field->id.'" name="f_'.$this->field->id.'" value="'.s($value).'" />';
     }
 
     function parse_search_field() {
index 54cd497..90aee4c 100644 (file)
@@ -27,7 +27,7 @@ class data_field_text extends data_field_base {
     var $type = 'text';
 
     function display_search_field($value = '') {
-        return '<label class="accesshide" for="f_' . $this->field->id . '">'. $this->field->name.'</label>' . '<input type="text" size="16" id="f_'.$this->field->id.'" name="f_'.$this->field->id.'" value="'.$value.'" />';
+        return '<label class="accesshide" for="f_' . $this->field->id . '">'. $this->field->name.'</label>' . '<input type="text" size="16" id="f_'.$this->field->id.'" name="f_'.$this->field->id.'" value="'.s($value).'" />';
     }
 
     function parse_search_field() {
index ddf5486..98c3f6d 100644 (file)
@@ -79,7 +79,7 @@ class data_field_textarea extends data_field_base {
             }
             $fieldname = 'field_' . $this->field->id . '_itemid';
             if (isset($formdata->$fieldname)) {
-                $draftitemid = $formdata->$fieldname;
+                $draftitemid = clean_param($formdata->$fieldname, PARAM_INT);
             } else {
                 $draftitemid = file_get_unused_draft_itemid();
             }
@@ -146,7 +146,7 @@ class data_field_textarea extends data_field_base {
         }
         $editor->set_text($text);
         $editor->use_editor($field, $options, $fpoptions);
-        $str .= '<input type="hidden" name="'.$field.'_itemid" value="'.$draftitemid.'" />';
+        $str .= '<input type="hidden" name="'.$field.'_itemid" value="'.s($draftitemid).'" />';
         $str .= '<div class="mod-data-input">';
         $str .= '<div><textarea id="'.$field.'" name="'.$field.'" rows="'.$this->field->param3.'" cols="'.$this->field->param2.'" spellcheck="true">'.s($text).'</textarea></div>';
         $str .= '<div><label class="accesshide" for="' . $field . '_content1">' . get_string('format') . '</label>';
@@ -166,7 +166,7 @@ class data_field_textarea extends data_field_base {
 
     function display_search_field($value = '') {
         return '<label class="accesshide" for="f_' . $this->field->id . '">' . $this->field->name . '</label>' .
-               '<input type="text" size="16" id="f_'.$this->field->id.'" name="f_'.$this->field->id.'" value="'.$value.'" />';
+               '<input type="text" size="16" id="f_'.$this->field->id.'" name="f_'.$this->field->id.'" value="'.s($value).'" />';
     }
 
     function parse_search_field() {
index 4d22f32..da87cb5 100644 (file)
@@ -81,7 +81,7 @@ class data_field_url extends data_field_base {
             }
             $str .= '</td><td>';
             $str .= $label;
-            $str .= '<input type="text" name="field_'.$this->field->id.'_0" id="'.$fieldid.'" value="'.$url.'" size="60" />';
+            $str .= '<input type="text" name="field_'.$this->field->id.'_0" id="'.$fieldid.'" value="'.s($url).'" size="60" />';
             $str .= '<button id="filepicker-button-'.$options->client_id.'" style="display:none">'.$straddlink.'</button></td></tr>';
             $str .= '<tr><td align="right"><span class="mod-data-input">'.get_string('text', 'data').':</span></td><td>';
             $str .= '<input type="text" name="field_'.$this->field->id.'_1" id="field_'.$this->field->id.'_1" value="'.s($text).'"';
@@ -108,7 +108,7 @@ class data_field_url extends data_field_base {
 
     function display_search_field($value = '') {
         return '<label class="accesshide" for="f_'.$this->field->id.'">' . get_string('fieldname', 'data') . '</label>' .
-               '<input type="text" size="16" id="f_'.$this->field->id.'" name="f_'.$this->field->id.'" value="'.$value.'" />';
+               '<input type="text" size="16" id="f_'.$this->field->id.'" name="f_'.$this->field->id.'" value="'.s($value).'" />';
     }
 
     function parse_search_field() {
@@ -143,6 +143,7 @@ class data_field_url extends data_field_base {
                 if ($this->field->param3) {
                     // param3 defines whether this URL should open in a new window.
                     $attributes['target'] = '_blank';
+                    $attributes['rel'] = 'noreferrer';
                 }
 
                 if (empty($text)) {
index f4eaaa6..a85601c 100644 (file)
@@ -1736,9 +1736,9 @@ function data_print_preference_form($data, $perpage, $search, $sort='', $order='
     $fn = !empty($search_array[DATA_FIRSTNAME]->data) ? $search_array[DATA_FIRSTNAME]->data : '';
     $ln = !empty($search_array[DATA_LASTNAME]->data) ? $search_array[DATA_LASTNAME]->data : '';
     $patterns[]    = '/##firstname##/';
-    $replacement[] = '<label class="accesshide" for="u_fn">'.get_string('authorfirstname', 'data').'</label><input type="text" size="16" id="u_fn" name="u_fn" value="'.$fn.'" />';
+    $replacement[] = '<label class="accesshide" for="u_fn">'.get_string('authorfirstname', 'data').'</label><input type="text" size="16" id="u_fn" name="u_fn" value="'.s($fn).'" />';
     $patterns[]    = '/##lastname##/';
-    $replacement[] = '<label class="accesshide" for="u_ln">'.get_string('authorlastname', 'data').'</label><input type="text" size="16" id="u_ln" name="u_ln" value="'.$ln.'" />';
+    $replacement[] = '<label class="accesshide" for="u_ln">'.get_string('authorlastname', 'data').'</label><input type="text" size="16" id="u_ln" name="u_ln" value="'.s($ln).'" />';
 
     // actual replacement of the tags
     $newtext = preg_replace($patterns, $replacement, $data->asearchtemplate);
index 5ff6ce5..955567d 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015111600;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2015111601;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2015111000;       // Requires this Moodle version
 $plugin->component = 'mod_data';       // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;
index c24e4c5..c05b93a 100644 (file)
@@ -38,6 +38,7 @@ $string['autonumbering_help'] = 'Enables or disables automated numbers for each
 $string['average'] = 'Average';
 $string['bold'] = 'Bold';
 $string['cancel_moving'] = 'Cancel moving';
+$string['cannotaccess'] = 'You can only access this feedback from a course';
 $string['cannotmapfeedback'] = 'Database problem, unable to map feedback to course';
 $string['cannotsavetempl'] = 'saving templates is not allowed';
 $string['cannotunmap'] = 'Database problem, unable to unmap';
index 1b68cd5..ea98a20 100644 (file)
@@ -2815,7 +2815,11 @@ function feedback_get_feedbacks_from_sitecourse_map($courseid) {
         }
     }
 
-    return array_merge($feedbacks1, $feedbacks2);
+    $feedbacks = array_merge($feedbacks1, $feedbacks2);
+    $modinfo = get_fast_modinfo(SITEID);
+    return array_filter($feedbacks, function($f) use ($modinfo) {
+        return ($cm = $modinfo->get_cm($f->cmid)) && $cm->uservisible;
+    });
 
 }
 
index 83dec27..52a5df7 100644 (file)
@@ -49,11 +49,11 @@ if (has_capability('mod/feedback:complete', $context)) {
     $feedback_complete_cap = true;
 }
 
-if (isset($CFG->feedback_allowfullanonymous)
-            AND $CFG->feedback_allowfullanonymous
+if (!empty($CFG->feedback_allowfullanonymous)
             AND $course->id == SITEID
-            AND (!$courseid OR $courseid == SITEID)
-            AND $feedback->anonymous == FEEDBACK_ANONYMOUS_YES ) {
+            AND $feedback->anonymous == FEEDBACK_ANONYMOUS_YES
+            AND (!isloggedin() OR isguestuser())) {
+    // Guests are allowed to complete fully anonymous feedback without having 'mod/feedback:complete' capability.
     $feedback_complete_cap = true;
 }
 
@@ -62,16 +62,6 @@ if ($course->id == SITEID AND !$courseid) {
     $courseid = SITEID;
 }
 
-//check whether the feedback is mapped to the given courseid
-if ($course->id == SITEID AND !has_capability('mod/feedback:edititems', $context)) {
-    if ($DB->get_records('feedback_sitecourse_map', array('feedbackid'=>$feedback->id))) {
-        $params = array('feedbackid'=>$feedback->id, 'courseid'=>$courseid);
-        if (!$DB->get_record('feedback_sitecourse_map', $params)) {
-            print_error('invalidcoursemodule');
-        }
-    }
-}
-
 if ($feedback->anonymous != FEEDBACK_ANONYMOUS_YES) {
     if ($course->id == SITEID) {
         require_login($course, true);
@@ -86,6 +76,32 @@ if ($feedback->anonymous != FEEDBACK_ANONYMOUS_YES) {
     }
 }
 
+if ($course->id == SITEID) {
+    $PAGE->set_context($context);
+    $PAGE->set_cm($cm, $course);
+    $PAGE->set_pagelayout('incourse');
+}
+$PAGE->set_url('/mod/feedback/view.php', array('id'=>$cm->id, 'do_show'=>'view'));
+$PAGE->set_title($feedback->name);
+$PAGE->set_heading($course->fullname);
+
+// Check whether the feedback is mapped to the given courseid.
+if ($course->id == SITEID AND !has_capability('mod/feedback:edititems', $context)) {
+    if ($DB->get_records('feedback_sitecourse_map', array('feedbackid' => $feedback->id))) {
+        $params = array('feedbackid' => $feedback->id, 'courseid' => $courseid);
+        if (!$DB->get_record('feedback_sitecourse_map', $params)) {
+            if ($courseid == SITEID) {
+                echo $OUTPUT->header();
+                echo $OUTPUT->notification(get_string('cannotaccess', 'mod_feedback'));
+                echo $OUTPUT->footer();
+                exit;
+            } else {
+                print_error('invalidcoursemodule');
+            }
+        }
+    }
+}
+
 //check whether the given courseid exists
 if ($courseid AND $courseid != SITEID) {
     if ($course2 = $DB->get_record('course', array('id'=>$courseid))) {
@@ -113,15 +129,6 @@ $event->trigger();
 /// Print the page header
 $strfeedbacks = get_string("modulenameplural", "feedback");
 $strfeedback  = get_string("modulename", "feedback");
-
-if ($course->id == SITEID) {
-    $PAGE->set_context($context);
-    $PAGE->set_cm($cm, $course); // set's up global $COURSE
-    $PAGE->set_pagelayout('incourse');
-}
-$PAGE->set_url('/mod/feedback/view.php', array('id'=>$cm->id, 'do_show'=>'view'));
-$PAGE->set_title($feedback->name);
-$PAGE->set_heading($course->fullname);
 echo $OUTPUT->header();
 
 //ishidden check.
index 737ae35..dc99134 100644 (file)
@@ -33,7 +33,8 @@ $functions = array(
         'methodname'    => 'view_folder',
         'description'   => 'Simulate the view.php web interface folder: trigger events, completion, etc...',
         'type'          => 'write',
-        'capabilities'  => 'mod/folder:view'
+        'capabilities'  => 'mod/folder:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
 );
index 5bfa7f7..a2911c6 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2016020201;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2016020202;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2015111000;    // Requires this Moodle version
 $plugin->component = 'mod_folder';     // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;
index 43cf00e..a054483 100644 (file)
@@ -33,7 +33,8 @@ $functions = array(
             no courses are provided then all the forum instances the user has access to will be
             returned.',
         'type' => 'read',
-        'capabilities' => 'mod/forum:viewdiscussion'
+        'capabilities' => 'mod/forum:viewdiscussion',
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_forum_get_forum_discussions' => array(
@@ -52,7 +53,8 @@ $functions = array(
         'classpath' => 'mod/forum/externallib.php',
         'description' => 'Returns a list of forum posts for a discussion.',
         'type' => 'read',
-        'capabilities' => 'mod/forum:viewdiscussion, mod/forum:viewqandawithoutposting'
+        'capabilities' => 'mod/forum:viewdiscussion, mod/forum:viewqandawithoutposting',
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_forum_get_forum_discussions_paginated' => array(
@@ -61,7 +63,8 @@ $functions = array(
         'classpath' => 'mod/forum/externallib.php',
         'description' => 'Returns a list of forum discussions optionally sorted and paginated.',
         'type' => 'read',
-        'capabilities' => 'mod/forum:viewdiscussion, mod/forum:viewqandawithoutposting'
+        'capabilities' => 'mod/forum:viewdiscussion, mod/forum:viewqandawithoutposting',
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_forum_view_forum' => array(
@@ -70,7 +73,8 @@ $functions = array(
         'classpath' => 'mod/forum/externallib.php',
         'description' => 'Trigger the course module viewed event and update the module completion status.',
         'type' => 'write',
-        'capabilities' => 'mod/forum:viewdiscussion'
+        'capabilities' => 'mod/forum:viewdiscussion',
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_forum_view_forum_discussion' => array(
@@ -79,7 +83,8 @@ $functions = array(
         'classpath' => 'mod/forum/externallib.php',
         'description' => 'Trigger the forum discussion viewed event.',
         'type' => 'write',
-        'capabilities' => 'mod/forum:viewdiscussion'
+        'capabilities' => 'mod/forum:viewdiscussion',
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_forum_add_discussion_post' => array(
@@ -88,7 +93,8 @@ $functions = array(
         'classpath' => 'mod/forum/externallib.php',
         'description' => 'Create new posts into an existing discussion.',
         'type' => 'write',
-        'capabilities' => 'mod/forum:replypost'
+        'capabilities' => 'mod/forum:replypost',
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_forum_add_discussion' => array(
@@ -97,7 +103,8 @@ $functions = array(
         'classpath' => 'mod/forum/externallib.php',
         'description' => 'Add a new discussion into an existing forum.',
         'type' => 'write',
-        'capabilities' => 'mod/forum:startdiscussion'
+        'capabilities' => 'mod/forum:startdiscussion',
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_forum_can_add_discussion' => array(
@@ -105,6 +112,7 @@ $functions = array(
         'methodname' => 'can_add_discussion',
         'classpath' => 'mod/forum/externallib.php',
         'description' => 'Check if the current user can add discussions in the given forum (and optionally for the given group).',
-        'type' => 'read'
+        'type' => 'read',
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 );
index cd4eaf1..e5367c0 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015120801;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2015120802;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2015111000;       // Requires this Moodle version
 $plugin->component = 'mod_forum';      // Full name of the plugin (used for diagnostics)
index b2e20fc..0f146e7 100644 (file)
@@ -32,7 +32,8 @@ $functions = array(
         'methodname'    => 'get_glossaries_by_courses',
         'description'   => 'Retrieve a list of glossaries from several courses.',
         'type'          => 'read',
-        'capabilities'  => 'mod/glossary:view'
+        'capabilities'  => 'mod/glossary:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_glossary_view_glossary' => array(
@@ -40,7 +41,8 @@ $functions = array(
         'methodname'    => 'view_glossary',
         'description'   => 'Notify the glossary as being viewed.',
         'type'          => 'write',
-        'capabilities'  => 'mod/glossary:view'
+        'capabilities'  => 'mod/glossary:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_glossary_view_entry' => array(
@@ -48,7 +50,8 @@ $functions = array(
         'methodname'    => 'view_entry',
         'description'   => 'Notify a glossary entry as being viewed.',
         'type'          => 'write',
-        'capabilities'  => 'mod/glossary:view'
+        'capabilities'  => 'mod/glossary:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_glossary_get_entries_by_letter' => array(
@@ -56,7 +59,8 @@ $functions = array(
         'methodname'    => 'get_entries_by_letter',
         'description'   => 'Browse entries by letter.',
         'type'          => 'read',
-        'capabilities'  => 'mod/glossary:view'
+        'capabilities'  => 'mod/glossary:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_glossary_get_entries_by_date' => array(
@@ -64,7 +68,8 @@ $functions = array(
         'methodname'    => 'get_entries_by_date',
         'description'   => 'Browse entries by date.',
         'type'          => 'read',
-        'capabilities'  => 'mod/glossary:view'
+        'capabilities'  => 'mod/glossary:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_glossary_get_categories' => array(
@@ -72,7 +77,8 @@ $functions = array(
         'methodname'    => 'get_categories',
         'description'   => 'Get the categories.',
         'type'          => 'read',
-        'capabilities'  => 'mod/glossary:view'
+        'capabilities'  => 'mod/glossary:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_glossary_get_entries_by_category' => array(
@@ -80,7 +86,8 @@ $functions = array(
         'methodname'    => 'get_entries_by_category',
         'description'   => 'Browse entries by category.',
         'type'          => 'read',
-        'capabilities'  => 'mod/glossary:view'
+        'capabilities'  => 'mod/glossary:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_glossary_get_authors' => array(
@@ -88,7 +95,8 @@ $functions = array(
         'methodname'    => 'get_authors',
         'description'   => 'Get the authors.',
         'type'          => 'read',
-        'capabilities'  => 'mod/glossary:view'
+        'capabilities'  => 'mod/glossary:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_glossary_get_entries_by_author' => array(
@@ -96,7 +104,8 @@ $functions = array(
         'methodname'    => 'get_entries_by_author',
         'description'   => 'Browse entries by author.',
         'type'          => 'read',
-        'capabilities'  => 'mod/glossary:view'
+        'capabilities'  => 'mod/glossary:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_glossary_get_entries_by_author_id' => array(
@@ -104,7 +113,8 @@ $functions = array(
         'methodname'    => 'get_entries_by_author_id',
         'description'   => 'Browse entries by author ID.',
         'type'          => 'read',
-        'capabilities'  => 'mod/glossary:view'
+        'capabilities'  => 'mod/glossary:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_glossary_get_entries_by_search' => array(
@@ -112,7 +122,8 @@ $functions = array(
         'methodname'    => 'get_entries_by_search',
         'description'   => 'Browse entries by search query.',
         'type'          => 'read',
-        'capabilities'  => 'mod/glossary:view'
+        'capabilities'  => 'mod/glossary:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_glossary_get_entries_by_term' => array(
@@ -120,7 +131,8 @@ $functions = array(
         'methodname'    => 'get_entries_by_term',
         'description'   => 'Browse entries by term (concept or alias).',
         'type'          => 'read',
-        'capabilities'  => 'mod/glossary:view'
+        'capabilities'  => 'mod/glossary:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_glossary_get_entries_to_approve' => array(
@@ -128,7 +140,8 @@ $functions = array(
         'methodname'    => 'get_entries_to_approve',
         'description'   => 'Browse entries to be approved.',
         'type'          => 'read',
-        'capabilities'  => 'mod/glossary:approve'
+        'capabilities'  => 'mod/glossary:approve',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_glossary_get_entry_by_id' => array(
@@ -136,7 +149,8 @@ $functions = array(
         'methodname'    => 'get_entry_by_id',
         'description'   => 'Get an entry by ID',
         'type'          => 'read',
-        'capabilities'  => 'mod/glossary:view'
+        'capabilities'  => 'mod/glossary:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
 );
index 6b40737..9bb4fa6 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015111605;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2015111606;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2015111000;    // Requires this Moodle version
 $plugin->component = 'mod_glossary';   // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;
index 981858e..3233845 100644 (file)
@@ -33,7 +33,8 @@ $functions = array(
         'methodname'    => 'view_imscp',
         'description'   => 'Simulate the view.php web interface imscp: trigger events, completion, etc...',
         'type'          => 'write',
-        'capabilities'  => 'mod/imscp:view'
+        'capabilities'  => 'mod/imscp:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_imscp_get_imscps_by_courses' => array(
@@ -42,7 +43,8 @@ $functions = array(
         'description'   => 'Returns a list of IMSCP instances in a provided set of courses,
                             if no courses are provided then all the IMSCP instances the user has access to will be returned.',
         'type'          => 'read',
-        'capabilities'  => 'moodle/imscp:view'
+        'capabilities'  => 'mod/imscp:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
 );
index 201960d..bb81074 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015111600;  // The current module version (Date: YYYYMMDDXX).
+$plugin->version   = 2015111601;  // The current module version (Date: YYYYMMDDXX).
 $plugin->requires  = 2015111000;  // Requires this Moodle version.
 $plugin->component = 'mod_imscp'; // Full name of the plugin (used for diagnostics).
 $plugin->cron      = 0;
diff --git a/mod/lti/classes/service_exception_handler.php b/mod/lti/classes/service_exception_handler.php
new file mode 100644 (file)
index 0000000..597a4d3
--- /dev/null
@@ -0,0 +1,122 @@
+<?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/>.
+
+/**
+ * Exception handler for LTI services
+ *
+ * @package   mod_lti
+ * @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com)
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_lti;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__.'/../locallib.php');
+require_once(__DIR__.'/../servicelib.php');
+
+/**
+ * Handles exceptions when handling incoming LTI messages.
+ *
+ * Ensures that LTI always returns a XML message that can be consumed by the caller.
+ *
+ * @package   mod_lti
+ * @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com)
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class service_exception_handler {
+    /**
+     * Enable error response logging.
+     *
+     * @var bool
+     */
+    protected $log = false;
+
+    /**
+     * The LTI service message ID, if known.
+     *
+     * @var string
+     */
+    protected $id = '';
+
+    /**
+     * The LTI service message type, if known.
+     *
+     * @var string
+     */
+    protected $type = 'unknownRequest';
+
+    /**
+     * Constructor.
+     *
+     * @param boolean $log Enable error response logging.
+     */
+    public function __construct($log) {
+        $this->log = $log;
+    }
+
+    /**
+     * Set the LTI message ID being handled.
+     *
+     * @param string $id
+     */
+    public function set_message_id($id) {
+        if (!empty($id)) {
+            $this->id = $id;
+        }
+    }
+
+    /**
+     * Set the LTI message type being handled.
+     *
+     * @param string $type
+     */
+    public function set_message_type($type) {
+        if (!empty($type)) {
+            $this->type = $type;
+        }
+    }
+
+    /**
+     * Echo an exception message encapsulated in XML.
+     *
+     * @param \Exception $exception The exception that was thrown
+     */
+    public function handle(\Exception $exception) {
+        $message = $exception->getMessage();
+
+        // Add the exception backtrace for developers.
+        if (debugging('', DEBUG_DEVELOPER)) {
+            $message .= "\n".format_backtrace(get_exception_info($exception)->backtrace, true);
+        }
+
+        // Switch to response.
+        $type = str_replace('Request', 'Response', $this->type);
+
+        // Build the appropriate xml.
+        $response = lti_get_response_xml('failure', $message, $this->id, $type);
+
+        $xml = $response->asXML();
+
+        // Log the request if necessary.
+        if ($this->log) {
+            lti_log_response($xml, $exception);
+        }
+
+        echo $xml;
+    }
+}
index aeee9b8..48e395a 100644 (file)
@@ -31,7 +31,8 @@ $functions = array(
         'methodname'    => 'get_tool_launch_data',
         'description'   => 'Return the launch data for a given external tool.',
         'type'          => 'read',
-        'capabilities'  => 'mod/lti:view'
+        'capabilities'  => 'mod/lti:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_lti_get_ltis_by_courses' => array(
@@ -40,7 +41,8 @@ $functions = array(
         'description'   => 'Returns a list of external tool instances in a provided set of courses, if
                             no courses are provided then all the external tool instances the user has access to will be returned.',
         'type'          => 'read',
-        'capabilities'  => 'mod/lti:view'
+        'capabilities'  => 'mod/lti:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_lti_view_lti' => array(
@@ -48,6 +50,7 @@ $functions = array(
         'methodname'    => 'view_lti',
         'description'   => 'Trigger the course module viewed event and update the module completion status.',
         'type'          => 'write',
-        'capabilities'  => 'mod/lti:view'
+        'capabilities'  => 'mod/lti:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 );
index 9840dba..aaa95ba 100644 (file)
@@ -1906,14 +1906,50 @@ function lti_should_log_request($rawbody) {
 }
 
 /**
- * Logs the request to a file in temp dir
+ * Logs the request to a file in temp dir.
  *
  * @param string $rawbody
  */
 function lti_log_request($rawbody) {
     if ($tempdir = make_temp_directory('mod_lti', false)) {
         if ($tempfile = tempnam($tempdir, 'mod_lti_request'.date('YmdHis'))) {
-            file_put_contents($tempfile, $rawbody);
+            $content  = "Request Headers:\n";
+            foreach (moodle\mod\lti\OAuthUtil::get_headers() as $header => $value) {
+                $content .= "$header: $value\n";
+            }
+            $content .= "Request Body:\n";
+            $content .= $rawbody;
+
+            file_put_contents($tempfile, $content);
+            chmod($tempfile, 0644);
+        }
+    }
+}
+
+/**
+ * Log an LTI response.
+ *
+ * @param string $responsexml The response XML
+ * @param Exception $e If there was an exception, pass that too
+ */
+function lti_log_response($responsexml, $e = null) {
+    if ($tempdir = make_temp_directory('mod_lti', false)) {
+        if ($tempfile = tempnam($tempdir, 'mod_lti_response'.date('YmdHis'))) {
+            $content = '';
+            if ($e instanceof Exception) {
+                $info = get_exception_info($e);
+
+                $content .= "Exception:\n";
+                $content .= "Message: $info->message\n";
+                $content .= "Debug info: $info->debuginfo\n";
+                $content .= "Backtrace:\n";
+                $content .= format_backtrace($info->backtrace, true);
+                $content .= "\n";
+            }
+            $content .= "Response XML:\n";
+            $content .= $responsexml;
+
+            file_put_contents($tempfile, $content);
             chmod($tempfile, 0644);
         }
     }
index 0075f8e..a00ce88 100644 (file)
@@ -31,11 +31,18 @@ require_once($CFG->dirroot.'/mod/lti/locallib.php');
 require_once($CFG->dirroot.'/mod/lti/servicelib.php');
 
 // TODO: Switch to core oauthlib once implemented - MDL-30149.
+use mod_lti\service_exception_handler;
 use moodle\mod\lti as lti;
 
 $rawbody = file_get_contents("php://input");
 
-if (lti_should_log_request($rawbody)) {
+$logrequests  = lti_should_log_request($rawbody);
+$errorhandler = new service_exception_handler($logrequests);
+
+// Register our own error handler so we can always send valid XML response.
+set_exception_handler(array($errorhandler, 'handle'));
+
+if ($logrequests) {
     lti_log_request($rawbody);
 }
 
@@ -73,20 +80,13 @@ foreach ($body->children() as $child) {
     $messagetype = $child->getName();
 }
 
+// We know more about the message, update error handler to send better errors.
+$errorhandler->set_message_id(lti_parse_message_id($xml));
+$errorhandler->set_message_type($messagetype);
+
 switch ($messagetype) {
     case 'replaceResultRequest':
-        try {
-            $parsed = lti_parse_grade_replace_message($xml);
-        } catch (Exception $e) {
-            $responsexml = lti_get_response_xml(
-                'failure',
-                $e->getMessage(),
-                uniqid(),
-                'replaceResultResponse');
-
-            echo $responsexml->asXML();
-            break;
-        }
+        $parsed = lti_parse_grade_replace_message($xml);
 
         $ltiinstance = $DB->get_record('lti', array('id' => $parsed->instanceid));
 
@@ -99,8 +99,12 @@ switch ($messagetype) {
 
         $gradestatus = lti_update_grade($ltiinstance, $parsed->userid, $parsed->launchid, $parsed->gradeval);
 
+        if (!$gradestatus) {
+            throw new Exception('Grade replace response');
+        }
+
         $responsexml = lti_get_response_xml(
-                $gradestatus ? 'success' : 'failure',
+                'success',
                 'Grade replace response',
                 $parsed->messageid,
                 'replaceResultResponse'
@@ -157,8 +161,12 @@ switch ($messagetype) {
 
         $gradestatus = lti_delete_grade($ltiinstance, $parsed->userid);
 
+        if (!$gradestatus) {
+            throw new Exception('Grade delete request');
+        }
+
         $responsexml = lti_get_response_xml(
-                $gradestatus ? 'success' : 'failure',
+                'success',
                 'Grade delete request',
                 $parsed->messageid,
                 'deleteResultResponse'
index 438cea2..b6d4e46 100644 (file)
@@ -57,6 +57,10 @@ function lti_get_response_xml($codemajor, $description, $messageref, $messagetyp
 }
 
 function lti_parse_message_id($xml) {
+    if (empty($xml->imsx_POXHeader)) {
+        return '';
+    }
+
     $node = $xml->imsx_POXHeader->imsx_POXRequestHeaderInfo->imsx_messageIdentifier;
     $messageid = (string)$node;
 
@@ -217,14 +221,11 @@ function lti_read_grade($ltiinstance, $userid) {
     if (!empty($ltigrade) && isset($grades) && isset($grades->items[0]) && is_array($grades->items[0]->grades)) {
         foreach ($grades->items[0]->grades as $agrade) {
             $grade = $agrade->grade;
-            $grade = $grade / $ltigrade;
-            break;
+            if (isset($grade)) {
+                return $grade / $ltigrade;
+            }
         }
     }
-
-    if (isset($grade)) {
-        return $grade;
-    }
 }
 
 function lti_delete_grade($ltiinstance, $userid) {
@@ -285,29 +286,14 @@ function lti_verify_sourcedid($ltiinstance, $parsed) {
 function lti_extend_lti_services($data) {
     $plugins = get_plugin_list_with_function('ltisource', $data->messagetype);
     if (!empty($plugins)) {
-        try {
-            // There can only be one.
-            if (count($plugins) > 1) {
-                throw new coding_exception('More than one ltisource plugin handler found');
-            }
-            $data->xml = new SimpleXMLElement($data->body);
-            $callback = current($plugins);
-            call_user_func($callback, $data);
-        } catch (moodle_exception $e) {
-            $error = $e->getMessage();
-            if (debugging('', DEBUG_DEVELOPER)) {
-                $error .= ' '.format_backtrace(get_exception_info($e)->backtrace);
-            }
-            $responsexml = lti_get_response_xml(
-                'failure',
-                $error,
-                $data->messageid,
-                $data->messagetype
-            );
-
-            header('HTTP/1.0 400 bad request');
-            echo $responsexml->asXML();
+        // There can only be one.
+        if (count($plugins) > 1) {
+            throw new coding_exception('More than one ltisource plugin handler found');
         }
+        $data->xml = new SimpleXMLElement($data->body);
+        $callback = current($plugins);
+        call_user_func($callback, $data);
+
         return true;
     }
     return false;
diff --git a/mod/lti/tests/service_exception_handler_test.php b/mod/lti/tests/service_exception_handler_test.php
new file mode 100644 (file)
index 0000000..cd7270f
--- /dev/null
@@ -0,0 +1,85 @@
+<?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/>.
+
+/**
+ * Tests Exception handler for LTI services
+ *
+ * @package   mod_lti
+ * @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com)
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use mod_lti\service_exception_handler;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Tests Exception handler for LTI services
+ *
+ * @package   mod_lti
+ * @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com)
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mod_lti_service_exception_handler_testcase extends advanced_testcase {
+    /**
+     * Testing service error handling.
+     */
+    public function test_handle() {
+        $handler = new service_exception_handler(false);
+        $handler->set_message_id('123');
+        $handler->set_message_type('testRequest');
+        $handler->handle(new Exception('Error happened'));
+
+        $this->expectOutputRegex('/imsx_codeMajor>failure/');
+        $this->expectOutputRegex('/imsx_description>Error happened/');
+        $this->expectOutputRegex('/imsx_messageRefIdentifier>123/');
+        $this->expectOutputRegex('/imsx_operationRefIdentifier>testRequest/');
+        $this->expectOutputRegex('/imsx_POXBody><testResponse/');
+    }
+
+    /**
+     * Testing service error handling when message ID and type are not known yet.
+     */
+    public function test_handle_early_error() {
+        $handler = new service_exception_handler(false);
+        $handler->handle(new Exception('Error happened'));
+
+        $this->expectOutputRegex('/imsx_codeMajor>failure/');
+        $this->expectOutputRegex('/imsx_description>Error happened/');
+        $this->expectOutputRegex('/imsx_messageRefIdentifier\/>/');
+        $this->expectOutputRegex('/imsx_operationRefIdentifier>unknownRequest/');
+        $this->expectOutputRegex('/imsx_POXBody><unknownResponse/');
+    }
+
+    /**
+     * Testing that a log file is generated when logging is turned on.
+     */
+    public function test_handle_log() {
+        global $CFG;
+
+        $this->resetAfterTest();
+
+        $handler = new service_exception_handler(true);
+
+        ob_start();
+        $handler->handle(new Exception('Error happened'));
+        ob_end_clean();
+
+        $this->assertTrue(is_dir($CFG->dataroot.'/temp/mod_lti'));
+        $files = glob($CFG->dataroot.'/temp/mod_lti/mod_lti_response*');
+        $this->assertEquals(1, count($files));
+    }
+}
\ No newline at end of file
diff --git a/mod/lti/tests/servicelib_test.php b/mod/lti/tests/servicelib_test.php
new file mode 100644 (file)
index 0000000..286fd63
--- /dev/null
@@ -0,0 +1,115 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Tests for servicelib.php
+ *
+ * @package   mod_lti
+ * @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com)
+ * @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/servicelib.php');
+
+/**
+ * Tests for servicelib.php
+ *
+ * @package   mod_lti
+ * @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com)
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mod_lti_servicelib_testcase extends basic_testcase {
+    /**
+     * Test that lti_parse_message_id never fails with good and bad XML.
+     *
+     * @dataProvider message_id_provider
+     * @param mixed $expected Expected message ID.
+     * @param string $xml XML to parse.
+     */
+    public function test_lti_parse_message_id($expected, $xml) {
+        $xml = simplexml_load_string($xml);
+        $this->assertEquals($expected, lti_parse_message_id($xml));
+    }
+
+    /**
+     * Test data provider for testing lti_parse_message_id
+     *
+     * @return array
+     */
+    public function message_id_provider() {
+        $valid = <<<XML
+<?xml version="1.0" encoding="UTF-8"?>
+<imsx_POXEnvelopeRequest xmlns="http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0">
+    <imsx_POXHeader>
+        <imsx_POXRequestHeaderInfo>
+            <imsx_version>V1.0</imsx_version>
+            <imsx_messageIdentifier>9999</imsx_messageIdentifier>
+        </imsx_POXRequestHeaderInfo>
+    </imsx_POXHeader>
+    <imsx_POXBody/>
+</imsx_POXEnvelopeRequest>
+XML;
+
+        $noheader = <<<XML
+<?xml version="1.0" encoding="UTF-8"?>
+<imsx_POXEnvelopeRequest xmlns="http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0">
+    <badXmlHere>
+        <imsx_POXRequestHeaderInfo>
+            <imsx_version>V1.0</imsx_version>
+            <imsx_messageIdentifier>9999</imsx_messageIdentifier>
+        </imsx_POXRequestHeaderInfo>
+    </badXmlHere>
+    <imsx_POXBody/>
+</imsx_POXEnvelopeRequest>
+XML;
+
+        $noinfo = <<<XML
+<?xml version="1.0" encoding="UTF-8"?>
+<imsx_POXEnvelopeRequest xmlns="http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0">
+    <imsx_POXHeader>
+        <badXmlHere>
+            <imsx_version>V1.0</imsx_version>
+            <imsx_messageIdentifier>9999</imsx_messageIdentifier>
+        </badXmlHere>
+    </imsx_POXHeader>
+    <imsx_POXBody/>
+</imsx_POXEnvelopeRequest>
+XML;
+
+        $noidentifier = <<<XML
+<?xml version="1.0" encoding="UTF-8"?>
+<imsx_POXEnvelopeRequest xmlns="http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0">
+    <imsx_POXHeader>
+        <imsx_POXRequestHeaderInfo>
+            <imsx_version>V1.0</imsx_version>
+        </imsx_POXRequestHeaderInfo>
+    </imsx_POXHeader>
+    <imsx_POXBody/>
+</imsx_POXEnvelopeRequest>
+XML;
+
+        return array(
+            array(9999, $valid),
+            array('', $noheader),
+            array('', $noinfo),
+            array('', $noidentifier),
+        );
+    }
+}
\ No newline at end of file
index 501cb4c..7b15886 100644 (file)
@@ -1,5 +1,10 @@
 This files describes API changes in the lti code.
 
+=== 3.1 ===
+
+* When requesting a grade from the outcome service, and empty grade will now be returned as
+  an empty grade in accordance with the spec. Previously a grade of 0 would be returned.
+
 === 3.0 ===
 
 * Function lti_view (locallib.php) was renamed to lti_launch_tool.
index 678bacd..5e91322 100644 (file)
@@ -48,7 +48,7 @@
 
 defined('MOODLE_INTERNAL') || die;
 
-$plugin->version   = 2015111600;    // The current module version (Date: YYYYMMDDXX).
+$plugin->version   = 2015111601;    // The current module version (Date: YYYYMMDDXX).
 $plugin->requires  = 2015111000;    // Requires this Moodle version.
 $plugin->component = 'mod_lti';     // Full name of the plugin (used for diagnostics).
 $plugin->cron      = 0;
index be85f2e..6080069 100644 (file)
@@ -33,7 +33,8 @@ $functions = array(
         'methodname'    => 'view_page',
         'description'   => 'Simulate the view.php web interface page: trigger events, completion, etc...',
         'type'          => 'write',
-        'capabilities'  => 'mod/page:view'
+        'capabilities'  => 'mod/page:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
 );
index 3739256..04731bd 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015111600;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2015111601;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2015111000;    // Requires this Moodle version
 $plugin->component = 'mod_page';       // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;
index 130fc3d..cddad33 100644 (file)
@@ -331,4 +331,309 @@ class mod_quiz_external extends external_api {
         );
     }
 
+    /**
+     * Describes the parameters for get_user_attempts.
+     *
+     * @return external_external_function_parameters
+     * @since Moodle 3.1
+     */
+    public static function get_user_attempts_parameters() {
+        return new external_function_parameters (
+            array(
+                'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
+                'userid' => new external_value(PARAM_INT, 'user id, empty for current user', VALUE_DEFAULT, 0),
+                'status' => new external_value(PARAM_ALPHA, 'quiz status: all, finished or unfinished', VALUE_DEFAULT, 'finished'),
+                'includepreviews' => new external_value(PARAM_BOOL, 'whether to include previews or not', VALUE_DEFAULT, false),
+
+            )
+        );
+    }
+
+    /**
+     * Return a list of attempts for the given quiz and user.
+     *
+     * @param int $quizid quiz instance id
+     * @param int $userid user id
+     * @param string $status quiz status: all, finished or unfinished
+     * @param bool $includepreviews whether to include previews or not
+     * @return array of warnings and the list of attempts
+     * @since Moodle 3.1
+     * @throws invalid_parameter_exception
+     */
+    public static function get_user_attempts($quizid, $userid = 0, $status = 'finished', $includepreviews = false) {
+        global $DB, $USER;
+
+        $warnings = array();
+
+        $params = array(
+            'quizid' => $quizid,
+            'userid' => $userid,
+            'status' => $status,
+            'includepreviews' => $includepreviews,
+        );
+        $params = self::validate_parameters(self::get_user_attempts_parameters(), $params);
+
+        // Request and permission validation.
+        $quiz = $DB->get_record('quiz', array('id' => $params['quizid']), '*', MUST_EXIST);
+        list($course, $cm) = get_course_and_cm_from_instance($quiz, 'quiz');
+
+        $context = context_module::instance($cm->id);
+        self::validate_context($context);
+
+        if (!in_array($params['status'], array('all', 'finished', 'unfinished'))) {
+            throw new invalid_parameter_exception('Invalid status value');
+        }
+
+        // Default value for userid.
+        if (empty($params['userid'])) {
+            $params['userid'] = $USER->id;
+        }
+
+        $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
+        core_user::require_active_user($user);
+
+        // Extra checks so only users with permissions can view other users attempts.
+        if ($USER->id != $user->id) {
+            require_capability('mod/quiz:viewreports', $context);
+        }
+
+        $attempts = quiz_get_user_attempts($quiz->id, $user->id, $params['status'], $params['includepreviews']);
+
+        $result = array();
+        $result['attempts'] = $attempts;
+        $result['warnings'] = $warnings;
+        return $result;
+    }
+
+    /**
+     * Describes the get_user_attempts return value.
+     *
+     * @return external_single_structure
+     * @since Moodle 3.1
+     */
+    public static function get_user_attempts_returns() {
+        return new external_single_structure(
+            array(
+                'attempts' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'id' => new external_value(PARAM_INT, 'Attempt id.', VALUE_OPTIONAL),
+                            'quiz' => new external_value(PARAM_INT, 'Foreign key reference to the quiz that was attempted.',
+                                                            VALUE_OPTIONAL),
+                            'userid' => new external_value(PARAM_INT, 'Foreign key reference to the user whose attempt this is.',
+                                                            VALUE_OPTIONAL),
+                            'attempt' => new external_value(PARAM_INT, 'Sequentially numbers this students attempts at this quiz.',
+                                                            VALUE_OPTIONAL),
+                            'uniqueid' => new external_value(PARAM_INT, 'Foreign key reference to the question_usage that holds the
+                                                                details of the the question_attempts that make up this quiz
+                                                                attempt.', VALUE_OPTIONAL),
+                            'layout' => new external_value(PARAM_RAW, 'Attempt layout.', VALUE_OPTIONAL),
+                            'currentpage' => new external_value(PARAM_INT, 'Attempt current page.', VALUE_OPTIONAL),
+                            'preview' => new external_value(PARAM_INT, 'Whether is a preview attempt or not.', VALUE_OPTIONAL),
+                            'state' => new external_value(PARAM_ALPHA, 'The current state of the attempts. \'inprogress\',
+                                                            \'overdue\', \'finished\' or \'abandoned\'.', VALUE_OPTIONAL),
+                            'timestart' => new external_value(PARAM_INT, 'Time when the attempt was started.', VALUE_OPTIONAL),
+                            'timefinish' => new external_value(PARAM_INT, 'Time when the attempt was submitted.
+                                                                0 if the attempt has not been submitted yet.', VALUE_OPTIONAL),
+                            'timemodified' => new external_value(PARAM_INT, 'Last modified time.', VALUE_OPTIONAL),
+                            'timecheckstate' => new external_value(PARAM_INT, 'Next time quiz cron should check attempt for
+                                                                    state changes.  NULL means never check.', VALUE_OPTIONAL),
+                            'sumgrades' => new external_value(PARAM_FLOAT, 'Total marks for this attempt.', VALUE_OPTIONAL),
+                        )
+                    )
+                ),
+                'warnings' => new external_warnings(),
+            )
+        );
+    }
+
+    /**
+     * Describes the parameters for get_user_best_grade.
+     *
+     * @return external_external_function_parameters
+     * @since Moodle 3.1
+     */
+    public static function get_user_best_grade_parameters() {
+        return new external_function_parameters (
+            array(
+                'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
+                'userid' => new external_value(PARAM_INT, 'user id', VALUE_DEFAULT, 0),
+            )
+        );
+    }
+
+    /**
+     * Get the best current grade for the given user on a quiz.
+     *
+     * @param int $quizid quiz instance id
+     * @param int $userid user id
+     * @return array of warnings and the grade information
+     * @since Moodle 3.1
+     */
+    public static function get_user_best_grade($quizid, $userid = 0) {
+        global $DB, $USER;
+
+        $warnings = array();
+
+        $params = array(
+            'quizid' => $quizid,
+            'userid' => $userid,
+        );
+        $params = self::validate_parameters(self::get_user_best_grade_parameters(), $params);
+
+        // Request and permission validation.
+        $quiz = $DB->get_record('quiz', array('id' => $params['quizid']), '*', MUST_EXIST);
+        list($course, $cm) = get_course_and_cm_from_instance($quiz, 'quiz');
+
+        $context = context_module::instance($cm->id);
+        self::validate_context($context);
+
+        // Default value for userid.
+        if (empty($params['userid'])) {
+            $params['userid'] = $USER->id;
+        }
+
+        $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
+        core_user::require_active_user($user);
+
+        // Extra checks so only users with permissions can view other users attempts.
+        if ($USER->id != $user->id) {
+            require_capability('mod/quiz:viewreports', $context);
+        }
+
+        $result = array();
+        $grade = quiz_get_best_grade($quiz, $user->id);
+
+        if ($grade === null) {
+            $result['hasgrade'] = false;
+        } else {
+            $result['hasgrade'] = true;
+            $result['grade'] = $grade;
+        }
+        $result['warnings'] = $warnings;
+        return $result;
+    }
+
+    /**
+     * Describes the get_user_best_grade return value.
+     *
+     * @return external_single_structure
+     * @since Moodle 3.1
+     */
+    public static function get_user_best_grade_returns() {
+        return new external_single_structure(
+            array(
+                'hasgrade' => new external_value(PARAM_BOOL, 'Whether the user has a grade on the given quiz.'),
+                'grade' => new external_value(PARAM_FLOAT, 'The grade (only if the user has a grade).', VALUE_OPTIONAL),
+                'warnings' => new external_warnings(),
+            )
+        );
+    }
+
+    /**
+     * Describes the parameters for get_combined_review_options.
+     *
+     * @return external_external_function_parameters
+     * @since Moodle 3.1
+     */
+    public static function get_combined_review_options_parameters() {
+        return new external_function_parameters (
+            array(
+                'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
+                'userid' => new external_value(PARAM_INT, 'user id (empty for current user)', VALUE_DEFAULT, 0),
+
+            )
+        );
+    }
+
+    /**
+     * Combines the review options from a number of different quiz attempts.
+     *
+     * @param int $quizid quiz instance id
+     * @param int $userid user id (empty for current user)
+     * @return array of warnings and the review options
+     * @since Moodle 3.1
+     */
+    public static function get_combined_review_options($quizid, $userid = 0) {
+        global $DB, $USER;
+
+        $warnings = array();
+
+        $params = array(
+            'quizid' => $quizid,
+            'userid' => $userid,
+        );
+        $params = self::validate_parameters(self::get_combined_review_options_parameters(), $params);
+
+        // Request and permission validation.
+        $quiz = $DB->get_record('quiz', array('id' => $params['quizid']), '*', MUST_EXIST);
+        list($course, $cm) = get_course_and_cm_from_instance($quiz, 'quiz');
+
+        $context = context_module::instance($cm->id);
+        self::validate_context($context);
+
+        // Default value for userid.
+        if (empty($params['userid'])) {
+            $params['userid'] = $USER->id;
+        }
+
+        $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
+        core_user::require_active_user($user);
+
+        // Extra checks so only users with permissions can view other users attempts.
+        if ($USER->id != $user->id) {
+            require_capability('mod/quiz:viewreports', $context);
+        }
+
+        $attempts = quiz_get_user_attempts($quiz->id, $user->id, 'all', true);
+
+        $result = array();
+        $result['someoptions'] = [];
+        $result['alloptions'] = [];
+
+        list($someoptions, $alloptions) = quiz_get_combined_reviewoptions($quiz, $attempts);
+
+        foreach (array('someoptions', 'alloptions') as $typeofoption) {
+            foreach ($$typeofoption as $key => $value) {
+                $result[$typeofoption][] = array(
+                    "name" => $key,
+                    "value" => (!empty($value)) ? $value : 0
+                );
+            }
+        }
+
+        $result['warnings'] = $warnings;
+        return $result;
+    }
+
+    /**
+     * Describes the get_combined_review_options return value.
+     *
+     * @return external_single_structure
+     * @since Moodle 3.1
+     */
+    public static function get_combined_review_options_returns() {
+        return new external_single_structure(
+            array(
+                'someoptions' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'name' => new external_value(PARAM_ALPHANUMEXT, 'option name'),
+                            'value' => new external_value(PARAM_INT, 'option value'),
+                        )
+                    )
+                ),
+                'alloptions' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'name' => new external_value(PARAM_ALPHANUMEXT, 'option name'),
+                            'value' => new external_value(PARAM_INT, 'option value'),
+                        )
+                    )
+                ),
+                'warnings' => new external_warnings(),
+            )
+        );
+    }
+
 }
index cf5adbd..ff73ee6 100644 (file)
@@ -34,7 +34,8 @@ $functions = array(
         'description'   => 'Returns a list of quizzes in a provided list of courses,
                             if no list is provided all quizzes that the user can view will be returned.',
         'type'          => 'read',
-        'capabilities'  => 'mod/quiz:view'
+        'capabilities'  => 'mod/quiz:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_quiz_view_quiz' => array(
@@ -42,6 +43,34 @@ $functions = array(
         'methodname'    => 'view_quiz',
         'description'   => 'Trigger the course module viewed event and update the module completion status.',
         'type'          => 'write',
-        'capabilities'  => 'mod/quiz:view'
+        'capabilities'  => 'mod/quiz:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+    ),
+
+    'mod_quiz_get_user_attempts' => array(
+        'classname'     => 'mod_quiz_external',
+        'methodname'    => 'get_user_attempts',
+        'description'   => 'Return a list of attempts for the given quiz and user.',
+        'type'          => 'read',
+        'capabilities'  => 'mod/quiz:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+    ),
+
+    'mod_quiz_get_user_best_grade' => array(
+        'classname'     => 'mod_quiz_external',
+        'methodname'    => 'get_user_best_grade',
+        'description'   => 'Get the best current grade for the given user on a quiz.',
+        'type'          => 'read',
+        'capabilities'  => 'mod/quiz:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+    ),
+
+    'mod_quiz_get_combined_review_options' => array(
+        'classname'     => 'mod_quiz_external',
+        'methodname'    => 'get_combined_review_options',
+        'description'   => 'Combines the review options from a number of different quiz attempts.',
+        'type'          => 'read',
+        'capabilities'  => 'mod/quiz:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 );
index e1d2f76..59add9f 100644 (file)
@@ -1435,6 +1435,11 @@ function quiz_get_combined_reviewoptions($quiz, $attempts) {
     $someoptions->marks = question_display_options::HIDDEN;
     $alloptions->marks = question_display_options::MARK_AND_MAX;
 
+    // This shouldn't happen, but we need to prevent reveal information.
+    if (empty($attempts)) {
+        return array($someoptions, $someoptions);
+    }
+
     foreach ($attempts as $attempt) {
         $attemptoptions = mod_quiz_display_options::make_from_quiz($quiz,
                 quiz_attempt_state($quiz, $attempt));
index 5cd2645..5b66531 100644 (file)
@@ -268,4 +268,321 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase {
 
     }
 
+    /**
+     * Test get_user_attempts
+     */
+    public function test_get_user_attempts() {
+
+        // Create a new quiz with attempts.
+        $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
+        $data = array('course' => $this->course->id,
+                      'sumgrades' => 1);
+        $quiz = $quizgenerator->create_instance($data);
+
+        // Create a couple of questions.
+        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
+
+        $cat = $questiongenerator->create_question_category();
+        $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
+        quiz_add_quiz_question($question->id, $quiz);
+
+        $quizobj = quiz::create($quiz->id, $this->student->id);
+
+        // Set grade to pass.
+        $item = grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod',
+                                        'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null));
+        $item->gradepass = 80;
+        $item->update();
+
+        // Start the passing attempt.
+        $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
+        $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
+
+        $timenow = time();
+        $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $this->student->id);
+        quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
+        quiz_attempt_save_started($quizobj, $quba, $attempt);
+
+        // Process some responses from the student.
+        $attemptobj = quiz_attempt::create($attempt->id);
+        $tosubmit = array(1 => array('answer' => '3.14'));
+        $attemptobj->process_submitted_actions($timenow, false, $tosubmit);
+
+        // Finish the attempt.
+        $attemptobj = quiz_attempt::create($attempt->id);
+        $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
+        $attemptobj->process_finish($timenow, false);
+
+        $this->setUser($this->student);
+        $result = mod_quiz_external::get_user_attempts($quiz->id);
+        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
+
+        $this->assertCount(1, $result['attempts']);
+        $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
+        $this->assertEquals($quiz->id, $result['attempts'][0]['quiz']);
+        $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
+        $this->assertEquals(1, $result['attempts'][0]['attempt']);
+
+        // Test filters. Only finished.
+        $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'finished', false);
+        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
+
+        $this->assertCount(1, $result['attempts']);
+        $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
+
+        // Test filters. All attempts.
+        $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'all', false);
+        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
+
+        $this->assertCount(1, $result['attempts']);
+        $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
+
+        // Test filters. Unfinished.
+        $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'unfinished', false);
+        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
+
+        $this->assertCount(0, $result['attempts']);
+
+        // Start a new attempt, but not finish it.
+        $timenow = time();
+        $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id);
+        $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
+        $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
+
+        quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
+        quiz_attempt_save_started($quizobj, $quba, $attempt);
+
+        // Test filters. All attempts.
+        $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'all', false);
+        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
+
+        $this->assertCount(2, $result['attempts']);
+
+        // Test filters. Unfinished.
+        $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'unfinished', false);
+        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
+
+        $this->assertCount(1, $result['attempts']);
+
+        // Test manager can see user attempts.
+        $this->setUser($this->teacher);
+        $result = mod_quiz_external::get_user_attempts($quiz->id, $this->student->id);
+        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
+
+        $this->assertCount(1, $result['attempts']);
+        $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
+
+        $result = mod_quiz_external::get_user_attempts($quiz->id, $this->student->id, 'all');
+        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
+
+        $this->assertCount(2, $result['attempts']);
+        $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
+
+        // Invalid parameters.
+        try {
+            mod_quiz_external::get_user_attempts($quiz->id, $this->student->id, 'INVALID_PARAMETER');
+            $this->fail('Exception expected due to missing capability.');
+        } catch (invalid_parameter_exception $e) {
+            $this->assertEquals('invalidparameter', $e->errorcode);
+        }
+    }
+
+    /**
+     * Test get_user_best_grade
+     */
+    public function test_get_user_best_grade() {
+        global $DB;
+
+        $this->setUser($this->student);
+
+        $result = mod_quiz_external::get_user_best_grade($this->quiz->id);
+        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result);
+
+        // No grades yet.
+        $this->assertFalse($result['hasgrade']);
+        $this->assertTrue(!isset($result['grade']));
+
+        $grade = new stdClass();
+        $grade->quiz = $this->quiz->id;
+        $grade->userid = $this->student->id;
+        $grade->grade = 8.9;
+        $grade->timemodified = time();
+        $grade->id = $DB->insert_record('quiz_grades', $grade);
+
+        $result = mod_quiz_external::get_user_best_grade($this->quiz->id);
+        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result);
+
+        // Now I have grades.
+        $this->assertTrue($result['hasgrade']);
+        $this->assertEquals(8.9, $result['grade']);
+
+        // We should not see other users grades.
+        $anotherstudent = self::getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($anotherstudent->id, $this->course->id, $this->studentrole->id, 'manual');
+
+        try {
+            mod_quiz_external::get_user_best_grade($this->quiz->id, $anotherstudent->id);
+            $this->fail('Exception expected due to missing capability.');
+        } catch (required_capability_exception $e) {
+            $this->assertEquals('nopermissions', $e->errorcode);
+        }
+
+        // Teacher must be able to see student grades.
+        $this->setUser($this->teacher);
+
+        $result = mod_quiz_external::get_user_best_grade($this->quiz->id, $this->student->id);
+        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result);
+
+        $this->assertTrue($result['hasgrade']);
+        $this->assertEquals(8.9, $result['grade']);
+
+        // Invalid user.
+        try {
+            mod_quiz_external::get_user_best_grade($this->quiz->id, -1);
+            $this->fail('Exception expected due to missing capability.');
+        } catch (dml_missing_record_exception $e) {
+            $this->assertEquals('invaliduser', $e->errorcode);
+        }
+
+        // Remove the created data.
+        $DB->delete_records('quiz_grades', array('id' => $grade->id));
+
+    }
+    /**
+     * Test get_combined_review_options.
+     * This is a basic test, this is already tested in mod_quiz_display_options_testcase.
+     */
+    public function test_get_combined_review_options() {
+        global $DB;
+
+        // Create a new quiz with attempts.
+        $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
+        $data = array('course' => $this->course->id,
+                      'sumgrades' => 1);
+        $quiz = $quizgenerator->create_instance($data);
+
+        // Create a couple of questions.
+        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
+
+        $cat = $questiongenerator->create_question_category();
+        $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
+        quiz_add_quiz_question($question->id, $quiz);
+
+        $quizobj = quiz::create($quiz->id, $this->student->id);
+
+        // Set grade to pass.
+        $item = grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod',
+                                        'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null));
+        $item->gradepass = 80;
+        $item->update();
+
+        // Start the passing attempt.
+        $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
+        $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
+
+        $timenow = time();
+        $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $this->student->id);
+        quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
+        quiz_attempt_save_started($quizobj, $quba, $attempt);
+
+        $this->setUser($this->student);
+
+        $result = mod_quiz_external::get_combined_review_options($quiz->id);
+        $result = external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result);
+
+        // Expected values.
+        $expected = array(
+            "someoptions" => array(
+                array("name" => "feedback", "value" => 1),
+                array("name" => "generalfeedback", "value" => 1),
+                array("name" => "rightanswer", "value" => 1),
+                array("name" => "overallfeedback", "value" => 0),
+                array("name" => "marks", "value" => 2),
+            ),
+            "alloptions" => array(
+                array("name" => "feedback", "value" => 1),
+                array("name" => "generalfeedback", "value" => 1),
+                array("name" => "rightanswer", "value" => 1),
+                array("name" => "overallfeedback", "value" => 0),
+                array("name" => "marks", "value" => 2),
+            ),
+            "warnings" => [],
+        );
+
+        $this->assertEquals($expected, $result);
+
+        // Now, finish the attempt.
+        $attemptobj = quiz_attempt::create($attempt->id);
+        $attemptobj->process_finish($timenow, false);
+
+        $expected = array(
+            "someoptions" => array(
+                array("name" => "feedback", "value" => 1),
+                array("name" => "generalfeedback", "value" => 1),
+                array("name" => "rightanswer", "value" => 1),
+                array("name" => "overallfeedback", "value" => 1),
+                array("name" => "marks", "value" => 2),
+            ),
+            "alloptions" => array(
+                array("name" => "feedback", "value" => 1),
+                array("name" => "generalfeedback", "value" => 1),
+                array("name" => "rightanswer", "value" => 1),
+                array("name" => "overallfeedback", "value" => 1),
+                array("name" => "marks", "value" => 2),
+            ),
+            "warnings" => [],
+        );
+
+        // We should see now the overall feedback.
+        $result = mod_quiz_external::get_combined_review_options($quiz->id);
+        $result = external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result);
+        $this->assertEquals($expected, $result);
+
+        // Start a new attempt, but not finish it.
+        $timenow = time();
+        $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id);
+        $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
+        $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
+        quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
+        quiz_attempt_save_started($quizobj, $quba, $attempt);
+
+        $expected = array(
+            "someoptions" => array(
+                array("name" => "feedback", "value" => 1),
+                array("name" => "generalfeedback", "value" => 1),
+                array("name" => "rightanswer", "value" => 1),
+                array("name" => "overallfeedback", "value" => 1),
+                array("name" => "marks", "value" => 2),
+            ),
+            "alloptions" => array(
+                array("name" => "feedback", "value" => 1),
+                array("name" => "generalfeedback", "value" => 1),
+                array("name" => "rightanswer", "value" => 1),
+                array("name" => "overallfeedback", "value" => 0),
+                array("name" => "marks", "value" => 2),
+            ),
+            "warnings" => [],
+        );
+
+        $result = mod_quiz_external::get_combined_review_options($quiz->id);
+        $result = external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result);
+        $this->assertEquals($expected, $result);
+
+        // Teacher, for see student options.
+        $this->setUser($this->teacher);
+
+        $result = mod_quiz_external::get_combined_review_options($quiz->id, $this->student->id);
+        $result = external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result);
+
+        $this->assertEquals($expected, $result);
+
+        // Invalid user.
+        try {
+            mod_quiz_external::get_combined_review_options($quiz->id, -1);
+            $this->fail('Exception expected due to missing capability.');
+        } catch (dml_missing_record_exception $e) {
+            $this->assertEquals('invaliduser', $e->errorcode);
+        }
+    }
+
 }
index 70a0e4b..a9fedb3 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015111602;
+$plugin->version   = 2015111606;
 $plugin->requires  = 2015111000;
 $plugin->component = 'mod_quiz';
 $plugin->cron      = 60;
index 7699540..d5f5658 100644 (file)
@@ -33,7 +33,8 @@ $functions = array(
         'methodname'    => 'view_resource',
         'description'   => 'Simulate the view.php web interface resource: trigger events, completion, etc...',
         'type'          => 'write',
-        'capabilities'  => 'mod/resource:view'
+        'capabilities'  => 'mod/resource:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
 );
index c87b79c..fc7514e 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015111600;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2015111601;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2015111000;    // Requires this Moodle version
 $plugin->component = 'mod_resource'; // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;
index ac85f70..d767872 100644 (file)
@@ -31,7 +31,8 @@ $functions = array(
         'methodname'    => 'view_scorm',
         'description'   => 'Trigger the course module viewed event.',
         'type'          => 'write',
-        'capabilities'  => ''
+        'capabilities'  => '',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_scorm_get_scorm_attempt_count' => array(
@@ -39,7 +40,8 @@ $functions = array(
         'methodname'    => 'get_scorm_attempt_count',
         'description'   => 'Return the number of attempts done by a user in the given SCORM.',
         'type'          => 'read',
-        'capabilities'  => ''
+        'capabilities'  => '',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_scorm_get_scorm_scoes' => array(
@@ -47,7 +49,8 @@ $functions = array(
         'methodname' => 'get_scorm_scoes',
         'description' => 'Returns a list containing all the scoes data related to the given scorm id',
         'type' => 'read',
-        'capabilities' => ''
+        'capabilities' => '',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_scorm_get_scorm_user_data' => array(
@@ -55,7 +58,8 @@ $functions = array(
         'methodname' => 'get_scorm_user_data',
         'description' => 'Retrieves user tracking and SCO data and default SCORM values',
         'type' => 'read',
-        'capabilities' => ''
+        'capabilities' => '',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_scorm_insert_scorm_tracks' => array(
@@ -66,7 +70,8 @@ $functions = array(
                           Validation should be performed before running the function to ensure the user will not lose any existing
                           attempt data.',
         'type' => 'write',
-        'capabilities' => 'mod/scorm:savetrack'
+        'capabilities' => 'mod/scorm:savetrack',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_scorm_get_scorm_sco_tracks' => array(
@@ -74,7 +79,8 @@ $functions = array(
         'methodname' => 'get_scorm_sco_tracks',
         'description' => 'Retrieves SCO tracking data for the given user id and attempt number',
         'type' => 'read',
-        'capabilities' => ''
+        'capabilities' => '',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),