Merge branch 'MDL-52786-master' of git://github.com/jleyva/moodle
authorDavid Monllao <davidm@moodle.com>
Wed, 9 Mar 2016 07:29:02 +0000 (15:29 +0800)
committerDavid Monllao <davidm@moodle.com>
Wed, 9 Mar 2016 07:29:02 +0000 (15:29 +0800)
Conflicts:
lib/db/services.php

119 files changed:
admin/roles/classes/define_role_table_advanced.php
admin/roles/define.php
admin/tool/monitor/lib.php
blocks/community/communitycourse.php
calendar/externallib.php
calendar/tests/externallib_test.php
calendar/view.php
course/externallib.php
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
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/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/coursecatlib.php
lib/datalib.php
lib/db/install.xml
lib/db/services.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/tests/coursecatlib_test.php
lib/tests/htmlpurifier_test.php
lib/tests/text_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/db/services.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/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..2904832 100644 (file)
@@ -103,10 +103,13 @@ 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)));
+            if ($course->visible === true || has_capability('moodle/course:viewhiddencourses',
+                    context_course::instance($course->id))) {
+                $options[$course->id] = format_string($course->fullname, true,
+                    array('context' => context_course::instance($course->id)));
+            }
         }
     }
     // If there are no courses and there is no site permission then return false.
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 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 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..f6f5d3e 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..baf9fbf 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);
@@ -747,11 +755,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;
+        }
+    };
+});
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 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 234f0d9..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,72 +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_get_combined_review_options',
-            'mod_quiz_get_user_attempts',
-            'mod_quiz_view_quiz',
-            'mod_quiz_get_user_best_grade',
-            '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 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 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 d23f702..d9192e6 100644 (file)
@@ -3,6 +3,11 @@ information provided here is intended especially for developers.
 
 === 3.1 ===
 
