Merge branch 'MDL-55022-master' of git://github.com/junpataleta/moodle
authorDan Poltawski <dan@moodle.com>
Tue, 28 Jun 2016 10:27:35 +0000 (11:27 +0100)
committerDan Poltawski <dan@moodle.com>
Thu, 30 Jun 2016 08:24:56 +0000 (09:24 +0100)
55 files changed:
admin/settings/plugins.php
admin/tool/behat/cli/run.php
admin/tool/lp/classes/external/course_module_summary_exporter.php
admin/tool/mobile/tests/externallib_test.php
competency/classes/api.php
competency/classes/evidence.php
competency/tests/api_test.php
enrol/lti/classes/helper.php
grade/grading/form/guide/lang/en/gradingform_guide.php
grade/grading/form/guide/tests/behat/behat_gradingform_guide.php
grade/grading/form/guide/tests/behat/edit_guide.feature
grade/lib.php
lang/en/backup.php
lang/en/badges.php
lang/en/question.php
lib/moodlelib.php
lib/tests/behat/behat_hooks.php
lib/tests/moodlelib_test.php
lib/upgrade.txt
login/tests/behat/behat_login.php [new file with mode: 0644]
login/tests/behat/change_password.feature [new file with mode: 0644]
mod/assign/externallib.php
mod/assign/feedback/editpdf/styles.css
mod/choice/backup/moodle2/backup_choice_stepslib.php
mod/choice/lang/en/choice.php
mod/choice/lib.php
mod/choice/mod_form.php
mod/data/lang/en/data.php
mod/data/mod_form.php
mod/feedback/lang/en/feedback.php
mod/feedback/mod_form.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/lesson/mod_form.php
mod/lti/lang/en/lti.php
mod/lti/tests/behat/addtool.feature
mod/lti/tests/behat/toolconfigure.feature
mod/survey/save.php
repository/filepicker.php
repository/filesystem/lib.php
repository/lib.php
repository/repository_ajax.php
repository/upgrade.txt
search/classes/document.php
search/classes/manager.php
search/tests/fixtures/mock_search_area.php
search/tests/fixtures/testable_core_search.php
search/tests/manager_test.php
webservice/lib.php
webservice/soap/locallib.php
webservice/tests/externallib_test.php
webservice/upgrade.txt
webservice/xmlrpc/locallib.php
webservice/xmlrpc/tests/locallib_test.php [new file with mode: 0644]
webservice/xmlrpc/tests/xmlrpc_server_test.php [new file with mode: 0644]

index 1beaca5..680bd3d 100644 (file)
@@ -40,7 +40,9 @@ if ($hassiteconfig) {
         get_string('requiremodintro', 'admin'), get_string('requiremodintro_desc', 'admin'), 0));
     $ADMIN->add('modsettings', $temp);
 
-    foreach (core_plugin_manager::instance()->get_plugins_of_type('mod') as $plugin) {
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('mod');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
         /** @var \core\plugininfo\mod $plugin */
         $plugin->load_settings($ADMIN, 'modsettings', $hassiteconfig);
     }
@@ -50,7 +52,9 @@ if ($hassiteconfig) {
     $temp = new admin_settingpage('manageformats', new lang_string('manageformats', 'core_admin'));
     $temp->add(new admin_setting_manageformats());
     $ADMIN->add('formatsettings', $temp);
-    foreach (core_plugin_manager::instance()->get_plugins_of_type('format') as $plugin) {
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('format');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
         /** @var \core\plugininfo\format $plugin */
         $plugin->load_settings($ADMIN, 'formatsettings', $hassiteconfig);
     }
@@ -58,7 +62,9 @@ if ($hassiteconfig) {
     // blocks
     $ADMIN->add('modules', new admin_category('blocksettings', new lang_string('blocks')));
     $ADMIN->add('blocksettings', new admin_page_manageblocks());
-    foreach (core_plugin_manager::instance()->get_plugins_of_type('block') as $plugin) {
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('block');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
         /** @var \core\plugininfo\block $plugin */
         $plugin->load_settings($ADMIN, 'blocksettings', $hassiteconfig);
     }
@@ -67,7 +73,9 @@ if ($hassiteconfig) {
     $ADMIN->add('modules', new admin_category('messageoutputs', new lang_string('messageoutputs', 'message')));
     $ADMIN->add('messageoutputs', new admin_page_managemessageoutputs());
     $ADMIN->add('messageoutputs', new admin_page_defaultmessageoutputs());
-    foreach (core_plugin_manager::instance()->get_plugins_of_type('message') as $plugin) {
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('message');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
         /** @var \core\plugininfo\message $plugin */
         $plugin->load_settings($ADMIN, 'messageoutputs', $hassiteconfig);
     }
@@ -108,7 +116,9 @@ if ($hassiteconfig) {
     $temp = new admin_externalpage('authtestsettings', get_string('testsettings', 'core_auth'), new moodle_url("/auth/test_settings.php"), 'moodle/site:config', true);
     $ADMIN->add('authsettings', $temp);
 
-    foreach (core_plugin_manager::instance()->get_plugins_of_type('auth') as $plugin) {
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('auth');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
         /** @var \core\plugininfo\auth $plugin */
         $plugin->load_settings($ADMIN, 'authsettings', $hassiteconfig);
     }
@@ -122,7 +132,9 @@ if ($hassiteconfig) {
     $temp = new admin_externalpage('enroltestsettings', get_string('testsettings', 'core_enrol'), new moodle_url("/enrol/test_settings.php"), 'moodle/site:config', true);
     $ADMIN->add('enrolments', $temp);
 
-    foreach(core_plugin_manager::instance()->get_plugins_of_type('enrol') as $plugin) {
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('enrol');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
         /** @var \core\plugininfo\enrol $plugin */
         $plugin->load_settings($ADMIN, 'enrolments', $hassiteconfig);
     }
@@ -133,7 +145,9 @@ if ($hassiteconfig) {
     $temp = new admin_settingpage('manageeditors', new lang_string('editorsettings', 'editor'));
     $temp->add(new admin_setting_manageeditors());
     $ADMIN->add('editorsettings', $temp);
-    foreach (core_plugin_manager::instance()->get_plugins_of_type('editor') as $plugin) {
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('editor');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
         /** @var \core\plugininfo\editor $plugin */
         $plugin->load_settings($ADMIN, 'editorsettings', $hassiteconfig);
     }
@@ -143,7 +157,9 @@ if ($hassiteconfig) {
     $temp = new admin_settingpage('manageantiviruses', new lang_string('antivirussettings', 'antivirus'));
     $temp->add(new admin_setting_manageantiviruses());
     $ADMIN->add('antivirussettings', $temp);
-    foreach (core_plugin_manager::instance()->get_plugins_of_type('antivirus') as $plugin) {
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('antivirus');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
         /* @var \core\plugininfo\antivirus $plugin */
         $plugin->load_settings($ADMIN, 'antivirussettings', $hassiteconfig);
     }
@@ -182,7 +198,9 @@ if ($hassiteconfig) {
     }
     $ADMIN->add('filtersettings', $temp);
 
-    foreach (core_plugin_manager::instance()->get_plugins_of_type('filter') as $plugin) {
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('filter');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
         /** @var \core\plugininfo\filter $plugin */
         $plugin->load_settings($ADMIN, 'filtersettings', $hassiteconfig);
     }
@@ -283,7 +301,9 @@ if ($hassiteconfig) {
         new lang_string('createrepository', 'repository'), $url, 'moodle/site:config', true));
     $ADMIN->add('repositorysettings', new admin_externalpage('repositoryinstanceedit',
         new lang_string('editrepositoryinstance', 'repository'), $url, 'moodle/site:config', true));
-    foreach (core_plugin_manager::instance()->get_plugins_of_type('repository') as $plugin) {
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('repository');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
         /** @var \core\plugininfo\repository $plugin */
         $plugin->load_settings($ADMIN, 'repositorysettings', $hassiteconfig);
     }
@@ -337,7 +357,9 @@ if ($hassiteconfig) {
                         'admin'), new lang_string('configenablewsdocumentation', 'admin', $wsdoclink), false));
     $ADMIN->add('webservicesettings', $temp);
     /// links to protocol pages
-    foreach (core_plugin_manager::instance()->get_plugins_of_type('webservice') as $plugin) {
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('webservice');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
         /** @var \core\plugininfo\webservice $plugin */
         $plugin->load_settings($ADMIN, 'webservicesettings', $hassiteconfig);
     }
@@ -409,7 +431,9 @@ if ($hassiteconfig || has_capability('moodle/question:config', $systemcontext))
             get_string('responsehistory', 'question'), '', 0, $hiddenofvisible));
 
     // Settings for particular question types.
-    foreach (core_plugin_manager::instance()->get_plugins_of_type('qtype') as $plugin) {
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('qtype');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
         /** @var \core\plugininfo\qtype $plugin */
         $plugin->load_settings($ADMIN, 'qtypesettings', $hassiteconfig);
     }
@@ -421,7 +445,9 @@ if ($hassiteconfig && !empty($CFG->enableplagiarism)) {
     $ADMIN->add('plagiarism', new admin_externalpage('manageplagiarismplugins', new lang_string('manageplagiarism', 'plagiarism'),
         $CFG->wwwroot . '/' . $CFG->admin . '/plagiarism.php'));
 
-    foreach (core_plugin_manager::instance()->get_plugins_of_type('plagiarism') as $plugin) {
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('plagiarism');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
         /** @var \core\plugininfo\plagiarism $plugin */
         $plugin->load_settings($ADMIN, 'plagiarism', $hassiteconfig);
     }
@@ -445,6 +471,7 @@ if ($hassiteconfig) {
     }
     if (!empty($pages)) {
         $ADMIN->add('modules', new admin_category('coursereports', new lang_string('coursereports')));
+        core_collator::asort_objects_by_property($pages, 'visiblename');
         foreach ($pages as $page) {
             $ADMIN->add('coursereports', $page);
         }
@@ -468,6 +495,7 @@ foreach (core_component::get_plugin_list('report') as $report => $plugindir) {
 $ADMIN->add('modules', new admin_category('reportplugins', new lang_string('reports')));
 $ADMIN->add('reportplugins', new admin_externalpage('managereports', new lang_string('reportsmanage', 'admin'),
                                                     $CFG->wwwroot . '/' . $CFG->admin . '/reports.php'));
+core_collator::asort_objects_by_property($pages, 'visiblename');
 foreach ($pages as $page) {
     $ADMIN->add('reportplugins', $page);
 }
@@ -510,6 +538,7 @@ if ($hassiteconfig) {
     }
     $ADMIN->add('searchplugins', $temp);
 
+    core_collator::asort_objects_by_property($pages, 'visiblename');
     foreach ($pages as $page) {
         $ADMIN->add('searchplugins', $page);
     }
@@ -523,7 +552,9 @@ if ($hassiteconfig) {
 }
 
 // Now add various admin tools.
-foreach (core_plugin_manager::instance()->get_plugins_of_type('tool') as $plugin) {
+$plugins = core_plugin_manager::instance()->get_plugins_of_type('tool');
+core_collator::asort_objects_by_property($plugins, 'displayname');
+foreach ($plugins as $plugin) {
     /** @var \core\plugininfo\tool $plugin */
     $plugin->load_settings($ADMIN, null, $hassiteconfig);
 }
@@ -534,6 +565,7 @@ if ($hassiteconfig) {
     $ADMIN->add('cache', new admin_externalpage('cacheconfig', new lang_string('cacheconfig', 'cache'), $CFG->wwwroot .'/cache/admin.php'));
     $ADMIN->add('cache', new admin_externalpage('cachetestperformance', new lang_string('testperformance', 'cache'), $CFG->wwwroot . '/cache/testperformance.php'));
     $ADMIN->add('cache', new admin_category('cachestores', new lang_string('cachestores', 'cache')));
+    $ADMIN->locate('cachestores')->set_sorting(true);
     foreach (core_component::get_plugin_list('cachestore') as $plugin => $path) {
         $settingspath = $path.'/settings.php';
         if (file_exists($settingspath)) {
@@ -547,7 +579,9 @@ if ($hassiteconfig) {
 // Add Calendar type settings.
 if ($hassiteconfig) {
     $ADMIN->add('modules', new admin_category('calendartype', new lang_string('calendartypes', 'calendar')));
-    foreach (core_plugin_manager::instance()->get_plugins_of_type('calendartype') as $plugin) {
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('calendartype');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
         /** @var \core\plugininfo\calendartype $plugin */
         $plugin->load_settings($ADMIN, 'calendartype', $hassiteconfig);
     }
@@ -562,7 +596,9 @@ if ($hassiteconfig) {
 
 // Extend settings for each local plugin. Note that their settings may be in any part of the
 // settings tree and may be visible not only for administrators.
-foreach (core_plugin_manager::instance()->get_plugins_of_type('local') as $plugin) {
+$plugins = core_plugin_manager::instance()->get_plugins_of_type('local');
+core_collator::asort_objects_by_property($plugins, 'displayname');
+foreach ($plugins as $plugin) {
     /** @var \core\plugininfo\local $plugin */
     $plugin->load_settings($ADMIN, null, $hassiteconfig);
 }
index 98fc305..f8a37e7 100644 (file)
@@ -101,6 +101,9 @@ if (empty($options['torun'])) {
 if (extension_loaded('pcntl')) {
     $disabled = explode(',', ini_get('disable_functions'));
     if (!in_array('pcntl_signal', $disabled)) {
+        // Handle interrupts on PHP7.
+        declare(ticks = 1);
+
         pcntl_signal(SIGTERM, "signal_handler");
         pcntl_signal(SIGINT, "signal_handler");
     }
index dfd24fd..e776a1a 100644 (file)
@@ -40,14 +40,18 @@ class course_module_summary_exporter extends \core_competency\external\exporter
     }
 
     protected function get_other_values(renderer_base $output) {
-        $context = $this->related['cm']->context;
+        $cm = $this->related['cm'];
+        $context = $cm->context;
 
-        return array(
-            'id' => $this->related['cm']->id,
-            'name' => external_format_string($this->related['cm']->name, $context->id),
-            'url' => $this->related['cm']->url->out(),
-            'iconurl' => $this->related['cm']->get_icon_url()->out()
+        $values = array(
+            'id' => $cm->id,
+            'name' => external_format_string($cm->name, $context->id),
+            'iconurl' => $cm->get_icon_url()->out()
         );
+        if ($cm->url) {
+            $values['url'] = $cm->url->out();
+        }
+        return $values;
     }
 
 
@@ -60,7 +64,8 @@ class course_module_summary_exporter extends \core_competency\external\exporter
                 'type' => PARAM_TEXT
             ),
             'url' => array(
-                'type' => PARAM_URL
+                'type' => PARAM_URL,
+                'optional' => true,
             ),
             'iconurl' => array(
                 'type' => PARAM_URL
index 166fe5d..5a17c57 100644 (file)
@@ -50,7 +50,8 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
         $result = external::get_plugins_supporting_mobile();
         $result = external_api::clean_returnvalue(external::get_plugins_supporting_mobile_returns(), $result);
         $this->assertCount(0, $result['warnings']);
-        $this->assertCount(0, $result['plugins']);
+        $this->assertArrayHasKey('plugins', $result);
+        $this->assertTrue(is_array($result['plugins']));
     }
 
 }
index b11daa4..a440bc2 100644 (file)
@@ -4152,7 +4152,7 @@ class api {
      * @param int $limit Number of records to return.
      * @return \core_competency\evidence[]
      */
-    public static function list_evidence_in_course($userid = 0, $courseid = 0, $competencyid = 0, $sort = 'timecreated',
+    public static function list_evidence_in_course($userid = 0, $courseid = 0, $competencyid = 0, $sort = 'timecreated, id',
                                                    $order = 'DESC', $skip = 0, $limit = 0) {
         static::require_enabled();
 
@@ -4166,11 +4166,8 @@ class api {
             return array();
         }
 
-        $params = array(
-            'usercompetencyid' => $usercompetency->get_id(),
-            'contextid' => context_course::instance($courseid)->id
-        );
-        return evidence::get_records($params, $sort, $order, $skip, $limit);
+        $context = context_course::instance($courseid);
+        return evidence::get_records_for_usercompetency($usercompetency->get_id(), $context, $sort, $order, $skip, $limit);
     }
 
     /**
index ed3445d..cc8ec05 100644 (file)
@@ -284,4 +284,52 @@ class evidence extends persistent {
         return has_capability('moodle/competency:evidencedelete', context_user::instance($userid));
     }
 
+    /**
+     * Load a list of records in a context for a user competency.
+     *
+     * @param int $usercompetencyid The id of the user competency.
+     * @param context $context Context to filter the evidence list.
+     * @param string $sort The field from the evidence table to sort on.
+     * @param string $order The sort direction
+     * @param int $skip Limitstart.
+     * @param int $limit Number of rows to return.
+     *
+     * @return \core_competency\persistent[]
+     */
+    public static function get_records_for_usercompetency($usercompetencyid,
+                                                          \context $context,
+                                                          $sort = '',
+                                                          $order = 'ASC',
+                                                          $skip = 0,
+                                                          $limit = 0) {
+        global $DB;
+
+        $params = array(
+            'usercompid' => $usercompetencyid,
+            'path' => $context->path . '/%',
+            'contextid' => $context->id
+        );
+
+        if (!empty($sort)) {
+            $sortcolumns = explode(',', $sort);
+            $sortcolumns = array_map('trim', $sortcolumns);
+            $sort = ' ORDER BY e.' . implode(', e.', $sortcolumns) . ' ' . $order;
+        }
+
+        $sql = 'SELECT e.*
+                  FROM {' . static::TABLE . '} e
+                  JOIN {context} c ON c.id = e.contextid
+                 WHERE (c.path LIKE :path OR c.id = :contextid)
+                   AND e.usercompetencyid = :usercompid
+                 ' . $sort;
+        $records = $DB->get_records_sql($sql, $params, $skip, $limit);
+        $instances = array();
+
+        foreach ($records as $record) {
+            $newrecord = new static(0, $record);
+            array_push($instances, $newrecord);
+        }
+        return $instances;
+    }
+
 }
index 9e5c4d3..44aaf76 100644 (file)
@@ -2579,6 +2579,47 @@ class core_competency_api_testcase extends advanced_testcase {
         $this->assertEquals(null, $ev4->get_actionuserid());
     }
 
+    public function test_list_evidence_in_course() {
+        global $SITE;
+
+        $this->resetAfterTest(true);
+        $dg = $this->getDataGenerator();
+        $lpg = $dg->get_plugin_generator('core_competency');
+        $u1 = $dg->create_user();
+        $course = $dg->create_course();
+        $coursecontext = context_course::instance($course->id);
+
+        $this->setAdminUser();
+        $f = $lpg->create_framework();
+        $c = $lpg->create_competency(array('competencyframeworkid' => $f->get_id()));
+        $c2 = $lpg->create_competency(array('competencyframeworkid' => $f->get_id()));
+        $cc = api::add_competency_to_course($course->id, $c->get_id());
+        $cc2 = api::add_competency_to_course($course->id, $c2->get_id());
+
+        $pagegenerator = $this->getDataGenerator()->get_plugin_generator('mod_page');
+        $page = $pagegenerator->create_instance(array('course' => $course->id));
+
+        $cm = get_coursemodule_from_instance('page', $page->id);
+        $cmcontext = context_module::instance($cm->id);
+        // Add the competency to the course module.
+        $ccm = api::add_competency_to_course_module($cm, $c->get_id());
+
+        // Now add the evidence to the course.
+        $evidence1 = api::add_evidence($u1->id, $c->get_id(), $coursecontext->id, \core_competency\evidence::ACTION_LOG,
+            'invaliddata', 'error');
+
+        $result = api::list_evidence_in_course($u1->id, $course->id, $c->get_id());
+        $this->assertEquals($result[0]->get_id(), $evidence1->get_id());
+
+        // Now add the evidence to the course module.
+        $evidence2 = api::add_evidence($u1->id, $c->get_id(), $cmcontext->id, \core_competency\evidence::ACTION_LOG,
+            'invaliddata', 'error');
+
+        $result = api::list_evidence_in_course($u1->id, $course->id, $c->get_id());
+        $this->assertEquals($result[0]->get_id(), $evidence2->get_id());
+        $this->assertEquals($result[1]->get_id(), $evidence1->get_id());
+    }
+
     public function test_list_course_modules_using_competency() {
         global $SITE;
 
index d2d6336..83a9b13 100644 (file)
@@ -202,8 +202,10 @@ class helper {
             'connecttimeout' => 5
         );
 
-        if (!$iconfiles = $fs->create_file_from_url($filerecord, $url, $urlparams)) {
-            return self::PROFILE_IMAGE_UPDATE_FAILED;
+        try {
+            $fs->create_file_from_url($filerecord, $url, $urlparams);
+        } catch (\file_exception $e) {
+            return get_string($e->errorcode, $e->module, $e->a);
         }
 
         $iconfile = $fs->get_area_files($context->id, 'user', 'newicon', false, 'itemid', false);
index 71e2c5d..ed2ae2d 100644 (file)
@@ -70,7 +70,7 @@ $string['guidestatus'] = 'Current marking guide status';
 $string['hidemarkerdesc'] = 'Hide marker criterion descriptions';
 $string['hidestudentdesc'] = 'Hide student criterion descriptions';
 $string['insertcomment'] = 'Insert frequently used comment';
-$string['maxscore'] = 'Maximum mark';
+$string['maxscore'] = 'Maximum score';
 $string['name'] = 'Name';
 $string['needregrademessage'] = 'The marking guide definition was changed after this student had been graded. The student can not see this marking guide until you check the marking guide and update the grade.';
 $string['pluginname'] = 'Marking guide';
index f368c14..7c90524 100644 (file)
@@ -57,7 +57,7 @@ class behat_gradingform_guide extends behat_base {
      * @param TableNode $guide
      */
     public function i_define_the_following_marking_guide(TableNode $guide) {
-        $steptableinfo = '| Criterion name | Description for students | Description for markers | Maximum mark |';
+        $steptableinfo = '| Criterion name | Description for students | Description for markers | Maximum score |';
 
         if ($criteria = $guide->getHash()) {
             $addcriterionbutton = $this->find_button(get_string('addcriterion', 'gradingform_guide'));
@@ -92,7 +92,7 @@ class behat_gradingform_guide extends behat_base {
                 $this->set_guide_field_value($criterionroot . '[descriptionmarkers]', $criterion['Description for markers']);
 
                 // Set the field value for the Max score field.
-                $this->set_guide_field_value($criterionroot . '[maxscore]', $criterion['Maximum mark']);
+                $this->set_guide_field_value($criterionroot . '[maxscore]', $criterion['Maximum score']);
             }
         }
     }
index abe994a..f043693 100644 (file)
@@ -29,10 +29,10 @@ Feature: Marking guides can be created and edited
       | Name        | Assignment 1 marking guide     |
       | Description | Marking guide test description |
     And I define the following marking guide:
-      | Criterion name    | Description for students         | Description for markers         | Maximum mark |
-      | Guide criterion A | Guide A description for students | Guide A description for markers | 30           |
-      | Guide criterion B | Guide B description for students | Guide B description for markers | 30           |
-      | Guide criterion C | Guide C description for students | Guide C description for markers | 40           |
+      | Criterion name    | Description for students         | Description for markers         | Maximum score |
+      | Guide criterion A | Guide A description for students | Guide A description for markers | 30            |
+      | Guide criterion B | Guide B description for students | Guide B description for markers | 30            |
+      | Guide criterion C | Guide C description for students | Guide C description for markers | 40            |
     And I define the following frequently used comments:
       | Comment 1 |
       | Comment 2 |
index 1c0da1e..2983874 100644 (file)
@@ -1039,7 +1039,8 @@ function print_grade_page_head($courseid, $active_type, $active_plugin=null,
             if (isset($user)) {
                 $output = $OUTPUT->context_header(
                         array(
-                            'heading' => fullname($user),
+                            'heading' => html_writer::link(new moodle_url('/user/view.php', array('id' => $user->id,
+                                'course' => $courseid)), fullname($user)),
                             'user' => $user,
                             'usercontext' => context_user::instance($user->id)
                         ), 2
index a63c3e6..3f75c53 100644 (file)
@@ -76,11 +76,11 @@ $string['backuptypesection'] = 'Section';
 $string['backupversion'] = 'Backup version';
 $string['cannotfindassignablerole'] = 'The {$a} role in the backup file cannot be mapped to any of the roles that you are allowed to assign.';
 $string['choosefilefromcoursebackup'] = 'Course backup area';
-$string['choosefilefromcoursebackup_help'] = 'When backup courses using default settings, backup files will be stored here';
+$string['choosefilefromcoursebackup_help'] = 'Course backups made using default settings are stored here.';
 $string['choosefilefromuserbackup'] = 'User private backup area';
-$string['choosefilefromuserbackup_help'] = 'When backup courses with "Anonymize user information" option ticked, backup files will be stored here';
+$string['choosefilefromuserbackup_help'] = 'Backup files with anonymized user information are stored here.';
 $string['choosefilefromactivitybackup'] = 'Activity backup area';
-$string['choosefilefromactivitybackup_help'] = 'When backup activities using default settings, backup files will be stored here';
+$string['choosefilefromactivitybackup_help'] = 'Activity backups made using default settings are stored here.';
 $string['choosefilefromautomatedbackup'] = 'Automated backups';
 $string['choosefilefromautomatedbackup_help'] = 'Contains automatically generated backups.';
 $string['configgeneralactivities'] = 'Sets the default for including activities in a backup.';
@@ -130,9 +130,9 @@ $string['filealiasesrestorefailures_help'] = 'Aliases are symbolic links to othe
 
 More details and the actual reason of the failure can be found in the restore log file.';
 $string['filealiasesrestorefailures_link'] = 'restore/filealiases';
-$string['filereferencesincluded'] = 'File references to external contents included in backup package, they won\'t work on other sites.';
-$string['filereferencessamesite'] = 'Backup is from the same site, file references can be restored';
-$string['filereferencesnotsamesite'] = 'Backup is from other site, file references cannot be restored';
+$string['filereferencesincluded'] = 'File references to external contents are included in the backup file. These won\'t work if the backup is restored on a different site.';
+$string['filereferencessamesite'] = 'The backup file is from this site, and so file references can be restored.';
+$string['filereferencesnotsamesite'] = 'The backup file is from a different site, and so file references cannot be restored.';
 $string['generalactivities'] = 'Include activities and resources';
 $string['generalanonymize'] = 'Anonymise information';
 $string['generalbackdefaults'] = 'General backup defaults';
@@ -180,7 +180,7 @@ $string['lockedbyconfig'] = 'This setting has been locked by the default backup
 $string['lockedbyhierarchy'] = 'Locked by dependencies';
 $string['loglifetime'] = 'Keep logs for';
 $string['managefiles'] = 'Manage backup files';
-$string['missingfilesinpool'] = 'Some files could not be saved during the backup, it won\'t be possible to restore them.';
+$string['missingfilesinpool'] = 'Some files could not be saved during the backup, and so it will not be possible to restore them.';
 $string['moodleversion'] = 'Moodle version';
 $string['moreresults'] = 'There are too many results, enter a more specific search.';
 $string['nomatchingcourses'] = 'There are no courses to display';
index abf487a..1077607 100644 (file)
@@ -87,7 +87,7 @@ $string['backpackdetails'] = 'Backpack settings';
 $string['backpackemail'] = 'Email address';
 $string['backpackemail_help'] = 'The email address associated with your backpack. While you are connected, any badges earned on this site will be associated with this email address.';
 $string['personaconnection'] = 'Sign in with your email';
-$string['personaconnection_help'] = 'Persona is a system for identifying yourself across the web, using an email address that you own. The Open Badges backpack uses Persona as a login system, so to be able to connect to a backpack you with need a Persona account.
+$string['personaconnection_help'] = 'Persona is a system for identifying yourself across the web, using an email address that you own. The Open Badges backpack uses Persona as a login system, so to be able to connect to a backpack you will need a Persona account.
 
 For more information about Persona visit <a href="https://login.persona.org/about">https://login.persona.org/about</a>.';
 $string['backpackimport'] = 'Badge import settings';
index d245288..b3c377d 100644 (file)
@@ -65,7 +65,7 @@ $string['categorycurrent'] = 'Current category';
 $string['categorycurrentuse'] = 'Use this category';
 $string['categorydoesnotexist'] = 'This category does not exist';
 $string['categoryinfo'] = 'Category info';
-$string['categorymove'] = 'The category \'{$a->name}\' contains {$a->count} questions (some of them may be old, hidden, questions, or Random questions that are still in use in some existing quizzes). Please choose another category to move them to.';
+$string['categorymove'] = 'The category \'{$a->name}\' contains {$a->count} questions (some of which may be hidden questions or random questions that are still in use in a quiz). Please choose another category to move them to.';
 $string['categorymoveto'] = 'Save in category';
 $string['categorynamecantbeblank'] = 'The category name cannot be blank.';
 $string['clickflag'] = 'Flag question';
index 33569f6..4a15f63 100644 (file)
@@ -4302,7 +4302,7 @@ function authenticate_user_login($username, $password, $ignorelockout=false, &$f
  * @return stdClass A {@link $USER} object - BC only, do not use
  */
 function complete_user_login($user) {
-    global $CFG, $USER;
+    global $CFG, $USER, $SESSION;
 
     \core\session\manager::login_user($user);
 
@@ -4345,6 +4345,7 @@ function complete_user_login($user) {
             if ($changeurl = $userauth->change_password_url()) {
                 redirect($changeurl);
             } else {
+                $SESSION->wantsurl = core_login_get_return_url();
                 redirect($CFG->httpswwwroot.'/login/change_password.php');
             }
         } else {
@@ -6256,65 +6257,54 @@ function valid_uploaded_file($newfile) {
 /**
  * Returns the maximum size for uploading files.
  *
- * There are eight possible upload limits:
- * 1. No limit, if the upload isn't using a post request and the user has permission to ignore limits.
- * 2. in Apache using LimitRequestBody (no way of checking or changing this)
- * 3. in php.ini for 'upload_max_filesize' (can not be changed inside PHP)
- * 4. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP)
- * 5. in php.ini for 'post_max_size' (can not be changed inside PHP)
- * 6. by the Moodle admin in $CFG->maxbytes
- * 7. by the teacher in the current course $course->maxbytes
- * 8. by the teacher for the current module, eg $assignment->maxbytes
+ * There are seven possible upload limits:
+ * 1. in Apache using LimitRequestBody (no way of checking or changing this)
+ * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP)
+ * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP)
+ * 4. in php.ini for 'post_max_size' (can not be changed inside PHP)
+ * 5. by the Moodle admin in $CFG->maxbytes
+ * 6. by the teacher in the current course $course->maxbytes
+ * 7. by the teacher for the current module, eg $assignment->maxbytes
  *
  * These last two are passed to this function as arguments (in bytes).
  * Anything defined as 0 is ignored.
  * The smallest of all the non-zero numbers is returned.
  *
- * The php.ini settings are only used if $usespost is true. This allows repositories that do not use post requests, such as
- * repository_filesystem, to copy in files that are larger than post_max_size if the user has permission.
+ * @todo Finish documenting this function
  *
  * @param int $sitebytes Set maximum size
  * @param int $coursebytes Current course $course->maxbytes (in bytes)
  * @param int $modulebytes Current module ->maxbytes (in bytes)
- * @param bool $usespost Does the upload we're getting the max size for use a post request?
+ * @param bool $unused This parameter has been deprecated and is not used any more.
  * @return int The maximum size for uploading files.
  */
-function get_max_upload_file_size($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $usespost = true) {
-    $sizes = array();
-
-    if ($usespost) {
-        if (!$filesize = ini_get('upload_max_filesize')) {
-            $filesize = '5M';
-        }
-        $sizes[] = get_real_size($filesize);
+function get_max_upload_file_size($sitebytes=0, $coursebytes=0, $modulebytes=0, $unused = false) {
 
-        if ($postsize = ini_get('post_max_size')) {
-            $sizes[] = get_real_size($postsize);
-        }
+    if (! $filesize = ini_get('upload_max_filesize')) {
+        $filesize = '5M';
+    }
+    $minimumsize = get_real_size($filesize);
 
-        if ($sitebytes > 0) {
-            $sizes[] = $sitebytes;
-        }
-    } else {
-        if ($sitebytes != 0) {
-            // It's for possible that $sitebytes == USER_CAN_IGNORE_FILE_SIZE_LIMITS (-1).
-            $sizes[] = $sitebytes;
+    if ($postsize = ini_get('post_max_size')) {
+        $postsize = get_real_size($postsize);
+        if ($postsize < $minimumsize) {
+            $minimumsize = $postsize;
         }
     }
 
-    if ($coursebytes > 0) {
-        $sizes[] = $coursebytes;
+    if (($sitebytes > 0) and ($sitebytes < $minimumsize)) {
+        $minimumsize = $sitebytes;
     }
 
-    if ($modulebytes > 0) {
-        $sizes[] = $modulebytes;
+    if (($coursebytes > 0) and ($coursebytes < $minimumsize)) {
+        $minimumsize = $coursebytes;
     }
 
-    if (empty($sizes)) {
-        throw new coding_exception('You must specify at least one filesize limit.');
+    if (($modulebytes > 0) and ($modulebytes < $minimumsize)) {
+        $minimumsize = $modulebytes;
     }
 
-    return min($sizes);
+    return $minimumsize;
 }
 
 /**
@@ -6327,11 +6317,11 @@ function get_max_upload_file_size($sitebytes = 0, $coursebytes = 0, $modulebytes
  * @param int $coursebytes Current course $course->maxbytes (in bytes)
  * @param int $modulebytes Current module ->maxbytes (in bytes)
  * @param stdClass $user The user
- * @param bool $usespost Does the upload we're getting the max size for use a post request?
+ * @param bool $unused This parameter has been deprecated and is not used any more.
  * @return int The maximum size for uploading files.
  */
 function get_user_max_upload_file_size($context, $sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $user = null,
-        $usespost = true) {
+        $unused = false) {
     global $USER;
 
     if (empty($user)) {
@@ -6339,10 +6329,10 @@ function get_user_max_upload_file_size($context, $sitebytes = 0, $coursebytes =
     }
 
     if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) {
-        return get_max_upload_file_size(USER_CAN_IGNORE_FILE_SIZE_LIMITS, 0, 0, $usespost);
+        return USER_CAN_IGNORE_FILE_SIZE_LIMITS;
     }
 
-    return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes, $usespost);
+    return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes);
 }
 
 /**
index 0b95413..856968f 100644 (file)
@@ -160,6 +160,14 @@ class behat_hooks extends behat_base {
         if (!empty($CFG->behat_faildump_path) && !is_writable($CFG->behat_faildump_path)) {
             throw new Exception('You set $CFG->behat_faildump_path to a non-writable directory');
         }
+
+        // Handle interrupts on PHP7.
+        if (extension_loaded('pcntl')) {
+            $disabled = explode(',', ini_get('disable_functions'));
+            if (!in_array('pcntl_signal', $disabled)) {
+                declare(ticks = 1);
+            }
+        }
     }
 
     /**
index 3cfb551..34e05bc 100644 (file)
@@ -2216,84 +2216,6 @@ class core_moodlelib_testcase extends advanced_testcase {
         $this->assertArrayHasKey(get_max_upload_file_size(), $result);
     }
 
-    /**
-     * Provider for get_max_upload_file_size.
-     *
-     * @return array
-     */
-    public function get_max_upload_file_size_provider() {
-        $inisize = min(array(get_real_size(ini_get('post_max_size')), get_real_size(ini_get('upload_max_filesize'))));
-        return [
-            'POST: inisize smallest' => [
-                $inisize + 10,
-                $inisize + 20,
-                $inisize + 30,
-                true,
-                $inisize,
-            ],
-            'POST: sitebytes smallest' => [
-                $inisize - 30,
-                $inisize - 20,
-                $inisize - 10,
-                true,
-                $inisize - 30,
-            ],
-            'POST: coursebytes smallest' => [
-                $inisize - 20,
-                $inisize - 30,
-                $inisize - 10,
-                true,
-                $inisize - 30,
-            ],
-            'POST: modulebytes smallest' => [
-                $inisize - 20,
-                $inisize - 10,
-                $inisize - 30,
-                true,
-                $inisize - 30,
-            ],
-            'POST: User can ignore site limit (respect ini)' => [
-                USER_CAN_IGNORE_FILE_SIZE_LIMITS,
-                0,
-                0,
-                true,
-                $inisize,
-            ],
-            'NOPOST: inisize smallest' => [
-                $inisize + 10,
-                $inisize + 20,
-                $inisize + 30,
-                false,
-                $inisize + 10,
-            ],
-            'NOPOST: User can ignore site limit (no limit)' => [
-                USER_CAN_IGNORE_FILE_SIZE_LIMITS,
-                0,
-                0,
-                false,
-                USER_CAN_IGNORE_FILE_SIZE_LIMITS,
-            ],
-        ];
-    }
-
-    /**
-     * Test get_max_upload_file_size with various combinations.
-     *
-     * @dataProvider get_max_upload_file_size_provider
-     */
-    public function test_get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes, $ispost, $expectation) {
-        $this->assertEquals($expectation, get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes, $ispost));
-    }
-
-    /**
-     * Test that when get_max_upload_file_size is called with no sizes, and no post, an exception is thrown.
-     */
-    public function test_get_max_upload_file_size_no_sizes() {
-        // If not using post we have to provide at least one other limit.
-        $this->setExpectedException('coding_exception', 'You must specify at least one filesize limit.');
-        get_max_upload_file_size(0, 0, 0, false);
-    }
-
     /**
      * Test function password_is_legacy_hash().
      */
index 7218a1e..74666ac 100644 (file)
@@ -4,6 +4,9 @@ information provided here is intended especially for developers.
 === 3.2 ===
 
 * New option 'blanktarget' added to format_text. This option adds target="_blank" to links
+* The parameter $usepost of the following functions has been deprecated and is not used any more:
+  - get_max_upload_file_size()
+  - get_user_max_upload_file_size()
 
 === 3.1 ===
 
diff --git a/login/tests/behat/behat_login.php b/login/tests/behat/behat_login.php
new file mode 100644 (file)
index 0000000..cd2cb5f
--- /dev/null
@@ -0,0 +1,52 @@
+<?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/>.
+
+/**
+ * Behat login related steps definitions.
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2016 Universite de Montreal
+ * @author     Gilles-Philippe Leblanc
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// NOTE: no MOODLE_INTERNAL used, this file may be required by behat before including /config.php.
+
+require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
+
+/**
+ * Contains functions used by behat to test functionality.
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2016 Universite de Montreal
+ * @author     Gilles-Philippe Leblanc
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_login extends behat_base {
+
+    /**
+     * Force a password change for a specific user.
+     *
+     * @Given /^I force a password change for user "([^"]*)"$/
+     * @param string $username The username of the user whose password will expire
+     */
+    public function i_force_a_password_change_for_user($username) {
+        $user = core_user::get_user_by_username($username, 'id', null, MUST_EXIST);
+        set_user_preference("auth_forcepasswordchange", true, $user);
+    }
+}
diff --git a/login/tests/behat/change_password.feature b/login/tests/behat/change_password.feature
new file mode 100644 (file)
index 0000000..93ffb84
--- /dev/null
@@ -0,0 +1,65 @@
+@core
+Feature: Change password
+  In order to ensure the password change works as expected
+  As a user
+  I need to test all the way to change my password
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | testuser | Test | User | moodle@example.com |
+
+  Scenario: A user can change his password manually
+    Given I am on site homepage
+    And I log in as "testuser"
+    And I follow "Preferences" in the user menu
+    When I follow "Change password"
+    And I set the field "Current password" to "testuser"
+    And I set the field "New password" to "NewPassword1*"
+    And I set the field "New password (again)" to "NewPassword1*"
+    And I click on "Save changes" "button"
+    Then I should see "Password has been changed"
+    And I click on "Continue" "button"
+    And I should see "Preferences" in the "#region-main" "css_element"
+    And I log out
+    And I follow "Log in"
+    And I set the field "Username" to "testuser"
+    And I set the field "Password" to "NewPassword1*"
+    And I press "Log in"
+    Then I should see "You are logged in as Test User" in the "page-footer" "region"
+
+  Scenario: A user with expired password must change it when log in directly and then be redirected to the home page
+    Given I force a password change for user "testuser"
+    And I log in as "testuser"
+    And I should see "You must change your password to proceed"
+    When I set the field "Current password" to "testuser"
+    And I set the field "New password" to "NewPassword1*"
+    And I set the field "New password (again)" to "NewPassword1*"
+    And I click on "Save changes" "button"
+    Then I should see "Password has been changed"
+    And I click on "Continue" "button"
+    And I am on site homepage
+
+  @javascript
+  Scenario: A user with expired password trying to visit a required login page must change and it and then be redirected to this page
+    Given I force a password change for user "testuser"
+    And the following "courses" exist:
+      | fullname | shortname | visible |
+      | Course 1 | c1 | 1 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | testuser | c1 | student |
+    And I am on site homepage
+    And I expand "Courses" node
+    When I follow "Course 1"
+    And I set the field "Username" to "testuser"
+    And I set the field "Password" to "testuser"
+    And I press "Log in"
+    Then I should see "You must change your password to proceed"
+    And I set the field "Current password" to "testuser"
+    And I set the field "New password" to "NewPassword1*"
+    And I set the field "New password (again)" to "NewPassword1*"
+    And I click on "Save changes" "button"
+    And I should see "Password has been changed"
+    And I click on "Continue" "button"
+    And I should see "Course 1"
index d0924ca..f901c30 100644 (file)
@@ -2433,7 +2433,10 @@ class mod_assign_external extends external_api {
             // We need to change the type of some of the structures retrieved from the renderable.
             if (!empty($lastattempt->submissiongroup)) {
                 $lastattempt->submissiongroup = $lastattempt->submissiongroup->id;
+            } else {
+                unset($lastattempt->submissiongroup);
             }
+
             if (!empty($lastattempt->usergroups)) {
                 $lastattempt->usergroups = array_keys($lastattempt->usergroups);
             }
index 3a365e3..45551fa 100644 (file)
@@ -14,6 +14,7 @@
     background-color: #ccc;
     margin-left: auto;
     margin-right: auto;
+    box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 20px rgba(0,0,0,.2);
 }
 .assignfeedback_editpdf_widget .moodle-dialogue-bd .drawingregion {
     position: inherit;
@@ -32,6 +33,7 @@
     bottom: 0px;
     position: absolute;
     overflow: auto;
+    background-color: #ccc;
 }
 
 .assignfeedback_editpdf_widget {
index 415ada9..f097099 100644 (file)
@@ -39,7 +39,7 @@ class backup_choice_activity_structure_step extends backup_activity_structure_st
         // Define each element separated
         $choice = new backup_nested_element('choice', array('id'), array(
             'name', 'intro', 'introformat', 'publish',
-            'showresults', 'display', 'allowupdate', 'showunanswered',
+            'showresults', 'display', 'allowupdate', 'allowmultiple', 'showunanswered',
             'limitanswers', 'timeopen', 'timeclose', 'timemodified',
             'completionsubmit', 'showpreview', 'includeinactive'));
 
index e977d97..d7d4df0 100644 (file)
@@ -28,6 +28,7 @@ $string['allowupdate'] = 'Allow choice to be updated';
 $string['allowmultiple'] = 'Allow more than one choice to be selected';
 $string['answered'] = 'Answered';
 $string['cannotsubmit'] = 'Sorry, there was a problem submitting your choice. Please try again.';
+$string['closebeforeopen'] = 'You have specified a close date before the open date.';
 $string['completionsubmit'] = 'Show as complete when user makes a choice';
 $string['displayhorizontal'] = 'Display horizontally';
 $string['displaymode'] = 'Display mode for the options';
index 666df96..54c01b6 100644 (file)
@@ -479,7 +479,7 @@ function prepare_choice_show_results($choice, $course, $cm, $allresponses) {
 
     //overwrite options value;
     $display->options = array();
-    $totaluser = 0;
+    $allusers = [];
     foreach ($choice->option as $optionid => $optiontext) {
         $display->options[$optionid] = new stdClass;
         $display->options[$optionid]->text = $optiontext;
@@ -487,13 +487,13 @@ function prepare_choice_show_results($choice, $course, $cm, $allresponses) {
 
         if (array_key_exists($optionid, $allresponses)) {
             $display->options[$optionid]->user = $allresponses[$optionid];
-            $totaluser += count($allresponses[$optionid]);
+            $allusers = array_merge($allusers, array_keys($allresponses[$optionid]));
         }
     }
     unset($display->option);
     unset($display->maxanswers);
 
-    $display->numberofuser = $totaluser;
+    $display->numberofuser = count(array_unique($allusers));
     $context = context_module::instance($cm->id);
     $display->viewresponsecapability = has_capability('mod/choice:readresponses', $context);
     $display->deleterepsonsecapability = has_capability('mod/choice:deleteresponses',$context);
@@ -830,12 +830,13 @@ function choice_extend_settings_navigation(settings_navigation $settings, naviga
         // Big function, approx 6 SQL calls per user.
         $allresponses = choice_get_response_data($choice, $PAGE->cm, $groupmode, $onlyactive);
 
-        $responsecount =0;
+        $allusers = [];
         foreach($allresponses as $optionid => $userlist) {
             if ($optionid) {
-                $responsecount += count($userlist);
+                $allusers = array_merge($allusers, array_keys($userlist));
             }
         }
+        $responsecount = count(array_unique($allusers));
         $choicenode->add(get_string("viewallresponses", "choice", $responsecount), new moodle_url('/mod/choice/report.php', array('id'=>$PAGE->cm->id)));
     }
 }
@@ -917,8 +918,10 @@ function choice_print_overview($courses, &$htmlarray) {
 
             // Display relevant info based on permissions.
             if (has_capability('mod/choice:readresponses', context_module::instance($choice->coursemodule))) {
-                $attempts = $DB->count_records('choice_answers', array('choiceid' => $choice->id));
-                $str .= $OUTPUT->box(get_string('viewallresponses', 'choice', $attempts), 'info');
+                $attempts = $DB->count_records_sql('SELECT COUNT(DISTINCT userid) FROM {choice_answers} WHERE choiceid = ?',
+                    [$choice->id]);
+                $url = new moodle_url('/mod/choice/report.php', ['id' => $choice->coursemodule]);
+                $str .= $OUTPUT->box(html_writer::link($url, get_string('viewallresponses', 'choice', $attempts)), 'info');
 
             } else if (has_capability('mod/choice:choose', context_module::instance($choice->coursemodule))) {
                 // See if the user has submitted anything.
index 756a79b..e15ad2d 100644 (file)
@@ -139,6 +139,25 @@ class mod_choice_mod_form extends moodleform_mod {
         return $data;
     }
 
+    /**
+     * Enforce validation rules here
+     *
+     * @param array $data array of ("fieldname"=>value) of submitted data
+     * @param array $files array of uploaded files "element_name"=>tmp_file_path
+     * @return array
+     **/
+    public function validation($data, $files) {
+        $errors = parent::validation($data, $files);
+
+        // Check open and close times are consistent.
+        if ($data['timeopen'] && $data['timeclose'] &&
+                $data['timeclose'] < $data['timeopen']) {
+            $errors['timeclose'] = get_string('closebeforeopen', 'choice');
+        }
+
+        return $errors;
+    }
+
     function add_completion_rules() {
         $mform =& $this->_form;
 
index a8e0d38..842ef63 100644 (file)
@@ -50,6 +50,7 @@ Fields have the format [[fieldname]]. All other tags have the format ##sometag##
 
 Only the tags that are in the "Available tags" list may be used for the current template.';
 $string['availabletodate'] = 'Available to';
+$string['availabletodatevalidation'] = 'The available to date cannot be before the available from date.';
 $string['blank'] = 'Blank';
 $string['buttons'] = 'Actions';
 $string['bynameondate'] = 'by {$a->name} - {$a->date}';
@@ -368,4 +369,5 @@ $string['usestandard'] = 'Use a preset';
 $string['usestandard_help'] = 'To use a preset available to the whole site, select it from the list. (If you have added a preset to the list using the save as preset feature then you have the option of deleting it.)';
 $string['viewfromdate'] = 'Read only from';
 $string['viewtodate'] = 'Read only to';
+$string['viewtodatevalidation'] = 'The read only to date cannot be before the read only from date.';
 $string['wrongdataid'] = 'Wrong data id provided';
index fb6fc19..11c9db7 100644 (file)
@@ -81,6 +81,29 @@ class mod_data_mod_form extends moodleform_mod {
         $this->add_action_buttons();
     }
 
+    /**
+     * Enforce validation rules here
+     *
+     * @param array $data array of ("fieldname"=>value) of submitted data
+     * @param array $files array of uploaded files "element_name"=>tmp_file_path
+     * @return array
+     **/
+    public function validation($data, $files) {
+        $errors = parent::validation($data, $files);
+
+        // Check open and close times are consistent.
+        if ($data['timeavailablefrom'] && $data['timeavailableto'] &&
+                $data['timeavailableto'] < $data['timeavailablefrom']) {
+            $errors['timeavailableto'] = get_string('availabletodatevalidation', 'data');
+        }
+        if ($data['timeviewfrom'] && $data['timeviewto'] &&
+                $data['timeviewto'] < $data['timeviewfrom']) {
+            $errors['timeviewto'] = get_string('viewtodatevalidation', 'data');
+        }
+
+        return $errors;
+    }
+
     function data_preprocessing(&$default_values){
         parent::data_preprocessing($default_values);
     }
index 052930d..6e2988e 100644 (file)
@@ -41,6 +41,7 @@ $string['cannotaccess'] = 'You can only access this feedback from a course';
 $string['cannotsavetempl'] = 'saving templates is not allowed';
 $string['captcha'] = 'Captcha';
 $string['captchanotset'] = 'Captcha hasn\'t been set.';
+$string['closebeforeopen'] = 'You have specified an end date before the start date.';
 $string['completed_feedbacks'] = 'Submitted answers';
 $string['complete_the_form'] = 'Answer the questions...';
 $string['completionsubmit'] = 'View as completed if the feedback is submitted';
index 2bf91dd..81bd89b 100644 (file)
@@ -179,8 +179,21 @@ class mod_feedback_mod_form extends moodleform_mod {
         return $data;
     }
 
+    /**
+     * Enforce validation rules here
+     *
+     * @param array $data array of ("fieldname"=>value) of submitted data
+     * @param array $files array of uploaded files "element_name"=>tmp_file_path
+     * @return array
+     **/
     public function validation($data, $files) {
         $errors = parent::validation($data, $files);
+
+        // Check open and close times are consistent.
+        if ($data['timeopen'] && $data['timeclose'] &&
+                $data['timeclose'] < $data['timeopen']) {
+            $errors['timeclose'] = get_string('closebeforeopen', 'feedback');
+        }
         return $errors;
     }
 
index 0c4ab07..4804d4e 100644 (file)
@@ -505,11 +505,11 @@ $string['trackingoff'] = 'Off';
 $string['trackingon'] = 'Forced';
 $string['trackingoptional'] = 'Optional';
 $string['trackingtype'] = 'Read tracking';
-$string['trackingtype_help'] = 'If enabled, participants can track read and unread posts in the forum and in discussions. There are three options:
+$string['trackingtype_help'] = 'Read tracking enables participants to easily check which posts they have not yet seen by highlighting any new posts.
 
-* Optional - Participants can choose whether to turn tracking on or off via a link in the administration block. Forum tracking must also be enabled in the user\'s profile settings.
-* Forced - Tracking is always on, regardless of user setting. Available depending on administrative setting.
-* Off - Read and unread posts are not tracked.';
+If set to optional, participants can choose whether to turn tracking on or off via a link in the administration block. (Users must also enable forum tracking in their forum preferences.)
+
+If \'Allow forced read tracking\' is enabled in the site administration, then a further option is available - forced. This means that tracking is always on, regardless of users\' forum preferences.';
 $string['unread'] = 'Unread';
 $string['unreadposts'] = 'Unread posts';
 $string['unreadpostsnumber'] = '{$a} unread posts';
index 8ab2749..4940d41 100644 (file)
@@ -1022,7 +1022,7 @@ function forum_cron() {
 
                 $headerdata = new stdClass();
                 $headerdata->sitename = format_string($site->fullname, true);
-                $headerdata->userprefs = $CFG->wwwroot.'/user/edit.php?id='.$userid.'&amp;course='.$site->id;
+                $headerdata->userprefs = $CFG->wwwroot.'/user/forum.php?id='.$userid.'&amp;course='.$site->id;
 
                 $posttext = get_string('digestmailheader', 'forum', $headerdata)."\n\n";
                 $headerdata->userprefs = '<a target="_blank" href="'.$headerdata->userprefs.'">'.get_string('digestmailprefs', 'forum').'</a>';
index 44b1e14..f0fa12e 100644 (file)
@@ -426,6 +426,12 @@ class mod_lesson_mod_form extends moodleform_mod {
                 $data->completiontimespent = 0;
             }
         }
+        if (!empty($data->completionunlocked)) {
+            $autocompletion = !empty($data->completion) && $data->completion == COMPLETION_TRACKING_AUTOMATIC;
+            if (empty($data->completionendreached) || !$autocompletion) {
+                $data->completionendreached = 0;
+            }
+        }
         return $data;
     }
 }
index 324931e..93bc6d0 100644 (file)
@@ -191,7 +191,7 @@ Three icons are available after the preconfigured tool dropdown list:
 * **Delete** - Remove the selected course level tool.';
 $string['external_tool_types'] = 'Preconfigured tools';
 $string['failedtoconnect'] = 'Moodle was unable to communicate with the "{$a}" system';
-$string['failedtocreatetooltype'] = 'Failed to create new tool. Please check the url and try again.';
+$string['failedtocreatetooltype'] = 'Failed to create new tool. Please check the URL and try again.';
 $string['failedtodeletetoolproxy'] = 'Failed to delete tool registration. You may need to visit "Manage external tool registrations" and delete it manually.';
 $string['filter_basiclti_configlink'] = 'Configure your preferred sites and their passwords';
 $string['filter_basiclti_password'] = 'Password is mandatory';
@@ -399,11 +399,9 @@ $string['secure_icon_url'] = 'Secure icon URL';
 $string['secure_icon_url_help'] = 'Similar to the icon URL, but used if the user accessing Moodle securely through SSL. The main purpose for this field is to prevent
 the browser from warning the user if the underlying page was accessed over SSL, but requesting to show an unsecure image.';
 $string['secure_launch_url'] = 'Secure launch URL';
-$string['secure_launch_url_help'] = 'Similar to Launch URL, but used instead of the launch url if high security is required. Moodle will use the
-secure launch URL instead of the launch URL if the Moodle site is accessed through SSL, or if the tool configuration
-is set to always launch through SSL.
+$string['secure_launch_url_help'] = 'Similar to the launch URL, but used instead of the launch URL if high security is required. Moodle will use the secure launch URL instead of the launch URL if the Moodle site is accessed through SSL, or if the tool configuration is set to always launch through SSL.
 
-The Launch URL may also be set to an https address to force launching through SSL, and this field may be left blank.';
+The launch URL may also be set to an https address to force launching through SSL, and this field may be left blank.';
 $string['send'] = 'Send';
 $string['services'] = 'Services';
 $string['services_help'] = 'Select those services which you wish to offer to the tool provider.  More than one service can be selected.';
@@ -433,15 +431,13 @@ $string['share_roster_admin_help'] = 'Specify whether the tool can access the li
 $string['share_roster_help'] = 'Specify whether the tool can access the list of users enrolled in this course.
 
 Note that this setting may be overridden in the tool configuration.';
-$string['show_in_course_activity_chooser'] = 'Show in activity chooser and as preconfigured tool';
-$string['show_in_course_lti1'] = 'Show when creating activities';
-$string['show_in_course_lti1_help'] = 'This tool can be shown in the activity chooser for a teacher to select to add to a course. It also can be shown in the preconfigured tool dropdown menu when adding an external tool to a course.
-
-Alternatively, the tool configuration may not show up in the forms but only used if the correct launch URL is entered when adding an external tool to a course.';
-$string['show_in_course_lti2'] = 'Show when creating activities';
+$string['show_in_course_activity_chooser'] = 'Show in activity chooser and as a preconfigured tool';
+$string['show_in_course_lti1'] = 'Tool configuration usage';
+$string['show_in_course_lti1_help'] = 'This tool may be shown in the activity chooser for a teacher to select to add to a course. Alternatively, it may be shown in the preconfigured tool dropdown menu when adding an external tool to a course. A further option is for the tool configuration to only be used if the exact launch URL is entered when adding an external tool to a course.';
+$string['show_in_course_lti2'] = 'Tool configuration usage';
 $string['show_in_course_lti2_help'] = 'This tool can be shown in the activity chooser for a teacher to select to add to a course or in the preconfigured tool dropdown menu when adding an external tool to a course.';
-$string['show_in_course_no'] = 'Do not show, use for launch URL matching only';
-$string['show_in_course_preconfigured'] = 'Show as preconfigured tool when adding External tool';
+$string['show_in_course_no'] = 'Do not show; use only when a matching launch URL is entered';
+$string['show_in_course_preconfigured'] = 'Show as preconfigured tool when adding an external tool';
 $string['size'] = 'Size parameters';
 $string['submission'] = 'Submission';
 $string['submissions'] = 'Submissions';
index 4ecc90b..6da74b4 100644 (file)
@@ -20,7 +20,7 @@ Feature: Add tools
     And I follow "Add preconfigured tool"
     And I set the following fields to these values:
       | Tool name | Teaching Tool 1 |
-      | Show when creating activities | Show in activity chooser and as preconfigured tool  |
+      | Tool configuration usage | Show in activity chooser and as a preconfigured tool |
     And I set the field "Tool base URL/cartridge URL" to local url "/mod/lti/tests/fixtures/tool_provider.html"
     And I press "Save changes"
     And I log out
index 8ab4713..e1afbda 100644 (file)
@@ -33,7 +33,7 @@ Feature: Configure tool types
     And I press "Add"
     Then I should see "Enter your consumer key and shared secret"
     And I press "Save changes"
-    And I should see "Failed to create new tool. Please check the url and try again."
+    And I should see "Failed to create new tool. Please check the URL and try again."
 
   @javascript
   Scenario: Attempt to add a tool type from a configuration URL, then cancel
index 0fff2c7..afa25b2 100644 (file)
 
     survey_save_answers($survey, $formdata, $course, $context);
 
-    $params = array(
-        'context' => $context,
-        'courseid' => $course->id,
-        'other' => array('surveyid' => $survey->id)
-    );
-    $event = \mod_survey\event\response_submitted::create($params);
-    $event->trigger();
-
 // Print the page and finish up.
 
     notice(get_string("thanksforanswers","survey", $USER->firstname), "$CFG->wwwroot/course/view.php?id=$course->id");
index dccec42..e488e3d 100644 (file)
@@ -77,8 +77,6 @@ if (!$course = $DB->get_record('course', array('id'=>$courseid))) {
 }
 $PAGE->set_course($course);
 
-$usespost = true;
-
 if ($repo_id) {
     // Get repository instance information
     $repooptions = array(
@@ -89,14 +87,12 @@ if ($repo_id) {
 
     // Check permissions
     $repo->check_capability();
-
-    $usespost = $repo->uses_post_requests();
 }
 
 $context = context::instance_by_id($contextid);
 
 // Make sure maxbytes passed is within site filesize limits.
-$maxbytes = get_user_max_upload_file_size($context, $CFG->maxbytes, $course->maxbytes, $maxbytes, null, $usespost);
+$maxbytes = get_user_max_upload_file_size($context, $CFG->maxbytes, $course->maxbytes, $maxbytes);
 
 $params = array('ctx_id' => $contextid, 'itemid' => $itemid, 'env' => $env, 'course'=>$courseid, 'maxbytes'=>$maxbytes, 'areamaxbytes'=>$areamaxbytes, 'maxfiles'=>$maxfiles, 'subdirs'=>$subdirs, 'sesskey'=>sesskey());
 $params['action'] = 'browse';
index 066ad7e..409ef32 100644 (file)
@@ -810,17 +810,6 @@ class repository_filesystem extends repository {
     public function supports_relative_file() {
         return $this->get_option('relativefiles');
     }
-
-    /**
-     * Helper function to indicate if this repository uses post requests for uploading files.
-     *
-     * Files are copied from the filesystem so don't rely on POST requests.
-     *
-     * @return bool
-     */
-    public function uses_post_requests() {
-        return false;
-    }
 }
 
 /**
index 297ca6f..5d0d905 100644 (file)
@@ -2782,13 +2782,12 @@ abstract class repository implements cacheable_object {
     /**
      * Helper function to indicate if this repository uses post requests for uploading files.
      *
-     * If the respository doesn't rely on uploading via POST requests, this can be overridden to return false,
-     * allowing users with the right permissions to upload files of any size from this repository.
-     *
+     * @deprecated since Moodle 3.2, 3.1.1, 3.0.5
      * @return bool
      */
     public function uses_post_requests() {
-        return true;
+        debugging('The method repository::uses_post_requests() is deprecated and must not be used anymore.', DEBUG_DEVELOPER);
+        return false;
     }
 }
 
index a8c5e82..51ecb3b 100644 (file)
@@ -86,8 +86,7 @@ if (!empty($course)) {
     $coursemaxbytes = $course->maxbytes;
 }
 // Make sure maxbytes passed is within site filesize limits.
-$maxbytes = get_user_max_upload_file_size($context, $CFG->maxbytes, $coursemaxbytes, $maxbytes,
-        null, $repo->uses_post_requests());
+$maxbytes = get_user_max_upload_file_size($context, $CFG->maxbytes, $coursemaxbytes, $maxbytes);
 
 // Wait as long as it takes for this script to finish
 core_php_time_limit::raise();
index dc89c4d..ab9c084 100644 (file)
@@ -3,6 +3,10 @@ information provided here is intended especially for developers. Full
 details of the repository API are available on Moodle docs:
 http://docs.moodle.org/dev/Repository_API
 
+=== 3.2 ===
+
+* The method repository::uses_post_requests() has been deprecated and must not be used anymore.
+
 === 3.1 ===
 
 * The following functions, previously used (exclusively) by upgrade steps are not available
index 61658f5..b9e87e0 100644 (file)
@@ -563,6 +563,8 @@ class document implements \renderable, \templatable {
 
         $title = $this->is_set('title') ? $this->format_text($this->get('title')) : '';
         $data = [
+            'componentname' => $componentname,
+            'areaname' => $areaname,
             'courseurl' => course_get_url($this->get('courseid')),
             'coursefullname' => format_string($this->get('coursefullname'), true, array('context' => $this->get('contextid'))),
             'modified' => userdate($this->get('modified')),
index faaf5c1..49824f7 100644 (file)
@@ -346,7 +346,16 @@ class manager {
 
             $systemcontextid = \context_system::instance()->id;
             foreach ($areasbylevel[CONTEXT_SYSTEM] as $areaid => $searchclass) {
-                $areascontexts[$areaid][] = $systemcontextid;
+                $areascontexts[$areaid][$systemcontextid] = $systemcontextid;
+            }
+        }
+
+        if (!empty($areasbylevel[CONTEXT_USER])) {
+            if ($usercontext = \context_user::instance($USER->id, IGNORE_MISSING)) {
+                // Extra checking although only logged users should reach this point, guest users have a valid context id.
+                foreach ($areasbylevel[CONTEXT_USER] as $areaid => $searchclass) {
+                    $areascontexts[$areaid][$usercontext->id] = $usercontext->id;
+                }
             }
         }
 
index 8a638d7..c747685 100644 (file)
@@ -29,6 +29,12 @@ defined('MOODLE_INTERNAL') || die;
 
 class mock_search_area extends \core_search\area\base {
 
+    /**
+     * Multiple context level so we can test get_areas_user_accesses.
+     * @var int[]
+     */
+    protected static $levels = [CONTEXT_SYSTEM, CONTEXT_USER];
+
     /**
      * To make things easier, base class required config stuff.
      *
index 256f0b8..bce1437 100644 (file)
@@ -29,6 +29,11 @@ require_once(__DIR__ . '/mock_search_engine.php');
 /**
  * Core search class adapted to unit test.
  *
+ * Note that by default all core search areas are returned when calling get_search_areas_list,
+ * if you want to use the mock search area you can use testable_core_search::add_search_area
+ * although if you want to add mock search areas on top of the core ones you should call
+ * testable_core_search::add_core_search_areas before calling testable_core_search::add_search_area.
+ *
  * @package    core_search
  * @copyright  2015 David Monllao {@link http://www.davidmonllao.com}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
@@ -82,4 +87,14 @@ class testable_core_search extends \core_search\manager {
        self::$enabledsearchareas[$areaid] = $searcharea;
        self::$allsearchareas[$areaid] = $searcharea;
     }
+
+    /**
+     * Loads all core search areas.
+     *
+     * @return void
+     */
+    public function add_core_search_areas() {
+        self::get_search_areas_list(false);
+        self::get_search_areas_list(true);
+    }
 }
index 20db8ab..e6db62d 100644 (file)
@@ -26,6 +26,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 require_once(__DIR__ . '/fixtures/testable_core_search.php');
+require_once(__DIR__ . '/fixtures/mock_search_area.php');
 
 /**
  * Unit tests for search manager.
@@ -165,8 +166,11 @@ class search_manager_testcase extends advanced_testcase {
         $course2 = $this->getDataGenerator()->create_course();
         $course2ctx = context_course::instance($course2->id);
         $teacher = $this->getDataGenerator()->create_user();
+        $teacherctx = context_user::instance($teacher->id);
         $student = $this->getDataGenerator()->create_user();
+        $studentctx = context_user::instance($student->id);
         $noaccess = $this->getDataGenerator()->create_user();
+        $noaccessctx = context_user::instance($noaccess->id);
         $this->getDataGenerator()->enrol_user($teacher->id, $course1->id, 'teacher');
         $this->getDataGenerator()->enrol_user($student->id, $course1->id, 'student');
 
@@ -180,17 +184,23 @@ class search_manager_testcase extends advanced_testcase {
         $context3 = context_module::instance($forum3->cmid);
 
         $search = testable_core_search::instance();
+        $mockareaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
+        $search->add_core_search_areas();
+        $search->add_search_area($mockareaid, new core_mocksearch\search\mock_search_area());
 
         $this->setAdminUser();
         $this->assertTrue($search->get_areas_user_accesses());
 
         $sitectx = \context_course::instance(SITEID);
+        $systemctxid = \context_system::instance()->id;
 
         // Can access the frontpage ones.
         $this->setUser($noaccess);
         $contexts = $search->get_areas_user_accesses();
         $this->assertEquals(array($frontpageforumcontext->id => $frontpageforumcontext->id), $contexts[$this->forumpostareaid]);
         $this->assertEquals(array($sitectx->id => $sitectx->id), $contexts[$this->mycoursesareaid]);
+        $mockctxs = array($noaccessctx->id => $noaccessctx->id, $systemctxid => $systemctxid);
+        $this->assertEquals($mockctxs, $contexts[$mockareaid]);
 
         $this->setUser($teacher);
         $contexts = $search->get_areas_user_accesses();
@@ -199,12 +209,16 @@ class search_manager_testcase extends advanced_testcase {
         $this->assertEquals($frontpageandcourse1, $contexts[$this->forumpostareaid]);
         $this->assertEquals(array($sitectx->id => $sitectx->id, $course1ctx->id => $course1ctx->id),
             $contexts[$this->mycoursesareaid]);
+        $mockctxs = array($teacherctx->id => $teacherctx->id, $systemctxid => $systemctxid);
+        $this->assertEquals($mockctxs, $contexts[$mockareaid]);
 
         $this->setUser($student);
         $contexts = $search->get_areas_user_accesses();
         $this->assertEquals($frontpageandcourse1, $contexts[$this->forumpostareaid]);
         $this->assertEquals(array($sitectx->id => $sitectx->id, $course1ctx->id => $course1ctx->id),
             $contexts[$this->mycoursesareaid]);
+        $mockctxs = array($studentctx->id => $studentctx->id, $systemctxid => $systemctxid);
+        $this->assertEquals($mockctxs, $contexts[$mockareaid]);
 
         // Hide the activity.
         set_coursemodule_visible($forum2->cmid, 0);
index fc9c178..4a608a2 100644 (file)
@@ -460,7 +460,8 @@ class webservice {
                       FROM {external_functions} f
                      WHERE f.name IN (SELECT sf.functionname
                                         FROM {external_services_functions} sf
-                                       WHERE sf.externalserviceid $serviceids)";
+                                       WHERE sf.externalserviceid $serviceids)
+                     ORDER BY f.name ASC";
             $functions = $DB->get_records_sql($sql, $params);
         } else {
             $functions = array();
index 02c6440..ebfa5ca 100644 (file)
@@ -240,11 +240,8 @@ class webservice_soap_server extends webservice_base_server {
         $envelope->appendChild($body);
         $dom->appendChild($envelope);
 
-        // Send headers.
-        $this->send_headers();
-
-        // Output the XML.
-        echo $dom->saveXML();
+        $this->response = $dom->saveXML();
+        $this->send_response();
     }
 
     /**
@@ -263,7 +260,7 @@ class webservice_soap_server extends webservice_base_server {
         header('Expires: ' . gmdate('D, d M Y H:i:s', 0) . ' GMT');
         header('Pragma: no-cache');
         header('Accept-Ranges: none');
-        header('Content-Length: ' . count($this->response));
+        header('Content-Length: ' . strlen($this->response));
         header('Content-Type: application/xml; charset=utf-8');
         header('Content-Disposition: inline; filename="response.xml"');
     }
index 9d98d35..5c80986 100644 (file)
@@ -151,9 +151,7 @@ class core_webservice_externallib_testcase extends externallib_advanced_testcase
         $siteinfo = external_api::clean_returnvalue(core_webservice_external::get_site_info_returns(), $siteinfo);
 
         $this->assertEquals(0, $siteinfo['userquota']);
-
-        // The max_size is dependant upon the post_max_size, and upload_max_filesize values in php.ini.
-        $this->assertEquals(get_max_upload_file_size(USER_CAN_IGNORE_FILE_SIZE_LIMITS), $siteinfo['usermaxuploadfilesize']);
+        $this->assertEquals(USER_CAN_IGNORE_FILE_SIZE_LIMITS, $siteinfo['usermaxuploadfilesize']);
         $this->assertEquals(true, $siteinfo['usercanmanageownfiles']);
 
         $this->assertEquals(HOMEPAGE_SITE, $siteinfo['userhomepage']);
index 9d5a009..5159d50 100644 (file)
@@ -3,6 +3,10 @@ information provided here is intended especially for developers.
 
 This information is intended for authors of webservices, not people writing webservice clients.
 
+=== 3.2 ===
+
+* webservice->get_external_functions now returns the external function list ordered by name ASC.
+
 === 3.1 ===
 
 * The xmlrpc backend has changed, Zend_XmlRpc has been dropped and there might be slight differences in
index 87cd695..e03728f 100644 (file)
@@ -73,11 +73,11 @@ class webservice_xmlrpc_server extends webservice_base_server {
         }
 
         // Get the XML-RPC request data.
-        $rawpostdata = file_get_contents("php://input");
+        $rawpostdata = $this->fetch_input_content();
         $methodname = null;
 
         // Decode the request to get the decoded parameters and the name of the method to be called.
-        $decodedparams = xmlrpc_decode_request($rawpostdata, $methodname);
+        $decodedparams = xmlrpc_decode_request($rawpostdata, $methodname, 'UTF-8');
         $methodinfo = external_api::external_function_info($methodname);
         $methodparams = array_keys($methodinfo->parameters_desc->keys);
 
@@ -94,6 +94,15 @@ class webservice_xmlrpc_server extends webservice_base_server {
         $this->parameters = $methodvariables;
     }
 
+    /**
+     * Fetch content from the client.
+     *
+     * @return string
+     */
+    protected function fetch_input_content() {
+        return file_get_contents('php://input');
+    }
+
     /**
      * Prepares the response.
      */
@@ -102,8 +111,10 @@ class webservice_xmlrpc_server extends webservice_base_server {
             if (!empty($this->function->returns_desc)) {
                 $validatedvalues = external_api::clean_returnvalue($this->function->returns_desc, $this->returns);
                 $encodingoptions = array(
-                    "encoding" => "utf-8",
-                    "verbosity" => "no_white_space"
+                    "encoding" => "UTF-8",
+                    "verbosity" => "no_white_space",
+                    // See MDL-54868.
+                    "escaping" => ["markup"]
                 );
                 // We can now convert the response to the requested XML-RPC format.
                 $this->response = xmlrpc_encode_request(null, $validatedvalues, $encodingoptions);
@@ -186,8 +197,10 @@ class webservice_xmlrpc_server extends webservice_base_server {
         );
 
         $encodingoptions = array(
-            "encoding" => "utf-8",
-            "verbosity" => "no_white_space"
+            "encoding" => "UTF-8",
+            "verbosity" => "no_white_space",
+            // See MDL-54868.
+            "escaping" => ["markup"]
         );
 
         return xmlrpc_encode_request(null, $fault, $encodingoptions);
diff --git a/webservice/xmlrpc/tests/locallib_test.php b/webservice/xmlrpc/tests/locallib_test.php
new file mode 100644 (file)
index 0000000..ad99cbe
--- /dev/null
@@ -0,0 +1,162 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for the XML-RPC web service server.
+ *
+ * @package    webservice_xmlrpc
+ * @category   test
+ * @copyright  2016 Cameron Ball
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/webservice/xmlrpc/locallib.php');
+
+/**
+ * Unit tests for the XML-RPC web service server.
+ *
+ * @package    webservice_xmlrpc
+ * @category   test
+ * @copyright  2016 Cameron Ball
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class webservice_xmlrpc_locallib_testcase extends advanced_testcase {
+
+    /**
+     * Setup.
+     */
+    public function setUp() {
+        if (!function_exists('xmlrpc_decode')) {
+            $this->markTestSkipped('XMLRPC is not installed.');
+        }
+    }
+
+    /**
+     * Test that the response generated is correct
+     *
+     * There is a bug in PHP that causes the xml_rpc library to
+     * incorrectly escape multibyte characters. See https://bugs.php.net/bug.php?id=41650
+     *
+     * @dataProvider prepare_response_provider
+     * @param string $returnsdesc  Webservice function return description
+     * @param string $returns       Webservice function description
+     * @param string $expected      The expected XML-RPC response
+     */
+    public function test_prepare_response($returnsdesc, $returns, $expected) {
+        $server = $this->getMockBuilder('webservice_xmlrpc_server')
+                       ->disableOriginalConstructor()
+                       ->setMethods(null)
+                       ->getMock();
+
+        $rc = new \ReflectionClass('webservice_xmlrpc_server');
+        $rcm = $rc->getMethod('prepare_response');
+        $rcm->setAccessible(true);
+
+        $func = $rc->getProperty('function');
+        $func->setAccessible(true);
+        $func->setValue($server, (object) ['returns_desc' => new external_value(PARAM_RAW, $returnsdesc, VALUE_OPTIONAL)]);
+
+        $ret = $rc->getProperty('returns');
+        $ret->setAccessible(true);
+        $ret->setValue($server, $returns);
+
+        $rcm->invokeArgs($server, []);
+        $response = $rc->getProperty('response');
+        $response->setAccessible(true);
+
+        $this->assertEquals($expected, $response->getValue($server));
+    }
+
+    /**
+     * Test that the response generated is correct
+     *
+     * There is a bug in PHP that causes the xml_rpc library to
+     * incorrectly escape multibyte characters. See https://bugs.php.net/bug.php?id=41650
+     *
+     * @dataProvider generate_error_provider
+     * @param Exception $exception An exception to be provided to generate_error
+     * @param string    $code      An error code to be provided to generate_error
+     * @param string    $expected  The expected XML-RPC response
+     */
+    public function test_generate_error($exception, $code, $expected) {
+        $server = $this->getMockBuilder('webservice_xmlrpc_server')
+                ->disableOriginalConstructor()
+                ->setMethods(null)
+                ->getMock();
+
+        $rc = new \ReflectionClass('webservice_xmlrpc_server');
+        $rcm = $rc->getMethod('generate_error');
+        $rcm->setAccessible(true);
+
+        if ($code === null) {
+            $result = $rcm->invokeArgs($server, [$exception]);
+        } else {
+            $result = $rcm->invokeArgs($server, [$exception, $code]);
+        }
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
+     * Data provider for the prepare_response testcase
+     *
+     * @return array of testcases
+     */
+    public function prepare_response_provider() {
+        return [
+            'Description written with Latin script' => [
+                'Ennyn Durin, Aran Moria: pedo mellon a minno',
+                'Mellon!',
+                '<?xml version="1.0" encoding="UTF-8"?><methodResponse><params><param><value><string>Mellon!</string></value>'
+                . '</param></params></methodResponse>'
+            ],
+            'Description with non-Latin glyphs' => [
+                'What biscuits do you have?',
+                // V         Unicode 9!         V.
+                '😂🤵😂 ð’ªð“ƒð“ð“Ž ð“‰ð’½ð‘’ ð’»ð’¾ð“ƒð‘’𝓈𝓉 ð¼ð“‰ð’¶ð“ð’¾ð’¶ð“ƒ ð’·ð’¾ð“ˆð’¸ð“Šð’¾ð“‰ð“ˆ ðŸ˜‚🤵😂',
+                '<?xml version="1.0" encoding="UTF-8"?><methodResponse><params><param><value><string>'
+                . '😂🤵😂 ð’ªð“ƒð“ð“Ž ð“‰ð’½ð‘’ ð’»ð’¾ð“ƒð‘’𝓈𝓉 ð¼ð“‰ð’¶ð“ð’¾ð’¶ð“ƒ ð’·ð’¾ð“ˆð’¸ð“Šð’¾ð“‰ð“ˆ ðŸ˜‚🤵😂</string></value></param></params></methodResponse>'
+            ]
+        ];
+    }
+
+    /**
+     * Data provider for the generate_error testcase
+     *
+     * @return array of testcases
+     */
+    public function generate_error_provider() {
+        return [
+            'Standard exception with default faultcode' => [
+                new \Exception(),
+                null,
+                '<?xml version="1.0" encoding="UTF-8"?><methodResponse><fault><value><struct><member><name>faultCode</name><value><int>404</int></value></member><member><name>faultString</name><value><string/></value></member></struct></value></fault></methodResponse>'
+            ],
+            'Standard exception with default faultcode and exception content' => [
+                new \Exception('PC LOAD LETTER'),
+                null,
+                '<?xml version="1.0" encoding="UTF-8"?><methodResponse><fault><value><struct><member><name>faultCode</name><value><int>404</int></value></member><member><name>faultString</name><value><string>PC LOAD LETTER</string></value></member></struct></value></fault></methodResponse>'
+            ],
+            'Standard exception with really messed up non-Latin glyphs' => [
+                new \Exception('P̫̬̳̫̓͊̇r̨͎̜ͧa͚̬̙̺͎̙ͬẏ͎̲̦̲e̶̞͎͙̻͐̉r͙̙ͮ̓̈ͧ̔̃ Í¨ÌÍ­ÌŽÌŽÌ‡Ì¿Ì n̗̥̞͗o̼̖͛̂̒̿ͮ͘t̷̞͎̘̘̝̥̲͂̌ͭ ÌŠÌ†ÌÌ’͕̹͚̪͖̖ͫ̓̀fͤͦͭͥ͊ͩo̼̱̻̹͒̿͒u̡͕̞͕̜̠͕ͥͭ̈̄̈́͐ń̘̼̇͜d̸̰̻͎͉̱̰̥̿͒'),
+                null,
+                '<?xml version="1.0" encoding="UTF-8"?><methodResponse><fault><value><struct><member><name>faultCode</name><value><int>404</int></value></member><member><name>faultString</name><value><string>P̫̬̳̫̓͊̇r̨͎̜ͧa͚̬̙̺͎̙ͬẏ͎̲̦̲e̶̞͎͙̻͐̉r͙̙ͮ̓̈ͧ̔̃ Í¨ÌÍ­ÌŽÌŽÌ‡Ì¿Ì n̗̥̞͗o̼̖͛̂̒̿ͮ͘t̷̞͎̘̘̝̥̲͂̌ͭ ÌŠÌ†ÌÌ’͕̹͚̪͖̖ͫ̓̀fͤͦͭͥ͊ͩo̼̱̻̹͒̿͒u̡͕̞͕̜̠͕ͥͭ̈̄̈́͐ń̘̼̇͜d̸̰̻͎͉̱̰̥̿͒</string></value></member></struct></value></fault></methodResponse>'
+            ]
+        ];
+    }
+}
diff --git a/webservice/xmlrpc/tests/xmlrpc_server_test.php b/webservice/xmlrpc/tests/xmlrpc_server_test.php
new file mode 100644 (file)
index 0000000..a4f6fea
--- /dev/null
@@ -0,0 +1,114 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for the XML-RPC web service server.
+ *
+ * @package    webservice_xmlrpc
+ * @category   test
+ * @copyright  2016 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/webservice/xmlrpc/locallib.php');
+
+/**
+ * Unit tests for the XML-RPC web service server.
+ *
+ * @package    webservice_xmlrpc
+ * @category   test
+ * @copyright  2016 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class xmlrpc_server_test extends advanced_testcase {
+
+    /**
+     * Setup.
+     */
+    public function setUp() {
+        if (!function_exists('xmlrpc_decode')) {
+            $this->markTestSkipped('XMLRPC is not installed.');
+        }
+    }
+
+    /**
+     * Test parameter parsing.
+     *
+     * @dataProvider parse_request_provider
+     * @param string $input             The XML-RPC request
+     * @param string $expectfunction    The expected value for the function after decoding the request
+     * @param array  $expectparams      The expected value for the params after decoding the request
+     */
+    public function test_parse_request($input, $expectfunction, $expectparams) {
+        $server = $this->getMockBuilder('\webservice_xmlrpc_server')
+                       ->setMethods(['fetch_input_content'])
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+        $server->method('fetch_input_content')
+               ->willReturn($input);
+
+        $rc = new \ReflectionClass('\webservice_xmlrpc_server');
+        $rcm = $rc->getMethod('parse_request');
+        $rcm->setAccessible(true);
+        $rcm->invoke($server);
+
+        $rcp = $rc->getProperty('functionname');
+        $rcp->setAccessible(true);
+        $this->assertEquals($expectfunction, $rcp->getValue($server));
+
+        $rcp = $rc->getProperty('parameters');
+        $rcp->setAccessible(true);
+        $this->assertEquals($expectparams, $rcp->getValue($server));
+    }
+
+    /**
+     * Data provider for testing parse_request.
+     *
+     * @return array
+     */
+    public function parse_request_provider() {
+        $xml = '<?xml version="1.0" encoding="UTF-8"?>';
+
+        // This valid webservice call has one required param ('component'), and one optional param ('lang').
+        $validmethod = '<methodName>core_get_component_strings</methodName>';
+        $requiredparams = '<params><param><value><string>moodle</string></value></param></params>';
+        $allparams = '<params><param><value><string>moodle</string></value></param><param><value><string>en</string></value>'
+                . '</param></params>';
+        $requiredparamsnonlatin = '<params><param><value><string>ᛞᛁᛞᛃᛟᚢᚲᚾᛟᚹᛈᚺᛈᛋᚢᛈᛈᛟᚱᛏᛋᚢᛏᚠ8ᚡᚨᚱᛁᚨᛒᛚᛖᚾᚨᛗᛖᛋ</string></value></param></params>';
+
+        return [
+                'Valid method, required params only' => [
+                    "{$xml}<methodCall>{$validmethod}{$requiredparams}</methodCall>",
+                    'core_get_component_strings',
+                    ['component' => 'moodle'],
+                ],
+                'Valid method, all params' => [
+                    "{$xml}<methodCall>{$validmethod}{$allparams}</methodCall>",
+                    'core_get_component_strings',
+                    ['component' => 'moodle', 'lang' => 'en'],
+                ],
+                'Valid method required params only (non Latin)' => [
+                    "{$xml}<methodCall>{$validmethod}{$requiredparamsnonlatin}</methodCall>",
+                    'core_get_component_strings',
+                    ['component' => 'ᛞᛁᛞᛃᛟᚢᚲᚾᛟᚹᛈᚺᛈᛋᚢᛈᛈᛟᚱᛏᛋᚢᛏᚠ8ᚡᚨᚱᛁᚨᛒᛚᛖᚾᚨᛗᛖᛋ'],
+                ],
+            ];
+    }
+}