+* 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 +75,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 f7977b3..9fd4d75 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,7 +43,8 @@ $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(
@@ -50,7 +52,8 @@ $functions = array(
         'methodname'    => 'get_user_attempts',
         'description'   => 'Return a list of attempts for the given quiz and user.',
         'type'          => 'read',
-        'capabilities'  => 'mod/quiz:view'
+        'capabilities'  => 'mod/quiz:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_quiz_get_user_best_grade' => array(
@@ -58,7 +61,8 @@ $functions = array(
         'methodname'    => 'get_user_best_grade',
         'description'   => 'Get the best current grade for the given user on a quiz.',
         'type'          => 'read',
-        'capabilities'  => 'mod/quiz:view'
+        'capabilities'  => 'mod/quiz:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_quiz_get_combined_review_options' => array(
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)
     ),
 
     'mod_scorm_get_scorms_by_courses' => array(
@@ -83,7 +89,8 @@ $functions = array(
         'description'   => 'Returns a list of scorm instances in a provided set of courses, if
                             no courses are provided then all the scorm instances the user has access to will be returned.',
         'type'          => 'read',
-        'capabilities'  => ''
+        'capabilities'  => '',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_scorm_launch_sco' => array(
@@ -91,6 +98,7 @@ $functions = array(
         'methodname'    => 'launch_sco',
         'description'   => 'Trigger the SCO launched event.',
         'type'          => 'write',
-        'capabilities'  => ''
+        'capabilities'  => '',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 );
index fee8607..67d6043 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2016021000;    // The current module version (Date: YYYYMMDDXX).
+$plugin->version   = 2016021001;    // The current module version (Date: YYYYMMDDXX).
 $plugin->requires  = 2015111000;    // Requires this Moodle version.
 $plugin->component = 'mod_scorm';   // Full name of the plugin (used for diagnostics).
 $plugin->cron      = 300;
index ccc16b2..7ea4d6e 100644 (file)
@@ -34,7 +34,8 @@ $functions = array(
         'description'   => 'Returns a list of survey instances in a provided set of courses,
                             if no courses are provided then all the survey instances the user has access to will be returned.',
         'type'          => 'read',
-        'capabilities'  => ''
+        'capabilities'  => '',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_survey_view_survey' => array(
@@ -42,7 +43,8 @@ $functions = array(
         'methodname'    => 'view_survey',
         'description'   => 'Trigger the course module viewed event and update the module completion status.',
         'type'          => 'write',
-        'capabilities'  => 'mod/survey:participate'
+        'capabilities'  => 'mod/survey:participate',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_survey_get_questions' => array(
@@ -50,7 +52,8 @@ $functions = array(
         'methodname'    => 'get_questions',
         'description'   => 'Get the complete list of questions for the survey, including subquestions.',
         'type'          => 'read',
-        'capabilities'  => 'mod/survey:participate'
+        'capabilities'  => 'mod/survey:participate',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_survey_submit_answers' => array(
@@ -58,7 +61,8 @@ $functions = array(
         'methodname'    => 'submit_answers',
         'description'   => 'Submit the answers for a given survey.',
         'type'          => 'write',
-        'capabilities'  => 'mod/survey:participate'
+        'capabilities'  => 'mod/survey:participate',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
 );
index 2e15883..c3a6aa8 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_survey';     // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;
index ec2d55a..0481d94 100644 (file)
@@ -33,7 +33,8 @@ $functions = array(
         'methodname'    => 'view_url',
         'description'   => 'Trigger the course module viewed event and update the module completion status.',
         'type'          => 'write',
-        'capabilities'  => 'mod/url:view'
+        'capabilities'  => 'mod/url:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
 );
index 8db3995..5040cf6 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_url';        // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;
index e0ff470..76aa5ea 100644 (file)
@@ -32,7 +32,8 @@ $functions = array(
         'description'   => 'Returns a list of wiki instances in a provided set of courses, if ' .
                            'no courses are provided then all the wiki instances the user has access to will be returned.',
         'type'          => 'read',
-        'capabilities'  => 'mod/wiki:viewpage'
+        'capabilities'  => 'mod/wiki:viewpage',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_wiki_view_wiki' => array(
@@ -40,7 +41,8 @@ $functions = array(
         'methodname'    => 'view_wiki',
         'description'   => 'Trigger the course module viewed event and update the module completion status.',
         'type'          => 'write',
-        'capabilities'  => 'mod/wiki:viewpage'
+        'capabilities'  => 'mod/wiki:viewpage',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_wiki_view_page' => array(
@@ -48,7 +50,8 @@ $functions = array(
         'methodname'    => 'view_page',
         'description'   => 'Trigger the page viewed event and update the module completion status.',
         'type'          => 'write',
-        'capabilities'  => 'mod/wiki:viewpage'
+        'capabilities'  => 'mod/wiki:viewpage',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
     'mod_wiki_get_subwikis' => array(
@@ -56,6 +59,7 @@ $functions = array(
         'methodname'    => 'get_subwikis',
         'description'   => 'Returns the list of subwikis the user can see in a specific wiki.',
         'type'          => 'read',
-        'capabilities'  => 'mod/wiki:viewpage'
+        'capabilities'  => 'mod/wiki:viewpage',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     )
 );
index dc729d4..b7707ef 100644 (file)
@@ -33,7 +33,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2016011101;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2016011102;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2015111000;    // Requires this Moodle version
 $plugin->component = 'mod_wiki';       // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;
index 0111c6a..7de80b8 100644 (file)
@@ -29,6 +29,9 @@
 .que.ddmarker div.ddarea, form.mform fieldset#id_previewareaheader div.ddarea {
     text-align: center;
 }
+form.mform fieldset#id_previewareaheader div.ddarea .markertexts {
+    min-height: 80px;
+}
 .que.ddmarker .dropbackground, form.mform fieldset#id_previewareaheader .dropbackground {
     margin: 0 auto;
 }
index 41b9ad1..168c75d 100644 (file)
Binary files a/question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd-debug.js and b/question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd-debug.js differ
index f827d75..50ec94e 100644 (file)
Binary files a/question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd-min.js and b/question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd-min.js differ
index 41b9ad1..168c75d 100644 (file)
Binary files a/question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd.js and b/question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd.js differ
index 5fabc79..6644471 100644 (file)
@@ -44,7 +44,7 @@ Y.extend(DDWTOS_DD, Y.Base, {
         if (!this.get('readonly')) {
             Y.later(500, this, this.position_drag_items, [pendingid, true]);
         } else {
-            Y.later(500, this, this.position_drag_items, [pendingid]);
+            Y.later(500, this, this.position_drag_items, [pendingid, 3]);
             Y.one('window').on('resize', function() {
                 this.position_drag_items(pendingid);
             }, this);
@@ -303,11 +303,22 @@ Y.extend(DDWTOS_DD, Y.Base, {
     remove_drag_from_drop : function (drop) {
         this.place_drag_in_drop(null, drop);
     },
+
+    /**
+     * Postition, or reposition, all the drag items.
+     * @param pendingid (optional) if given, then mark the js task complete after the
+     * items are all positioned.
+     * @param dotimeout (optional) if true, continually re-position the items so
+     * they stay in place. Else, if an integer, reposition this many times before stopping.
+     */
     position_drag_items : function (pendingid, dotimeout) {
        Y.all(this.selectors.drags()).each(this.position_drag_item, this);
        M.util.js_complete(pendingid);
-       if (dotimeout) {
-           Y.later(500, this, this.position_drag_items, [pendingid, true]);
+       if (dotimeout === true || dotimeout > 0) {
+           if (dotimeout !== true) {
+               dotimeout -= 1;
+           }
+           Y.later(500, this, this.position_drag_items, [pendingid, dotimeout]);
        }
     },
     position_drag_item : function (drag) {
index 5da3d20..64b435c 100644 (file)
@@ -37,8 +37,6 @@ echo $OUTPUT->heading($pagetitle);
 if (\core_search\manager::is_global_search_enabled() === false) {
     $renderer = $PAGE->get_renderer('core_search');
     echo $renderer->render_search_disabled();
-    echo $OUTPUT->footer();
-    exit;
 }
 
 $renderer = $PAGE->get_renderer('report_search');
index da8b941..b657d29 100644 (file)
@@ -67,17 +67,21 @@ $repo->callback();
 // If Moodle is working on HTTPS mode, then we are not allowed to access
 // parent window, in this case, we need to alert user to refresh the repository
 // manually.
-$strhttpsbug = get_string('cannotaccessparentwin', 'repository');
+$strhttpsbug = json_encode(get_string('cannotaccessparentwin', 'repository'));
 $strrefreshnonjs = get_string('refreshnonjsfilepicker', 'repository');
 $js =<<<EOD
 <html>
 <head>
     <script type="text/javascript">
-    if(window.opener){
-        window.opener.M.core_filepicker.active_filepicker.list();
-        window.close();
-    } else {
-        alert("{$strhttpsbug }");
+    try {
+        if (window.opener) {
+            window.opener.M.core_filepicker.active_filepicker.list();
+            window.close();
+        } else {
+            throw new Error('Whoops!');
+        }
+    } catch (e) {
+        alert({$strhttpsbug});
     }
     </script>
 </head>
index bd77769..9711196 100644 (file)
@@ -229,6 +229,56 @@ abstract class engine {
         return $classname;
     }
 
+    /**
+     * Run any pre-indexing operations.
+     *
+     * Should be overwritten if the search engine needs to do any pre index preparation.
+     *
+     * @param bool $fullindex True if a full index will be performed
+     * @return void
+     */
+    public function index_starting($fullindex = false) {
+        // Nothing by default.
+    }
+
+    /**
+     * Run any post indexing operations.
+     *
+     * Should be overwritten if the search engine needs to do any post index cleanup.
+     *
+     * @param int $numdocs The number of documents that were added to the index
+     * @param bool $fullindex True if a full index was performed
+     * @return void
+     */
+    public function index_complete($numdocs = 0, $fullindex = false) {
+        // Nothing by default.
+    }
+
+    /**
+     * Do anything that may need to be done before an area is indexed.
+     *
+     * @param \core_search\area\base $searcharea The search area that was complete
+     * @param bool $fullindex True if a full index is being performed
+     * @return void
+     */
+    public function area_index_starting($searcharea, $fullindex = false) {
+        // Nothing by default.
+    }
+
+    /**
+     * Do any area cleanup needed, and do anything to confirm contents.
+     *
+     * Return false to prevent the search area completed time and stats from being updated.
+     *
+     * @param \core_search\area\base $searcharea The search area that was complete
+     * @param int $numdocs The number of documents that were added to the index
+     * @param bool $fullindex True if a full index is being performed
+     * @return bool True means that data is considered indexed
+     */
+    public function area_index_complete($searcharea, $numdocs = 0, $fullindex = false) {
+        return true;
+    }
+
     /**
      * Optimizes the search engine.
      *
@@ -289,13 +339,6 @@ abstract class engine {
      */
     abstract function add_document($doc);
 
-    /**
-     * Commits changes to the server.
-     *
-     * @return void
-     */
-    abstract function commit();
-
     /**
      * Executes the query on the engine.
      *
index 3f8f7da..64863b7 100644 (file)
@@ -99,10 +99,6 @@ class manager {
     /**
      * Returns an initialised \core_search instance.
      *
-     * It requires global search to be enabled. Use \core_search\manager::is_global_search_enabled
-     * to verify it is enabled.
-     *
-     * @throws \moodle_exception
      * @throws \core_search\engine_exception
      * @return \core_search\manager
      */
@@ -114,10 +110,6 @@ class manager {
             return static::$instance;
         }
 
-        if (!static::is_global_search_enabled()) {
-            throw new \moodle_exception('globalsearchdisabled', 'search');
-        }
-
         if (!$engine = static::search_engine_instance()) {
             throw new \core_search\engine_exception('enginenotfound', 'search', '', $CFG->searchengine);
         }
@@ -474,7 +466,10 @@ class manager {
         // Unlimited time.
         \core_php_time_limit::raise();
 
-        $anyupdate = false;
+        // Notify the engine that an index starting.
+        $this->engine->index_starting($fullindex);
+
+        $sumdocs = 0;
 
         $searchareas = $this->get_search_areas_list(true);
         foreach ($searchareas as $areaid => $searcharea) {
@@ -483,6 +478,9 @@ class manager {
                 mtrace('Processing ' . $searcharea->get_visible_name() . ' area');
             }
 
+            // Notify the engine that an area is starting.
+            $this->engine->area_index_starting($searcharea, $fullindex);
+
             $indexingstart = time();
 
             // This is used to store this component config.
@@ -526,38 +524,40 @@ class manager {
                 $numrecords++;
             }
 
-            if ($numdocs > 0) {
-                $anyupdate = true;
-
-                // Commit all remaining documents.
-                $this->engine->commit();
-
-                if (CLI_SCRIPT && !PHPUNIT_TEST) {
+            if (CLI_SCRIPT && !PHPUNIT_TEST) {
+                if ($numdocs > 0) {
                     mtrace('Processed ' . $numrecords . ' records containing ' . $numdocs . ' documents for ' .
-                        $searcharea->get_visible_name() . ' area. Commits completed.');
+                            $searcharea->get_visible_name() . ' area.');
+                } else  {
+                    mtrace('No new documents to index for ' . $searcharea->get_visible_name() . ' area.');
                 }
-            } else if (CLI_SCRIPT && !PHPUNIT_TEST) {
-                mtrace('No new documents to index for ' . $searcharea->get_visible_name() . ' area.');
             }
 
-            // Store last index run once documents have been commited to the search engine.
-            set_config($varname . '_indexingstart', $indexingstart, $componentconfigname);
-            set_config($varname . '_indexingend', time(), $componentconfigname);
-            set_config($varname . '_docsignored', $numdocsignored, $componentconfigname);
-            set_config($varname . '_docsprocessed', $numdocs, $componentconfigname);
-            set_config($varname . '_recordsprocessed', $numrecords, $componentconfigname);
-            if ($lastindexeddoc > 0) {
-                set_config($varname . '_lastindexrun', $lastindexeddoc, $componentconfigname);
+            // Notify the engine this area is complete, and only mark times if true.
+            if ($this->engine->area_index_complete($searcharea, $numdocs, $fullindex)) {
+                $sumdocs += $numdocs;
+
+                // Store last index run once documents have been commited to the search engine.
+                set_config($varname . '_indexingstart', $indexingstart, $componentconfigname);
+                set_config($varname . '_indexingend', time(), $componentconfigname);
+                set_config($varname . '_docsignored', $numdocsignored, $componentconfigname);
+                set_config($varname . '_docsprocessed', $numdocs, $componentconfigname);
+                set_config($varname . '_recordsprocessed', $numrecords, $componentconfigname);
+                if ($lastindexeddoc > 0) {
+                    set_config($varname . '_lastindexrun', $lastindexeddoc, $componentconfigname);
+                }
             }
         }
 
-        if ($anyupdate) {
+        if ($sumdocs > 0) {
             $event = \core\event\search_indexed::create(
                     array('context' => \context_system::instance()));
             $event->trigger();
         }
 
-        return $anyupdate;
+        $this->engine->index_complete($sumdocs, $fullindex);
+
+        return (bool)$sumdocs;
     }
 
     /**
@@ -606,7 +606,6 @@ class manager {
             $this->engine->delete();
             $this->reset_config();
         }
-        $this->engine->commit();
     }
 
     /**
@@ -616,7 +615,6 @@ class manager {
      */
     public function delete_index_by_id($id) {
         $this->engine->delete_by_id($id);
-        $this->engine->commit();
     }
 
     /**
diff --git a/search/cli/indexer.php b/search/cli/indexer.php
new file mode 100644 (file)
index 0000000..f42b8e9
--- /dev/null
@@ -0,0 +1,83 @@
+<?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/>.
+
+/**
+ * CLI search indexer
+ *
+ * @package    search
+ * @copyright  2016 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', true);
+
+require(__DIR__.'/../../config.php');
+require_once($CFG->libdir.'/clilib.php');      // cli only functions
+
+list($options, $unrecognized) = cli_get_params(array('help' => false, 'force' => false, 'reindex' => false),
+                                               array('h' => 'help', 'f' => 'force', 'r' => 'reindex'));
+
+if ($unrecognized) {
+    $unrecognized = implode("\n  ", $unrecognized);
+    cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
+}
+
+if ($options['help']) {
+    $help =
+"Index search data
+
+Options:
+-h, --help            Print out this help
+-r, --reindex         Reindex data
+-f, --force           Allow indexer to run, even if global search is disabled.
+
+Example:
+\$ sudo -u www-data /usr/bin/php search/cli/indexer.php --reindex
+";
+
+    echo $help;
+    die;
+}
+
+if (!\core_search\manager::is_global_search_enabled() && empty($options['force'])) {
+    cli_error('Global search is disabled. Use --force if you want to force an index while disabled');
+}
+
+if (!$searchengine = \core_search\manager::search_engine_instance()) {
+    cli_error(get_string('engineserverstatus', 'search'));
+}
+if (!$searchengine->is_installed()) {
+    cli_error('enginenotinstalled', 'search', $CFG->searchengine);
+}
+$serverstatus = $searchengine->is_server_ready();
+if ($serverstatus !== true) {
+    cli_error($serverstatus);
+}
+
+$globalsearch = \core_search\manager::instance();
+
+if (empty($options['reindex'])) {
+    echo "Running full index of site\n";
+    echo "==========================\n";
+    $globalsearch->index();
+} else {
+    echo "Running full reindex of site\n";
+    echo "============================\n";
+    $globalsearch->index(true);
+}
+
+// Optimize index at last.
+$globalsearch->optimize_index();
index c4ef6a8..20c1f3e 100644 (file)
@@ -53,7 +53,7 @@ class document extends \core_search\document {
     public static function format_string_for_engine($string) {
         // 2^15 default. We could convert this to a setting as is possible to
         // change the max in solr.
-        return substr($string, 0, 32766);
+        return \core_text::str_max_bytes($string, 32766);
     }
 
     /**
index 1e45829..e18b31e 100644 (file)
@@ -309,6 +309,10 @@ class engine extends \core_search\engine {
             $result = $this->get_search_client()->addDocument($solrdoc, true, static::AUTOCOMMIT_WITHIN);
         } catch (\SolrClientException $e) {
             debugging('Solr client error adding document with id ' . $doc['id'] . ': ' . $e->getMessage(), DEBUG_DEVELOPER);
+        } catch (\SolrServerException $e) {
+            // We only use the first line of the message, as it's a fully java stacktrace behind it.
+            $msg = strtok($e->getMessage(), "\n");
+            debugging('Solr server error adding document with id ' . $doc['id'] . ': ' . $msg, DEBUG_DEVELOPER);
         }
     }
 
@@ -317,17 +321,33 @@ class engine extends \core_search\engine {
      *
      * @return void
      */
-    public function commit() {
+    protected function commit() {
         $this->get_search_client()->commit();
     }
 
+    /**
+     * Do any area cleanup needed, and do anything to confirm contents.
+     *
+     * Return false to prevent the search area completed time and stats from being updated.
+     *
+     * @param \core_search\area\base $searcharea The search area that was complete
+     * @param int $numdocs The number of documents that were added to the index
+     * @param bool $fullindex True if a full index is being performed
+     * @return bool True means that data is considered indexed
+     */
+    public function area_index_complete($searcharea, $numdocs = 0, $fullindex = false) {
+        $this->commit();
+
+        return true;
+    }
+
     /**
      * Defragments the index.
      *
      * @return void
      */
     public function optimize() {
-        $this->get_search_client()->optimize();
+        $this->get_search_client()->optimize(1, true, false);
     }
 
     /**
@@ -338,6 +358,7 @@ class engine extends \core_search\engine {
      */
     public function delete_by_id($id) {
         $this->get_search_client()->deleteById($id);
+        $this->commit();
     }
 
     /**
@@ -352,6 +373,7 @@ class engine extends \core_search\engine {
         } else {
             $this->get_search_client()->deleteByQuery('*:*');
         }
+        $this->commit();
     }
 
     /**
@@ -424,6 +446,7 @@ class engine extends \core_search\engine {
             'ssl_password' => !empty($this->config->ssl_keypassword) ? $this->config->ssl_keypassword : '',
             'ssl_cainfo' => !empty($this->config->ssl_cainfo) ? $this->config->ssl_cainfo : '',
             'ssl_capath' => !empty($this->config->ssl_capath) ? $this->config->ssl_capath : '',
+            'timeout' => !empty($this->config->server_timeout) ? $this->config->server_timeout : '30'
         );
 
         $this->client = new \SolrClient($options);
index 5ed2872..4ea17e2 100644 (file)
@@ -34,14 +34,6 @@ require_once($CFG->libdir.'/adminlib.php');
 require_login(null, false);
 require_capability('moodle/site:config', context_system::instance());
 
-if (!\core_search\manager::is_global_search_enabled()) {
-    throw new moodle_exception('globalsearchdisabled', 'search');
-}
-
-if ($CFG->searchengine !== 'solr') {
-    throw new moodle_exception('solrnotselected', 'search_solr');
-}
-
 $schema = new \search_solr\schema();
 $schema->setup();
 
index a56bda8..0021d95 100644 (file)
@@ -41,10 +41,6 @@ class engine extends \core_search\engine {
         // No need to implement.
     }
 
-    public function commit() {
-        // No need to implement.
-    }
-
     public function execute_query($data, $usercontexts) {
         // No need to implement.
     }
index 46e3ec0..c1d104b 100644 (file)
@@ -649,7 +649,6 @@ if ($mode === MODE_USERDETAILS) {    // Print simple listing.
                     $row->cells[1]->text .= get_string('role').get_string('labelsep', 'langconfig').$user->role.'<br />';
                 }
                 if ($user->maildisplay == 1 or ($user->maildisplay == 2 and ($course->id != SITEID) and !isguestuser()) or
-                            has_capability('moodle/course:viewhiddenuserfields', $context) or
                             in_array('email', $extrafields) or ($user->id == $USER->id)) {
                     $row->cells[1]->text .= get_string('email').get_string('labelsep', 'langconfig').html_writer::link("mailto:$user->email", $user->email) . '<br />';
                 }
index 725a105..90fc4b0 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2016030400.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2016030400.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.