Merge branch 'wip-MDL-60591-master' of git://github.com/marinaglancy/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 28 Nov 2017 21:35:58 +0000 (22:35 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 28 Nov 2017 21:35:58 +0000 (22:35 +0100)
80 files changed:
admin/cli/fix_deleted_users.php
admin/tool/analytics/classes/clihelper.php [new file with mode: 0644]
admin/tool/analytics/cli/enable_model.php
admin/tool/analytics/cli/evaluate_model.php
admin/tool/analytics/lang/en/tool_analytics.php
admin/tool/messageinbound/index.php
admin/tool/spamcleaner/index.php
analytics/classes/manager.php
backup/moodle2/restore_stepslib.php
badges/badge.php
badges/classes/external.php
badges/edit_form.php
badges/mybadges.php
badges/tests/badgeslib_test.php
badges/tests/behat/add_badge.feature
calendar/tests/calendartype_test.php
calendar/view.php
cohort/lib.php
composer.json
composer.lock
course/externallib.php
course/tests/externallib_test.php
enrol/externallib.php
enrol/tests/externallib_test.php
grade/grading/form/lib.php
grade/report/singleview/classes/local/screen/screen.php
group/import.php
group/lib.php
group/tests/behat/groups_import.feature
group/tests/fixtures/groups_import_multicourse.csv [new file with mode: 0644]
lang/en/cache.php
lib/badgeslib.php
lib/blocklib.php
lib/classes/message/inbound/manager.php
lib/classes/plugininfo/portfolio.php
lib/db/caches.php
lib/db/upgrade.php
lib/dml/pgsql_native_moodle_database.php
lib/dml/pgsql_native_moodle_recordset.php
lib/dml/tests/pgsql_native_recordset_test.php [new file with mode: 0644]
lib/enrollib.php
lib/filelib.php
lib/filestorage/file_storage.php
lib/form/dateselector.php
lib/form/datetimeselector.php
lib/form/defaultcustom.php
lib/grouplib.php
lib/navigationlib.php
lib/outputcomponents.php
lib/tablelib.php
lib/testing/classes/util.php
lib/yui/build/moodle-core-checknet/moodle-core-checknet-debug.js
lib/yui/build/moodle-core-checknet/moodle-core-checknet-min.js
lib/yui/build/moodle-core-checknet/moodle-core-checknet.js
lib/yui/src/checknet/js/checknet.js
message/tests/externallib_test.php
mod/assign/styles.css
mod/chat/lib.php
mod/feedback/classes/responses_table.php
mod/forum/backup/moodle2/restore_forum_stepslib.php
mod/forum/tests/subscriptions_test.php
mod/glossary/lang/en/glossary.php
mod/glossary/lib.php
mod/glossary/print.php
mod/glossary/view.php
mod/quiz/lang/en/quiz.php
mod/quiz/lib.php
mod/quiz/mod_form.php
mod/quiz/override_form.php
mod/quiz/tests/attempts_test.php
mod/scorm/player.php
mod/wiki/locallib.php
question/classes/bank/view.php
report/security/locallib.php
search/engine/solr/classes/engine.php
user/lib.php
user/tests/externallib_test.php
user/tests/userlib_test.php
version.php
webservice/externallib.php

index d2a8950..cc052b4 100644 (file)
@@ -73,6 +73,7 @@ foreach ($rs as $user) {
     echo "Redeleting user $user->id: $user->username ($user->email)\n";
     delete_user($user);
 }
+$rs->close();
 
 cli_heading('Deleting all leftovers');
 
diff --git a/admin/tool/analytics/classes/clihelper.php b/admin/tool/analytics/classes/clihelper.php
new file mode 100644 (file)
index 0000000..c46e6fc
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Helper class that contains helper functions for cli scripts.
+ *
+ * @package   tool_analytics
+ * @copyright 2017 onwards Ankit Agarwal
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_analytics;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Helper class that contains helper functions for cli scripts.
+ *
+ * @package   tool_analytics
+ * @copyright 2017 onwards Ankit Agarwal
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class clihelper {
+
+    /**
+     * List all models in the system. To be used from cli scripts.
+     *
+     * @return void
+     */
+    public static function list_models() {
+        cli_heading("List of models");
+        echo str_pad(get_string('modelid', 'tool_analytics'), 15, ' ') . ' ' . str_pad(get_string('name'), 50, ' ') .
+            ' ' . str_pad(get_string('status'), 15, ' ') . "\n";
+        $models = \core_analytics\manager::get_all_models();
+        foreach ($models as $model) {
+            $modelid = $model->get_id();
+            $isenabled = $model->is_enabled() ? get_string('enabled', 'tool_analytics') : get_string('disabled', 'tool_analytics');
+            $name = $model->get_target()->get_name();
+            echo str_pad($modelid, 15, ' ') . ' ' . str_pad($name, 50, ' ') . ' ' . str_pad($isenabled, 15, ' ') . "\n";
+        }
+    }
+}
\ No newline at end of file
index bc1219f..e31dfec 100644 (file)
@@ -31,6 +31,7 @@ $help = "Enables the provided model.
 
 Options:
 --modelid           Model id
+--list              List models
 --timesplitting     Time splitting method full class name
 -h, --help          Print out this help
 
@@ -42,6 +43,7 @@ Example:
 list($options, $unrecognized) = cli_get_params(
     array(
         'help'            => false,
+        'list'            => false,
         'modelid'         => false,
         'timesplitting'   => false
     ),
@@ -55,7 +57,12 @@ if ($options['help']) {
     exit(0);
 }
 
-if ($options['modelid'] === false || $options['timesplitting'] === false) {
+if ($options['list'] || $options['modelid'] === false) {
+    \tool_analytics\clihelper::list_models();
+    exit(0);
+}
+
+if ($options['timesplitting'] === false) {
     echo $help;
     exit(0);
 }
index 5319f0d..25d182f 100644 (file)
@@ -31,6 +31,7 @@ $help = "Evaluates the provided model.
 
 Options:
 --modelid              Model id
+--list                 List models
 --non-interactive      Not interactive questions
 --timesplitting        Restrict the evaluation to 1 single time splitting method (Optional)
 --filter               Analyser dependant. e.g. A courseid would evaluate the model using a single course (Optional)
@@ -47,6 +48,7 @@ list($options, $unrecognized) = cli_get_params(
     array(
         'help'                  => false,
         'modelid'               => false,
+        'list'                  => false,
         'timesplitting'         => false,
         'reuse-prev-analysed'   => true,
         'non-interactive'       => false,
@@ -62,8 +64,8 @@ if ($options['help']) {
     exit(0);
 }
 
-if ($options['modelid'] === false) {
-    echo $help;
+if ($options['list'] || $options['modelid'] === false) {
+    \tool_analytics\clihelper::list_models();
     exit(0);
 }
 
index f5c1014..68ca347 100644 (file)
@@ -34,6 +34,7 @@ $string['clearmodelpredictions'] = 'Are you sure you want to clear all "{$a}" pr
 $string['clienablemodel'] = 'You can enable the model by selecting a time-splitting method by its ID. Note that you can also enable it later using the web interface (\'none\' to exit).';
 $string['clievaluationandpredictions'] = 'A scheduled task iterates through enabled models and gets predictions. Models evaluation via the web interface is disabled. You can allow these processes to be executed manually via the web interface by disabling the <a href="{$a}">\'onlycli\'</a> analytics setting.';
 $string['clievaluationandpredictionsnoadmin'] = 'A scheduled task iterates through enabled models and gets predictions. Models evaluation via the web interface is disabled. It may be enabled by a site administrator.';
+$string['disabled'] = 'Disabled';
 $string['editmodel'] = 'Edit "{$a}" model';
 $string['edittrainedwarning'] = 'This model has already been trained. Note that changing its indicators or its time-splitting method will delete its previous predictions and start generating new predictions.';
 $string['enabled'] = 'Enabled';
@@ -66,6 +67,7 @@ $string['invalidanalysablestable'] = 'Invalid site analysable elements table';
 $string['invalidprediction'] = 'Invalid to get predictions';
 $string['invalidtraining'] = 'Invalid to train the model';
 $string['loginfo'] = 'Log extra info';
+$string['modelid'] = 'Model id';
 $string['modelinvalidanalysables'] = 'Invalid analysable elements for "{$a}" model';
 $string['modelresults'] = '{$a} results';
 $string['modeltimesplitting'] = 'Time splitting';
index 8d04d45..90c2a12 100644 (file)
@@ -40,6 +40,7 @@ if (empty($classname)) {
     foreach ($records as $record) {
         $instances[] = \core\message\inbound\manager::get_handler($record->classname);
     }
+    $records->close();
 
     echo $OUTPUT->header();
     echo $renderer->messageinbound_handlers_table($instances);
index 63d3485..2f98bf4 100644 (file)
@@ -234,15 +234,19 @@ function search_spammers($keywords) {
     $keywordlist = implode(', ', $keywords);
     echo $OUTPUT->box(get_string('spamresult', 'tool_spamcleaner').s($keywordlist)).' ...';
 
-    print_user_list(array($spamusers_desc,
-                          $spamusers_blog,
-                          $spamusers_blogsub,
-                          $spamusers_comment,
-                          $spamusers_message,
-                          $spamusers_forumpost,
-                          $spamusers_forumpostsub
-                         ),
-                         $keywords);
+    $recordsets = [
+        $spamusers_desc,
+        $spamusers_blog,
+        $spamusers_blogsub,
+        $spamusers_comment,
+        $spamusers_message,
+        $spamusers_forumpost,
+        $spamusers_forumpostsub
+    ];
+    print_user_list($recordsets, $keywords);
+    foreach ($recordsets as $rs) {
+        $rs->close();
+    }
 }
 
 
index 4b0bcac..cdf787e 100644 (file)
@@ -360,6 +360,7 @@ class manager {
             }
             $existingcalculations[$calculation->indicator][$calculation->sampleid] = $calculation->value;
         }
+        $calculations->close();
         return $existingcalculations;
     }
 
index b53ffa6..f17a6ed 100644 (file)
@@ -981,8 +981,8 @@ class restore_process_course_modules_availability extends restore_execution_step
                 $DB->set_field('course_' . $table . 's', 'availability', $newvalue,
                         array('id' => $thingid));
             }
+            $rs->close();
         }
-        $rs->close();
     }
 }
 
index d21b96e..560bb88 100644 (file)
@@ -42,6 +42,7 @@ $badge = new issued_badge($id);
 if (!empty($badge->recipient->id)) {
     if ($bake && ($badge->recipient->id == $USER->id)) {
         $name = str_replace(' ', '_', $badge->badgeclass['name']) . '.png';
+        $name = clean_param($name, PARAM_FILE);
         $filehash = badges_bake($id, $badge->badgeid, $USER->id, true);
         $fs = get_file_storage();
         $file = $fs->get_file_by_hash($filehash);
index 25081bf..e5b2de0 100644 (file)
@@ -159,7 +159,7 @@ class core_badges_external extends external_api {
                     new external_single_structure(
                         array(
                             'id' => new external_value(PARAM_INT, 'Badge id.', VALUE_OPTIONAL),
-                            'name' => new external_value(PARAM_FILE, 'Badge name.'),
+                            'name' => new external_value(PARAM_TEXT, 'Badge name.'),
                             'description' => new external_value(PARAM_NOTAGS, 'Badge description.'),
                             'badgeurl' => new external_value(PARAM_URL, 'Badge URL.'),
                             'timecreated' => new external_value(PARAM_INT, 'Time created.', VALUE_OPTIONAL),
index cebc483..1c2a40a 100644 (file)
@@ -48,8 +48,8 @@ class edit_details_form extends moodleform {
 
         $mform->addElement('header', 'badgedetails', get_string('badgedetails', 'badges'));
         $mform->addElement('text', 'name', get_string('name'), array('size' => '70'));
-        // Using PARAM_FILE to avoid problems later when downloading badge files.
-        $mform->setType('name', PARAM_FILE);
+        // When downloading badge, it will be necessary to clean the name as PARAM_FILE.
+        $mform->setType('name', PARAM_TEXT);
         $mform->addRule('name', null, 'required');
         $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
 
index 8437ad4..97c4aee 100644 (file)
@@ -72,6 +72,7 @@ if ($hide) {
     require_sesskey();
     $badge = new badge($download);
     $name = str_replace(' ', '_', $badge->name) . '.png';
+    $name = clean_param($name, PARAM_FILE);
     $filehash = badges_bake($hash, $download, $USER->id, true);
     $fs = get_file_storage();
     $file = $fs->get_file_by_hash($filehash);
index 0f1f6ca..23ae6dc 100644 (file)
@@ -48,7 +48,7 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
 
         $fordb = new stdClass();
         $fordb->id = null;
-        $fordb->name = "Test badge";
+        $fordb->name = "Test badge with 'apostrophe' and other friends (<>&@#)";
         $fordb->description = "Testing badges";
         $fordb->timecreated = time();
         $fordb->timemodified = time();
index 8c09573..d228bbc 100644 (file)
@@ -31,14 +31,14 @@ Feature: Add badges to the system
   Scenario: Add a badge
     Given I navigate to "Add a new badge" node in "Site administration > Badges"
     And I set the following fields to these values:
-      | Name | Test Badge |
+      | Name | Test badge with 'apostrophe' and other friends (<>&@#) |
       | Description | Test badge description |
       | issuername | Test Badge Site |
       | issuercontact | testuser@example.com |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     When I press "Create badge"
     Then I should see "Edit details"
-    And I should see "Test Badge"
+    And I should see "Test badge with 'apostrophe' and other friends (&@#)"
     And I should not see "Create badge"
     And I follow "Manage badges"
     And I should see "Number of badges available: 1"
index 5d9af03..859acc6 100644 (file)
@@ -221,9 +221,11 @@ class core_calendar_type_testcase extends advanced_testcase {
         $counter++;
 
         if ($element == 'dateselector') {
-            $el = $this->mform->addElement('date_selector', 'dateselector' . $counter, null, array('timezone' => 0.0, 'step' => 1));
+            $el = $this->mform->addElement('date_selector',
+                    'dateselector' . $counter, null, array('timezone' => 0.0));
         } else {
-            $el = $this->mform->addElement('date_time_selector', 'dateselector' . $counter, null, array('timezone' => 0.0, 'step' => 1, 'optional' => false));
+            $el = $this->mform->addElement('date_time_selector',
+                    'dateselector' . $counter, null, array('timezone' => 0.0, 'optional' => false));
         }
         $submitvalues = array('dateselector' . $counter => $date);
 
index f6b0e6f..b7fba4d 100644 (file)
@@ -123,21 +123,10 @@ echo $renderer->start_layout();
 echo html_writer::start_tag('div', array('class'=>'heightcontainer'));
 echo $OUTPUT->heading(get_string('calendar', 'calendar'));
 
-if ($view == 'day' || $view == 'upcoming') {
-    switch($view) {
-        case 'day':
-            list($data, $template) = calendar_get_view($calendar, $view);
-            echo $renderer->render_from_template($template, $data);
-        break;
-        case 'upcoming':
-            list($data, $template) = calendar_get_view($calendar, $view);
-            echo $renderer->render_from_template($template, $data);
-        break;
-    }
-} else if ($view == 'month') {
-    list($data, $template) = calendar_get_view($calendar, $view);
-    echo $renderer->render_from_template($template, $data);
-}
+
+list($data, $template) = calendar_get_view($calendar, $view);
+echo $renderer->render_from_template($template, $data);
+
 echo html_writer::end_tag('div');
 
 list($data, $template) = calendar_get_footer_options($calendar);
index 4de545e..5f257cd 100644 (file)
@@ -504,6 +504,7 @@ function cohort_get_invisible_contexts() {
             $excludedcontexts[] = $ctx->id;
         }
     }
+    $records->close();
     return $excludedcontexts;
 }
 
index 6203b8a..3dd8de4 100644 (file)
@@ -7,7 +7,7 @@
     "require-dev": {
         "phpunit/phpunit": "6.4.*",
         "phpunit/dbUnit": "3.0.*",
-        "moodlehq/behat-extension": "3.34.1",
+        "moodlehq/behat-extension": "3.35.0",
         "mikey179/vfsStream": "^1.6"
     }
 }
index 506116c..f90ae23 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "content-hash": "b36746ace2486c033136c855a63f3793",
+    "content-hash": "7cd70172c941fb07f0a2d4173baef5f1",
     "packages": [],
     "packages-dev": [
         {
         },
         {
             "name": "behat/mink-extension",
-            "version": "v2.2",
+            "version": "2.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Behat/MinkExtension.git",
-                "reference": "5b4bda64ff456104564317e212c823e45cad9d59"
+                "reference": "badc565b7a1d05c4a4bf49c789045bcf7af6c6de"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Behat/MinkExtension/zipball/5b4bda64ff456104564317e212c823e45cad9d59",
-                "reference": "5b4bda64ff456104564317e212c823e45cad9d59",
+                "url": "https://api.github.com/repos/Behat/MinkExtension/zipball/badc565b7a1d05c4a4bf49c789045bcf7af6c6de",
+                "reference": "badc565b7a1d05c4a4bf49c789045bcf7af6c6de",
                 "shasum": ""
             },
             "require": {
-                "behat/behat": "~3.0,>=3.0.5",
-                "behat/mink": "~1.5",
+                "behat/behat": "^3.0.5",
+                "behat/mink": "^1.5",
                 "php": ">=5.3.2",
-                "symfony/config": "~2.2|~3.0"
+                "symfony/config": "^2.7|^3.0|^4.0"
             },
             "require-dev": {
-                "behat/mink-goutte-driver": "~1.1",
-                "phpspec/phpspec": "~2.0"
+                "behat/mink-goutte-driver": "^1.1",
+                "phpspec/phpspec": "^2.0"
             },
             "type": "behat-extension",
             "extra": {
                 "test",
                 "web"
             ],
-            "time": "2016-02-15T07:55:18+00:00"
+            "time": "2017-11-24T19:30:49+00:00"
         },
         {
             "name": "behat/mink-goutte-driver",
         },
         {
             "name": "fabpot/goutte",
-            "version": "v3.2.1",
+            "version": "v3.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/FriendsOfPHP/Goutte.git",
-                "reference": "db5c28f4a010b4161d507d5304e28a7ebf211638"
+                "reference": "395f61d7c2e15a813839769553a4de16fa3b3c96"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/db5c28f4a010b4161d507d5304e28a7ebf211638",
-                "reference": "db5c28f4a010b4161d507d5304e28a7ebf211638",
+                "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/395f61d7c2e15a813839769553a4de16fa3b3c96",
+                "reference": "395f61d7c2e15a813839769553a4de16fa3b3c96",
                 "shasum": ""
             },
             "require": {
                 "guzzlehttp/guzzle": "^6.0",
                 "php": ">=5.5.0",
-                "symfony/browser-kit": "~2.1|~3.0",
-                "symfony/css-selector": "~2.1|~3.0",
-                "symfony/dom-crawler": "~2.1|~3.0"
+                "symfony/browser-kit": "~2.1|~3.0|~4.0",
+                "symfony/css-selector": "~2.1|~3.0|~4.0",
+                "symfony/dom-crawler": "~2.1|~3.0|~4.0"
+            },
+            "require-dev": {
+                "symfony/phpunit-bridge": "^3.3 || ^4"
             },
             "type": "application",
             "extra": {
             "autoload": {
                 "psr-4": {
                     "Goutte\\": "Goutte"
-                }
+                },
+                "exclude-from-classmap": [
+                    "Goutte/Tests"
+                ]
             },
             "notification-url": "https://packagist.org/downloads/",
             "license": [
             "keywords": [
                 "scraper"
             ],
-            "time": "2017-01-03T13:21:43+00:00"
+            "time": "2017-11-19T08:45:40+00:00"
         },
         {
             "name": "guzzlehttp/guzzle",
         },
         {
             "name": "moodlehq/behat-extension",
-            "version": "v3.34.1",
+            "version": "v3.35.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/moodlehq/moodle-behat-extension.git",
         },
         {
             "name": "phpspec/prophecy",
-            "version": "v1.7.2",
+            "version": "1.7.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpspec/prophecy.git",
-                "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6"
+                "reference": "e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6",
-                "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6",
+                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf",
+                "reference": "e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf",
                 "shasum": ""
             },
             "require": {
             },
             "require-dev": {
                 "phpspec/phpspec": "^2.5|^3.2",
-                "phpunit/phpunit": "^4.8 || ^5.6.5"
+                "phpunit/phpunit": "^4.8.35 || ^5.7"
             },
             "type": "library",
             "extra": {
                 "spy",
                 "stub"
             ],
-            "time": "2017-09-04T11:05:03+00:00"
+            "time": "2017-11-24T13:59:53+00:00"
         },
         {
             "name": "phpunit/dbunit",
-            "version": "3.0.1",
+            "version": "3.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/dbunit.git",
-                "reference": "6b9cec80dca8694243aade33bceb425ccafbbd0d"
+                "reference": "403350339b6aca748ee0067d027d85621992e21f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/dbunit/zipball/6b9cec80dca8694243aade33bceb425ccafbbd0d",
-                "reference": "6b9cec80dca8694243aade33bceb425ccafbbd0d",
+                "url": "https://api.github.com/repos/sebastianbergmann/dbunit/zipball/403350339b6aca748ee0067d027d85621992e21f",
+                "reference": "403350339b6aca748ee0067d027d85621992e21f",
                 "shasum": ""
             },
             "require": {
                 "ext-simplexml": "*",
                 "php": "^7.0",
                 "phpunit/phpunit": "^6.0",
-                "symfony/yaml": "^3.0"
+                "symfony/yaml": "^3.0 || ^4.0"
             },
             "type": "library",
             "extra": {
                 "testing",
                 "xunit"
             ],
-            "time": "2017-10-19T13:21:48+00:00"
+            "time": "2017-11-18T17:40:34+00:00"
         },
         {
             "name": "phpunit/php-code-coverage",
         },
         {
             "name": "phpunit/php-file-iterator",
-            "version": "1.4.2",
+            "version": "1.4.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
-                "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5"
+                "reference": "8ebba84e5bd74fc5fdeb916b38749016c7232f93"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3cc8f69b3028d0f96a9078e6295d86e9bf019be5",
-                "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/8ebba84e5bd74fc5fdeb916b38749016c7232f93",
+                "reference": "8ebba84e5bd74fc5fdeb916b38749016c7232f93",
                 "shasum": ""
             },
             "require": {
                 "filesystem",
                 "iterator"
             ],
-            "time": "2016-10-03T07:40:28+00:00"
+            "time": "2017-11-24T15:00:59+00:00"
         },
         {
             "name": "phpunit/php-text-template",
         },
         {
             "name": "symfony/browser-kit",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/browser-kit.git",
-                "reference": "317d5bdf0127f06db7ea294186132b4f5b036839"
+                "reference": "03f957cd24bf939524f07b8b910c89cfcad722a8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/317d5bdf0127f06db7ea294186132b4f5b036839",
-                "reference": "317d5bdf0127f06db7ea294186132b4f5b036839",
+                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/03f957cd24bf939524f07b8b910c89cfcad722a8",
+                "reference": "03f957cd24bf939524f07b8b910c89cfcad722a8",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony BrowserKit Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-02T06:42:24+00:00"
+            "time": "2017-11-07T14:12:55+00:00"
         },
         {
             "name": "symfony/class-loader",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/class-loader.git",
-                "reference": "7572c904b209fa9907c69a6a9a68243c265a4d01"
+                "reference": "df173ac2af96ce202bf8bb5a3fc0bec8a4fdd4d1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/class-loader/zipball/7572c904b209fa9907c69a6a9a68243c265a4d01",
-                "reference": "7572c904b209fa9907c69a6a9a68243c265a4d01",
+                "url": "https://api.github.com/repos/symfony/class-loader/zipball/df173ac2af96ce202bf8bb5a3fc0bec8a4fdd4d1",
+                "reference": "df173ac2af96ce202bf8bb5a3fc0bec8a4fdd4d1",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony ClassLoader Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-02T06:42:24+00:00"
+            "time": "2017-11-05T15:47:03+00:00"
         },
         {
             "name": "symfony/config",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/config.git",
-                "reference": "4ab62407bff9cd97c410a7feaef04c375aaa5cfd"
+                "reference": "8d2649077dc54dfbaf521d31f217383d82303c5f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/config/zipball/4ab62407bff9cd97c410a7feaef04c375aaa5cfd",
-                "reference": "4ab62407bff9cd97c410a7feaef04c375aaa5cfd",
+                "url": "https://api.github.com/repos/symfony/config/zipball/8d2649077dc54dfbaf521d31f217383d82303c5f",
+                "reference": "8d2649077dc54dfbaf521d31f217383d82303c5f",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Config Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-04T18:56:58+00:00"
+            "time": "2017-11-07T14:16:22+00:00"
         },
         {
             "name": "symfony/console",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
-                "reference": "116bc56e45a8e5572e51eb43ab58c769a352366c"
+                "reference": "63cd7960a0a522c3537f6326706d7f3b8de65805"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/console/zipball/116bc56e45a8e5572e51eb43ab58c769a352366c",
-                "reference": "116bc56e45a8e5572e51eb43ab58c769a352366c",
+                "url": "https://api.github.com/repos/symfony/console/zipball/63cd7960a0a522c3537f6326706d7f3b8de65805",
+                "reference": "63cd7960a0a522c3537f6326706d7f3b8de65805",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Console Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-02T06:42:24+00:00"
+            "time": "2017-11-16T15:24:32+00:00"
         },
         {
             "name": "symfony/css-selector",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/css-selector.git",
-                "reference": "07447650225ca9223bd5c97180fe7c8267f7d332"
+                "reference": "66e6e046032ebdf1f562c26928549f613d428bd1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/css-selector/zipball/07447650225ca9223bd5c97180fe7c8267f7d332",
-                "reference": "07447650225ca9223bd5c97180fe7c8267f7d332",
+                "url": "https://api.github.com/repos/symfony/css-selector/zipball/66e6e046032ebdf1f562c26928549f613d428bd1",
+                "reference": "66e6e046032ebdf1f562c26928549f613d428bd1",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony CssSelector Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-02T06:42:24+00:00"
+            "time": "2017-11-05T15:47:03+00:00"
         },
         {
             "name": "symfony/debug",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/debug.git",
-                "reference": "eb95d9ce8f18dcc1b3dfff00cb624c402be78ffd"
+                "reference": "74557880e2846b5c84029faa96b834da37e29810"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/debug/zipball/eb95d9ce8f18dcc1b3dfff00cb624c402be78ffd",
-                "reference": "eb95d9ce8f18dcc1b3dfff00cb624c402be78ffd",
+                "url": "https://api.github.com/repos/symfony/debug/zipball/74557880e2846b5c84029faa96b834da37e29810",
+                "reference": "74557880e2846b5c84029faa96b834da37e29810",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Debug Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-02T06:42:24+00:00"
+            "time": "2017-11-10T16:38:39+00:00"
         },
         {
             "name": "symfony/dependency-injection",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dependency-injection.git",
-                "reference": "8ebad929aee3ca185b05f55d9cc5521670821ad1"
+                "reference": "4e84f5af2c2d51ee3dee72df40b7fc08f49b4ab8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/8ebad929aee3ca185b05f55d9cc5521670821ad1",
-                "reference": "8ebad929aee3ca185b05f55d9cc5521670821ad1",
+                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/4e84f5af2c2d51ee3dee72df40b7fc08f49b4ab8",
+                "reference": "4e84f5af2c2d51ee3dee72df40b7fc08f49b4ab8",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony DependencyInjection Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-04T17:15:30+00:00"
+            "time": "2017-11-13T18:10:32+00:00"
         },
         {
             "name": "symfony/dom-crawler",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dom-crawler.git",
-                "reference": "40dafd42d5dad7fe5ad4e958413d92a207522ac1"
+                "reference": "cebe3c068867956e012d9135282ba6a05d8a259e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/40dafd42d5dad7fe5ad4e958413d92a207522ac1",
-                "reference": "40dafd42d5dad7fe5ad4e958413d92a207522ac1",
+                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/cebe3c068867956e012d9135282ba6a05d8a259e",
+                "reference": "cebe3c068867956e012d9135282ba6a05d8a259e",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony DomCrawler Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-02T06:42:24+00:00"
+            "time": "2017-11-05T15:47:03+00:00"
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "d7ba037e4b8221956ab1e221c73c9e27e05dd423"
+                "reference": "271d8c27c3ec5ecee6e2ac06016232e249d638d9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d7ba037e4b8221956ab1e221c73c9e27e05dd423",
-                "reference": "d7ba037e4b8221956ab1e221c73c9e27e05dd423",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/271d8c27c3ec5ecee6e2ac06016232e249d638d9",
+                "reference": "271d8c27c3ec5ecee6e2ac06016232e249d638d9",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony EventDispatcher Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-02T06:42:24+00:00"
+            "time": "2017-11-05T15:47:03+00:00"
         },
         {
             "name": "symfony/filesystem",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/filesystem.git",
-                "reference": "90bc45abf02ae6b7deb43895c1052cb0038506f1"
+                "reference": "77db266766b54db3ee982fe51868328b887ce15c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/filesystem/zipball/90bc45abf02ae6b7deb43895c1052cb0038506f1",
-                "reference": "90bc45abf02ae6b7deb43895c1052cb0038506f1",
+                "url": "https://api.github.com/repos/symfony/filesystem/zipball/77db266766b54db3ee982fe51868328b887ce15c",
+                "reference": "77db266766b54db3ee982fe51868328b887ce15c",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Filesystem Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-03T13:33:10+00:00"
+            "time": "2017-11-07T14:12:55+00:00"
         },
         {
             "name": "symfony/polyfill-mbstring",
         },
         {
             "name": "symfony/process",
-            "version": "v2.8.28",
+            "version": "v2.8.31",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/process.git",
-                "reference": "26c9fb02bf06bd6b90f661a5bd17e510810d0176"
+                "reference": "d25449e031f600807949aab7cadbf267712f4eee"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/process/zipball/26c9fb02bf06bd6b90f661a5bd17e510810d0176",
-                "reference": "26c9fb02bf06bd6b90f661a5bd17e510810d0176",
+                "url": "https://api.github.com/repos/symfony/process/zipball/d25449e031f600807949aab7cadbf267712f4eee",
+                "reference": "d25449e031f600807949aab7cadbf267712f4eee",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Process Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-01T21:00:16+00:00"
+            "time": "2017-11-05T15:25:56+00:00"
         },
         {
             "name": "symfony/translation",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/translation.git",
-                "reference": "409bf229cd552bf7e3faa8ab7e3980b07672073f"
+                "reference": "373e553477e55cd08f8b86b74db766c75b987fdb"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/translation/zipball/409bf229cd552bf7e3faa8ab7e3980b07672073f",
-                "reference": "409bf229cd552bf7e3faa8ab7e3980b07672073f",
+                "url": "https://api.github.com/repos/symfony/translation/zipball/373e553477e55cd08f8b86b74db766c75b987fdb",
+                "reference": "373e553477e55cd08f8b86b74db766c75b987fdb",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Translation Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-02T06:42:24+00:00"
+            "time": "2017-11-07T14:12:55+00:00"
         },
         {
             "name": "symfony/yaml",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/yaml.git",
-                "reference": "8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46"
+                "reference": "0938408c4faa518d95230deabb5f595bf0de31b9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/yaml/zipball/8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46",
-                "reference": "8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46",
+                "url": "https://api.github.com/repos/symfony/yaml/zipball/0938408c4faa518d95230deabb5f595bf0de31b9",
+                "reference": "0938408c4faa518d95230deabb5f595bf0de31b9",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Yaml Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-05T14:43:42+00:00"
+            "time": "2017-11-10T18:26:04+00:00"
         },
         {
             "name": "theseer/tokenizer",
index d3aba3b..012f8f7 100644 (file)
@@ -504,10 +504,10 @@ class core_course_external extends external_api {
                 $courseinfo['groupmode'] = $course->groupmode;
                 $courseinfo['groupmodeforce'] = $course->groupmodeforce;
                 $courseinfo['defaultgroupingid'] = $course->defaultgroupingid;
-                $courseinfo['lang'] = $course->lang;
+                $courseinfo['lang'] = clean_param($course->lang, PARAM_LANG);
                 $courseinfo['timecreated'] = $course->timecreated;
                 $courseinfo['timemodified'] = $course->timemodified;
-                $courseinfo['forcetheme'] = $course->theme;
+                $courseinfo['forcetheme'] = clean_param($course->theme, PARAM_THEME);
                 $courseinfo['enablecompletion'] = $course->enablecompletion;
                 $courseinfo['completionnotify'] = $course->completionnotify;
                 $courseinfo['courseformatoptions'] = array();
@@ -1726,7 +1726,7 @@ class core_course_external extends external_api {
                         $categoryinfo['visible'] = $category->visible;
                         $categoryinfo['visibleold'] = $category->visibleold;
                         $categoryinfo['timemodified'] = $category->timemodified;
-                        $categoryinfo['theme'] = $category->theme;
+                        $categoryinfo['theme'] = clean_param($category->theme, PARAM_THEME);
                     }
 
                     $categoriesinfo[] = $categoryinfo;
@@ -3068,6 +3068,14 @@ class core_course_external extends external_api {
             foreach ($coursefields as $field) {
                 $coursesdata[$course->id][$field] = $course->{$field};
             }
+
+            // Clean lang and auth fields for external functions (it may content uninstalled themes or language packs).
+            if (isset($coursesdata[$course->id]['theme'])) {
+                $coursesdata[$course->id]['theme'] = clean_param($coursesdata[$course->id]['theme'], PARAM_THEME);
+            }
+            if (isset($coursesdata[$course->id]['lang'])) {
+                $coursesdata[$course->id]['lang'] = clean_param($coursesdata[$course->id]['lang'], PARAM_LANG);
+            }
         }
 
         return array(
index 63aa87f..2b6e4bd 100644 (file)
@@ -2211,6 +2211,21 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $this->assertCount(0, $result['courses']);
     }
 
+    /**
+     * Test get_courses_by_field_invalid_theme_and_lang
+     */
+    public function test_get_courses_by_field_invalid_theme_and_lang() {
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $course = self::getDataGenerator()->create_course(array('theme' => 'kkt', 'lang' => 'kkl'));
+        $result = core_course_external::get_courses_by_field('id', $course->id);
+        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
+        $this->assertEmpty($result['courses']['0']['theme']);
+        $this->assertEmpty($result['courses']['0']['lang']);
+    }
+
+
     public function test_check_updates() {
         global $DB;
         $this->resetAfterTest(true);
index 3a0c38f..55b074c 100644 (file)
@@ -343,7 +343,7 @@ class core_enrol_external extends external_api {
                 'summaryformat' => $course->summaryformat,
                 'format' => $course->format,
                 'showgrades' => $course->showgrades,
-                'lang' => $course->lang,
+                'lang' => clean_param($course->lang, PARAM_LANG),
                 'enablecompletion' => $course->enablecompletion,
                 'category' => $course->category,
                 'progress' => $progress,
@@ -453,7 +453,11 @@ class core_enrol_external extends external_api {
                                                $params['perpage']);
 
         $results = array();
-        $requiredfields = ['id', 'fullname', 'profileimageurl', 'profileimageurlsmall'];
+        // Add also extra user fields.
+        $requiredfields = array_merge(
+            ['id', 'fullname', 'profileimageurl', 'profileimageurlsmall'],
+            get_extra_user_fields($context)
+        );
         foreach ($users['users'] as $id => $user) {
             // Note: We pass the course here to validate that the current user can at least view user details in this course.
             // The user we are looking at is not in this course yet though - but we only fetch the minimal set of
index d85f4d3..22c3c5c 100644 (file)
@@ -376,8 +376,12 @@ class core_enrol_externallib_testcase extends externallib_advanced_testcase {
             'enddate'          => $timenow + WEEKSECS
         );
 
+        $coursedata2 = array(
+            'lang'             => 'kk', // Check invalid language pack.
+        );
+
         $course1 = self::getDataGenerator()->create_course($coursedata1);
-        $course2 = self::getDataGenerator()->create_course();
+        $course2 = self::getDataGenerator()->create_course($coursedata2);
         $courses = array($course1, $course2);
 
         // Enrol $USER in the courses.
@@ -414,6 +418,9 @@ class core_enrol_externallib_testcase extends externallib_advanced_testcase {
                 foreach ($coursedata1 as $fieldname => $value) {
                     $this->assertEquals($courseenrol[$fieldname], $course1->$fieldname);
                 }
+            } else {
+                // Check language pack. Should be empty since an incorrect one was used when creating the course.
+                $this->assertEmpty($courseenrol['lang']);
             }
         }
     }
index b79f5c7..2f06662 100644 (file)
@@ -428,6 +428,7 @@ abstract class gradingform_controller {
         foreach ($records as $record) {
             $rv[] = $this->get_instance($record);
         }
+        $records->close();
         return $rv;
     }
 
index 56709df..db3ecb6 100644 (file)
@@ -419,6 +419,7 @@ abstract class screen {
         while ($user = $gui->next_user()) {
             $users[$user->user->id] = $user->user;
         }
+        $gui->close();
         return $users;
     }
 
index 2c09e6d..ae0923b 100644 (file)
@@ -118,7 +118,7 @@ if ($mform_post->is_cancelled()) {
             //decode encoded commas
             $record[$header[$key]] = preg_replace($csv_encode, $csv_delimiter, trim($value));
         }
-        if ($record[$header[0]]) {
+        if (trim($rawline) !== '') {
             // add a new group to the database
 
             // add fields to object $user
@@ -134,28 +134,32 @@ if ($mform_post->is_cancelled()) {
                 }
             }
 
-            if (isset($newgroup->idnumber)){
+            if (!empty($newgroup->idnumber)) {
                 //if idnumber is set, we use that.
                 //unset invalid courseid
                 if (!$mycourse = $DB->get_record('course', array('idnumber'=>$newgroup->idnumber))) {
                     echo $OUTPUT->notification(get_string('unknowncourseidnumber', 'error', $newgroup->idnumber));
                     unset($newgroup->courseid);//unset so 0 doesn't get written to database
+                } else {
+                    $newgroup->courseid = $mycourse->id;
                 }
-                $newgroup->courseid = $mycourse->id;
 
-            } else if (isset($newgroup->coursename)){
+            } else if (!empty($newgroup->coursename)) {
                 //else use course short name to look up
                 //unset invalid coursename (if no id)
-                if (!$mycourse = $DB->get_record('course', array('shortname', $newgroup->coursename))) {
+                if (!$mycourse = $DB->get_record('course', array('shortname' => $newgroup->coursename))) {
                     echo $OUTPUT->notification(get_string('unknowncourse', 'error', $newgroup->coursename));
                     unset($newgroup->courseid);//unset so 0 doesn't get written to database
+                } else {
+                    $newgroup->courseid = $mycourse->id;
                 }
-                $newgroup->courseid = $mycourse->id;
 
             } else {
                 //else use use current id
                 $newgroup->courseid = $id;
             }
+            unset($newgroup->idnumber);
+            unset($newgroup->coursename);
 
             //if courseid is set
             if (isset($newgroup->courseid)) {
@@ -196,7 +200,7 @@ if ($mform_post->is_cancelled()) {
                     }
 
                     // Add group to grouping
-                    if (!empty($newgroup->groupingname) || is_numeric($newgroup->groupingname)) {
+                    if (isset($newgroup->groupingname) && strlen($newgroup->groupingname)) {
                         $groupingname = $newgroup->groupingname;
                         if (! $groupingid = groups_get_grouping_by_name($newgroup->courseid, $groupingname)) {
                             $data = new stdClass();
index 5df273b..57bd616 100644 (file)
@@ -104,6 +104,9 @@ function groups_add_member($grouporid, $userorid, $component=null, $itemid=0) {
     $DB->set_field('groups', 'timemodified', $member->timeadded, array('id'=>$groupid));
     $group->timemodified = $member->timeadded;
 
+    // Invalidate the group and grouping cache for users.
+    cache_helper::invalidate_by_definition('core', 'user_group_groupings', array(), array($userid));
+
     // Trigger group event.
     $params = array(
         'context' => $context,
@@ -205,6 +208,9 @@ function groups_remove_member($grouporid, $userorid) {
     $DB->set_field('groups', 'timemodified', $time, array('id' => $groupid));
     $group->timemodified = $time;
 
+    // Invalidate the group and grouping cache for users.
+    cache_helper::invalidate_by_definition('core', 'user_group_groupings', array(), array($userid));
+
     // Trigger group event.
     $params = array(
         'context' => context_course::instance($group->courseid),
@@ -496,6 +502,8 @@ function groups_delete_group($grouporid) {
 
     // Invalidate the grouping cache for the course
     cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($group->courseid));
+    // Purge the group and grouping cache for users.
+    cache_helper::purge_by_definition('core', 'user_group_groupings');
 
     // Trigger group event.
     $params = array(
@@ -547,6 +555,8 @@ function groups_delete_grouping($groupingorid) {
 
     // Invalidate the grouping cache for the course
     cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($grouping->courseid));
+    // Purge the group and grouping cache for users.
+    cache_helper::purge_by_definition('core', 'user_group_groupings');
 
     // Trigger group event.
     $params = array(
@@ -618,9 +628,12 @@ function groups_delete_groupings_groups($courseid, $showfeedback=false) {
     foreach ($results as $result) {
         groups_unassign_grouping($result->groupingid, $result->groupid, false);
     }
+    $results->close();
 
     // Invalidate the grouping cache for the course
     cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($courseid));
+    // Purge the group and grouping cache for users.
+    cache_helper::purge_by_definition('core', 'user_group_groupings');
 
     // TODO MDL-41312 Remove events_trigger_legacy('groups_groupings_groups_removed').
     // This event is kept here for backwards compatibility, because it cannot be
@@ -646,9 +659,12 @@ function groups_delete_groups($courseid, $showfeedback=false) {
     foreach ($groups as $group) {
         groups_delete_group($group);
     }
+    $groups->close();
 
     // Invalidate the grouping cache for the course
     cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($courseid));
+    // Purge the group and grouping cache for users.
+    cache_helper::purge_by_definition('core', 'user_group_groupings');
 
     // TODO MDL-41312 Remove events_trigger_legacy('groups_groups_deleted').
     // This event is kept here for backwards compatibility, because it cannot be
@@ -676,9 +692,12 @@ function groups_delete_groupings($courseid, $showfeedback=false) {
     foreach ($groupings as $grouping) {
         groups_delete_grouping($grouping);
     }
+    $groupings->close();
 
     // Invalidate the grouping cache for the course.
     cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($courseid));
+    // Purge the group and grouping cache for users.
+    cache_helper::purge_by_definition('core', 'user_group_groupings');
 
     // TODO MDL-41312 Remove events_trigger_legacy('groups_groupings_deleted').
     // This event is kept here for backwards compatibility, because it cannot be
@@ -818,7 +837,7 @@ function groups_parse_name($format, $groupnumber) {
  * @param int groupingid
  * @param int groupid
  * @param int $timeadded  The time the group was added to the grouping.
- * @param bool $invalidatecache If set to true the course group cache will be invalidated as well.
+ * @param bool $invalidatecache If set to true the course group cache and the user group cache will be invalidated as well.
  * @return bool true or exception
  */
 function groups_assign_grouping($groupingid, $groupid, $timeadded = null, $invalidatecache = true) {
@@ -841,6 +860,8 @@ function groups_assign_grouping($groupingid, $groupid, $timeadded = null, $inval
     if ($invalidatecache) {
         // Invalidate the grouping cache for the course
         cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($courseid));
+        // Purge the group and grouping cache for users.
+        cache_helper::purge_by_definition('core', 'user_group_groupings');
     }
 
     // Trigger event.
@@ -860,7 +881,7 @@ function groups_assign_grouping($groupingid, $groupid, $timeadded = null, $inval
  *
  * @param int groupingid
  * @param int groupid
- * @param bool $invalidatecache If set to true the course group cache will be invalidated as well.
+ * @param bool $invalidatecache If set to true the course group cache and the user group cache will be invalidated as well.
  * @return bool success
  */
 function groups_unassign_grouping($groupingid, $groupid, $invalidatecache = true) {
@@ -871,6 +892,8 @@ function groups_unassign_grouping($groupingid, $groupid, $invalidatecache = true
     if ($invalidatecache) {
         // Invalidate the grouping cache for the course
         cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($courseid));
+        // Purge the group and grouping cache for users.
+        cache_helper::purge_by_definition('core', 'user_group_groupings');
     }
 
     // Trigger event.
index 0bec1e3..95019f1 100644 (file)
@@ -8,12 +8,14 @@ Feature: Importing of groups and groupings
     Given the following "courses" exist:
       | fullname | shortname | category |
       | Course 1 | C1 | 0 |
+      | Course 2 | C2 | 0 |
     And the following "users" exist:
       | username | firstname | lastname | email |
       | teacher1 | Teacher | 1 | teacher1@example.com |
     And the following "course enrolments" exist:
       | user | course | role |
       | teacher1 | C1 | editingteacher |
+      | teacher1 | C2 | editingteacher |
 
   @javascript
   Scenario: Import groups and groupings as teacher
@@ -110,3 +112,32 @@ Feature: Importing of groups and groupings
     And I press "Edit group settings"
     And the field "id_idnumber" matches value ""
     And I press "Cancel"
+
+  @javascript
+  Scenario: Import groups into multiple courses as a teacher
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I navigate to "Users > Groups" in current page administration
+    And I press "Import groups"
+    When I upload "group/tests/fixtures/groups_import_multicourse.csv" file to "Import" filemanager
+    And I press "Import groups"
+    Then I should see "Group group7 added successfully"
+    And I should see "Unknown course named \"C-non-existing\""
+    And I should see "Group group8 added successfully"
+    And I should not see "group-will-not-be-created"
+    And I should see "Group group9 added successfully"
+    And I should see "Group group10 added successfully"
+    And I press "Continue"
+    And I should see "group10"
+    And I should see "group7"
+    And I should see "group8"
+    And I should not see "group9"
+    And I should not see "group-will-not-be-created"
+    And I am on "Course 2" course homepage
+    And I navigate to "Users > Groups" in current page administration
+    And I should see "group9"
+    And I should not see "group-will-not-be-created"
+    And I should not see "group7"
+    And I should not see "group8"
+    And I should not see "group10"
+    And I log out
diff --git a/group/tests/fixtures/groups_import_multicourse.csv b/group/tests/fixtures/groups_import_multicourse.csv
new file mode 100644 (file)
index 0000000..cbedd60
--- /dev/null
@@ -0,0 +1,6 @@
+coursename,groupname
+C1,group7
+C-non-existing,group-will-not-be-created
+C1,group8
+C2,group9
+,group10
index caa0991..88ecab1 100644 (file)
@@ -72,6 +72,7 @@ $string['cachedef_string'] = 'Language string cache';
 $string['cachedef_tags'] = 'Tags collections and areas';
 $string['cachedef_temp_tables'] = 'Temporary tables cache';
 $string['cachedef_userselections'] = 'Data used to persist user selections throughout Moodle';
+$string['cachedef_user_group_groupings'] = 'User\'s groupings and groups per course';
 $string['cachedef_yuimodules'] = 'YUI Module definitions';
 $string['cachelock_file_default'] = 'Default file locking';
 $string['cachestores'] = 'Cache stores';
index 8c42e62..b1adc3f 100644 (file)
@@ -1161,6 +1161,7 @@ function badges_download($userid) {
         // Need to make image name user-readable and unique using filename safe characters.
         $name =  $badge->name . ' ' . userdate($issued->dateissued, '%d %b %Y') . ' ' . hash('crc32', $badge->id);
         $name = str_replace(' ', '_', $name);
+        $name = clean_param($name, PARAM_FILE);
         if ($file = $fs->get_file($context->id, 'badges', 'userbadge', $issued->badgeid, '/', $issued->uniquehash . '.png')) {
             $filelist[$name . '.png'] = $file;
         }
index 81d563f..219a378 100644 (file)
@@ -800,6 +800,7 @@ class block_manager {
                 $unknown[] = $bi;
             }
         }
+        $blockinstances->close();
 
         // Pages don't necessarily have a defaultregion. The  one time this can
         // happen is when there are no theme block regions, but the script itself
index 8534df6..9bc16cd 100644 (file)
@@ -74,6 +74,7 @@ class manager {
                 self::remove_messageinbound_handler($handler);
             }
         }
+        $existinghandlers->close();
 
         self::create_missing_messageinbound_handlers_for_component($componentname);
     }
index 78e94f4..80f57d5 100644 (file)
@@ -43,6 +43,7 @@ class portfolio extends base {
         foreach ($rs as $repository) {
             $enabled[$repository->plugin] = $repository->plugin;
         }
+        $rs->close();
 
         return $enabled;
     }
@@ -91,4 +92,4 @@ class portfolio extends base {
 
         parent::uninstall_cleanup();
     }
-}
\ No newline at end of file
+}
index 70d2202..4f57dd7 100644 (file)
@@ -354,4 +354,12 @@ $definitions = array(
         'simpledata' => true,
         'staticacceleration' => false,
     ),
+
+    // Caches grouping and group ids of a user.
+    'user_group_groupings' => array(
+        'mode' => cache_store::MODE_APPLICATION,
+        'simplekeys' => true,
+        'simpledata' => true,
+        'staticacceleration' => true,
+    ),
 );
index 91eb81f..88cdee7 100644 (file)
@@ -1183,6 +1183,7 @@ function xmldb_main_upgrade($oldversion) {
             $i++;
             $pbar->update($i, $total, "Updating duplicate question category stamp - $i/$total.");
         }
+        $rs->close();
         unset($usedstamps);
 
         // The uniqueness of each (contextid, stamp) pair is now guaranteed, so add the unique index to stop future duplicates.
index 44d298c..b6114e5 100644 (file)
@@ -45,6 +45,12 @@ class pgsql_native_moodle_database extends moodle_database {
     /** @var bool savepoint hack for MDL-35506 - workaround for automatic transaction rollback on error */
     protected $savepointpresent = false;
 
+    /** @var int Number of cursors used (for constructing a unique ID) */
+    protected $cursorcount = 0;
+
+    /** @var int Default number of rows to fetch at a time when using recordsets with cursors */
+    const DEFAULT_FETCH_BUFFER_SIZE = 100000;
+
     /**
      * Detects if all needed PHP stuff installed.
      * Note: can be used before connect()
@@ -734,14 +740,89 @@ class pgsql_native_moodle_database extends moodle_database {
         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
 
         $this->query_start($sql, $params, SQL_QUERY_SELECT);
-        $result = pg_query_params($this->pgsql, $sql, $params);
+
+        // For any query that doesn't explicitly specify a limit, we must use cursors to stop it
+        // loading the entire thing (unless the config setting is turned off).
+        $usecursors = !$limitnum && ($this->get_fetch_buffer_size() > 0);
+        if ($usecursors) {
+            // Work out the cursor unique identifer. This is based on a simple count used which
+            // should be OK because the identifiers only need to be unique within the current
+            // transaction.
+            $this->cursorcount++;
+            $cursorname = 'crs' . $this->cursorcount;
+
+            // Do the query to a cursor.
+            $sql = 'DECLARE ' . $cursorname . ' NO SCROLL CURSOR WITH HOLD FOR ' . $sql;
+            $result = pg_query_params($this->pgsql, $sql, $params);
+        } else {
+            $result = pg_query_params($this->pgsql, $sql, $params);
+            $cursorname = '';
+        }
+
         $this->query_end($result);
+        if ($usecursors) {
+            pg_free_result($result);
+            $result = null;
+        }
 
-        return $this->create_recordset($result);
+        return new pgsql_native_moodle_recordset($result, $this, $cursorname);
     }
 
-    protected function create_recordset($result) {
-        return new pgsql_native_moodle_recordset($result);
+    /**
+     * Gets size of fetch buffer used for recordset queries.
+     *
+     * If this returns 0 then cursors will not be used, meaning recordset queries will occupy enough
+     * memory as needed for the Postgres library to hold the entire query results in memory.
+     *
+     * @return int Fetch buffer size or 0 indicating not to use cursors
+     */
+    protected function get_fetch_buffer_size() {
+        if (array_key_exists('fetchbuffersize', $this->dboptions)) {
+            return (int)$this->dboptions['fetchbuffersize'];
+        } else {
+            return self::DEFAULT_FETCH_BUFFER_SIZE;
+        }
+    }
+
+    /**
+     * Retrieves data from cursor. For use by recordset only; do not call directly.
+     *
+     * Return value contains the next batch of Postgres data, and a boolean indicating if this is
+     * definitely the last batch (if false, there may be more)
+     *
+     * @param string $cursorname Name of cursor to read from
+     * @return array Array with 2 elements (next data batch and boolean indicating last batch)
+     */
+    public function fetch_from_cursor($cursorname) {
+        $count = $this->get_fetch_buffer_size();
+
+        $sql = 'FETCH ' . $count . ' FROM ' . $cursorname;
+
+        $this->query_start($sql, [], SQL_QUERY_AUX);
+        $result = pg_query($this->pgsql, $sql);
+        $last = pg_num_rows($result) !== $count;
+
+        $this->query_end($result);
+
+        return [$result, $last];
+    }
+
+    /**
+     * Closes a cursor. For use by recordset only; do not call directly.
+     *
+     * @param string $cursorname Name of cursor to close
+     * @return bool True if we actually closed one, false if the transaction was cancelled
+     */
+    public function close_cursor($cursorname) {
+        // If the transaction got cancelled, then ignore this request.
+        $sql = 'CLOSE ' . $cursorname;
+        $this->query_start($sql, [], SQL_QUERY_AUX);
+        $result = pg_query($this->pgsql, $sql);
+        $this->query_end($result);
+        if ($result) {
+            pg_free_result($result);
+        }
+        return true;
     }
 
     /**
@@ -1366,7 +1447,7 @@ class pgsql_native_moodle_database extends moodle_database {
     protected function begin_transaction() {
         $this->savepointpresent = true;
         $sql = "BEGIN ISOLATION LEVEL READ COMMITTED; SAVEPOINT moodle_pg_savepoint";
-        $this->query_start($sql, NULL, SQL_QUERY_AUX);
+        $this->query_start($sql, null, SQL_QUERY_AUX);
         $result = pg_query($this->pgsql, $sql);
         $this->query_end($result);
 
@@ -1381,7 +1462,7 @@ class pgsql_native_moodle_database extends moodle_database {
     protected function commit_transaction() {
         $this->savepointpresent = false;
         $sql = "RELEASE SAVEPOINT moodle_pg_savepoint; COMMIT";
-        $this->query_start($sql, NULL, SQL_QUERY_AUX);
+        $this->query_start($sql, null, SQL_QUERY_AUX);
         $result = pg_query($this->pgsql, $sql);
         $this->query_end($result);
 
@@ -1396,7 +1477,7 @@ class pgsql_native_moodle_database extends moodle_database {
     protected function rollback_transaction() {
         $this->savepointpresent = false;
         $sql = "RELEASE SAVEPOINT moodle_pg_savepoint; ROLLBACK";
-        $this->query_start($sql, NULL, SQL_QUERY_AUX);
+        $this->query_start($sql, null, SQL_QUERY_AUX);
         $result = pg_query($this->pgsql, $sql);
         $this->query_end($result);
 
index dc99f5b..a6eed21 100644 (file)
@@ -40,26 +40,64 @@ class pgsql_native_moodle_recordset extends moodle_recordset {
     protected $current;
     protected $blobs = array();
 
+    /** @var string Name of cursor or '' if none */
+    protected $cursorname;
+
+    /** @var pgsql_native_moodle_database Postgres database resource */
+    protected $db;
+
+    /** @var bool True if there are no more rows to fetch from the cursor */
+    protected $lastbatch;
+
     /**
      * Build a new recordset to iterate over.
      *
-     * @param resource $result A pg_query() result object to create a recordset from.
+     * When using cursors, $result will be null initially.
+     *
+     * @param resource|null $result A pg_query() result object to create a recordset from.
+     * @param pgsql_native_moodle_database $db Database object (only required when using cursors)
+     * @param string $cursorname Name of cursor or '' if none
      */
-    public function __construct($result) {
+    public function __construct($result, pgsql_native_moodle_database $db = null, $cursorname = '') {
+        if ($cursorname && !$db) {
+            throw new coding_exception('When specifying a cursor, $db is required');
+        }
         $this->result = $result;
+        $this->db = $db;
+        $this->cursorname = $cursorname;
+
+        // When there is a cursor, do the initial fetch.
+        if ($cursorname) {
+            $this->fetch_cursor_block();
+        }
 
         // Find out if there are any blobs.
-        $numfields = pg_num_fields($result);
+        $numfields = pg_num_fields($this->result);
         for ($i = 0; $i < $numfields; $i++) {
-            $type = pg_field_type($result, $i);
+            $type = pg_field_type($this->result, $i);
             if ($type == 'bytea') {
-                $this->blobs[] = pg_field_name($result, $i);
+                $this->blobs[] = pg_field_name($this->result, $i);
             }
         }
 
         $this->current = $this->fetch_next();
     }
 
+    /**
+     * Fetches the next block of data when using cursors.
+     *
+     * @throws coding_exception If you call this when the fetch buffer wasn't freed yet
+     */
+    protected function fetch_cursor_block() {
+        if ($this->result) {
+            throw new coding_exception('Unexpected non-empty result when fetching from cursor');
+        }
+        list($this->result, $this->lastbatch) = $this->db->fetch_from_cursor($this->cursorname);
+        if (!$this->result) {
+            throw new coding_exception('Unexpected failure when fetching from cursor');
+        }
+    }
+
     public function __destruct() {
         $this->close();
     }
@@ -69,9 +107,21 @@ class pgsql_native_moodle_recordset extends moodle_recordset {
             return false;
         }
         if (!$row = pg_fetch_assoc($this->result)) {
+            // There are no more rows in this result.
             pg_free_result($this->result);
             $this->result = null;
-            return false;
+
+            // If using a cursor, can we fetch the next block?
+            if ($this->cursorname && !$this->lastbatch) {
+                $this->fetch_cursor_block();
+                if (!$row = pg_fetch_assoc($this->result)) {
+                    pg_free_result($this->result);
+                    $this->result = null;
+                    return false;
+                }
+            } else {
+                return false;
+            }
         }
 
         if ($this->blobs) {
@@ -111,5 +161,11 @@ class pgsql_native_moodle_recordset extends moodle_recordset {
         }
         $this->current = null;
         $this->blobs   = null;
+
+        // If using cursors, close the cursor.
+        if ($this->cursorname) {
+            $this->db->close_cursor($this->cursorname);
+            $this->cursorname = null;
+        }
     }
 }
diff --git a/lib/dml/tests/pgsql_native_recordset_test.php b/lib/dml/tests/pgsql_native_recordset_test.php
new file mode 100644 (file)
index 0000000..07ebe6f
--- /dev/null
@@ -0,0 +1,432 @@
+<?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/>.
+
+/**
+ * Test specific features of the Postgres dml support relating to recordsets.
+ *
+ * @package core
+ * @category test
+ * @copyright 2017 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot.'/lib/dml/pgsql_native_moodle_database.php');
+
+/**
+ * Test specific features of the Postgres dml support relating to recordsets.
+ *
+ * @package core
+ * @category test
+ * @copyright 2017 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class pgsql_native_recordset_testcase extends basic_testcase {
+
+    /** @var pgsql_native_moodle_database Special database connection */
+    protected $specialdb;
+
+    /**
+     * Creates a second db connection and a temp table with values in for testing.
+     */
+    protected function setUp() {
+        global $DB;
+
+        parent::setUp();
+
+        // Skip tests if not using Postgres.
+        if (!($DB instanceof pgsql_native_moodle_database)) {
+            $this->markTestSkipped('Postgres-only test');
+        }
+    }
+
+    /**
+     * Initialises database connection with given fetch buffer size
+     * @param int $fetchbuffersize Size of fetch buffer
+     */
+    protected function init_db($fetchbuffersize) {
+        global $CFG, $DB;
+
+        // To make testing easier, create a database with the same dboptions as the real one,
+        // but a low number for the cursor size.
+        $this->specialdb = \moodle_database::get_driver_instance('pgsql', 'native', true);
+        $dboptions = ['fetchbuffersize' => $fetchbuffersize];
+        $this->specialdb->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname,
+                $DB->get_prefix(), $dboptions);
+
+        // Create a temp table.
+        $dbman = $this->specialdb->get_manager();
+        $table = new xmldb_table('silly_test_table');
+        $table->add_field('id', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, XMLDB_SEQUENCE);
+        $table->add_field('msg', XMLDB_TYPE_CHAR, 255);
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
+        $dbman->create_temp_table($table);
+
+        // Add some records to the table.
+        for ($index = 1; $index <= 7; $index++) {
+            $this->specialdb->insert_record('silly_test_table', ['msg' => 'record' . $index]);
+        }
+    }
+
+    /**
+     * Gets rid of the second db connection.
+     */
+    protected function tearDown() {
+        if ($this->specialdb) {
+            $table = new xmldb_table('silly_test_table');
+            $this->specialdb->get_manager()->drop_table($table);
+            $this->specialdb->dispose();
+            $this->specialdb = null;
+        }
+        parent::tearDown();
+    }
+
+    /**
+     * Tests that get_recordset_sql works when using cursors, which it does when no limit is
+     * specified.
+     */
+    public function test_recordset_cursors() {
+        $this->init_db(3);
+
+        // Query the table and check the actual queries using debug mode, also check the count.
+        $this->specialdb->set_debug(true);
+        $before = $this->specialdb->perf_get_queries();
+        ob_start();
+        $rs = $this->specialdb->get_recordset_sql('SELECT * FROM {silly_test_table} ORDER BY id');
+        $index = 0;
+        foreach ($rs as $rec) {
+            $index++;
+            $this->assertEquals('record' . $index, $rec->msg);
+        }
+        $this->assertEquals(7, $index);
+        $rs->close();
+        $debugging = ob_get_contents();
+        ob_end_clean();
+
+        // Expect 4 fetches - first three, next three, last one (with 2).
+        $this->assert_query_regexps([
+                '~SELECT \* FROM~',
+                '~FETCH 3 FROM crs1~',
+                '~FETCH 3 FROM crs1~',
+                '~FETCH 3 FROM crs1~',
+                '~CLOSE crs1~'], $debugging);
+
+        // There should have been 7 queries tracked for perf log.
+        $this->assertEquals(5, $this->specialdb->perf_get_queries() - $before);
+
+        // Try a second time - this time we'll request exactly 3 items so that it has to query
+        // twice (as it can't tell if the first batch is the last).
+        $before = $this->specialdb->perf_get_queries();
+        ob_start();
+        $rs = $this->specialdb->get_recordset_sql(
+                'SELECT * FROM {silly_test_table} WHERE id <= ? ORDER BY id', [3]);
+        $index = 0;
+        foreach ($rs as $rec) {
+            $index++;
+            $this->assertEquals('record' . $index, $rec->msg);
+        }
+        $this->assertEquals(3, $index);
+        $rs->close();
+        $debugging = ob_get_contents();
+        ob_end_clean();
+
+        $this->specialdb->set_debug(false);
+
+        // Expect 2 fetches - first three, then next one (empty).
+        $this->assert_query_regexps([
+                '~SELECT \* FROM~',
+                '~FETCH 3 FROM crs2~',
+                '~FETCH 3 FROM crs2~',
+                '~CLOSE crs2~'], $debugging);
+
+        // There should have been 4 queries tracked for perf log.
+        $this->assertEquals(4, $this->specialdb->perf_get_queries() - $before);
+    }
+
+    /**
+     * Tests that get_recordset_sql works when using cursors and when there are two overlapping
+     * recordsets being used.
+     */
+    public function test_recordset_cursors_overlapping() {
+        $this->init_db(3);
+
+        $rs1 = $this->specialdb->get_recordset('silly_test_table', null, 'id');
+        $rs2 = $this->specialdb->get_recordset('silly_test_table', null, 'id DESC');
+
+        // Read first 3 from first recordset.
+        $read = [];
+        $read[] = $rs1->current()->id;
+        $rs1->next();
+        $read[] = $rs1->current()->id;
+        $rs1->next();
+        $read[] = $rs1->current()->id;
+        $rs1->next();
+        $this->assertEquals([1, 2, 3], $read);
+
+        // Read 5 from second recordset.
+        $read = [];
+        $read[] = $rs2->current()->id;
+        $rs2->next();
+        $read[] = $rs2->current()->id;
+        $rs2->next();
+        $read[] = $rs2->current()->id;
+        $rs2->next();
+        $read[] = $rs2->current()->id;
+        $rs2->next();
+        $read[] = $rs2->current()->id;
+        $rs2->next();
+        $this->assertEquals([7, 6, 5, 4, 3], $read);
+
+        // Now read remainder of first recordset and close it.
+        $read = [];
+        $read[] = $rs1->current()->id;
+        $rs1->next();
+        $read[] = $rs1->current()->id;
+        $rs1->next();
+        $read[] = $rs1->current()->id;
+        $rs1->next();
+        $read[] = $rs1->current()->id;
+        $rs1->next();
+        $this->assertFalse($rs1->valid());
+        $this->assertEquals([4, 5, 6, 7], $read);
+        $rs1->close();
+
+        // And remainder of second.
+        $read = [];
+        $read[] = $rs2->current()->id;
+        $rs2->next();
+        $read[] = $rs2->current()->id;
+        $rs2->next();
+        $this->assertFalse($rs2->valid());
+        $this->assertEquals([2, 1], $read);
+        $rs2->close();
+    }
+
+    /**
+     * Tests that get_recordset_sql works when using cursors and transactions inside.
+     */
+    public function test_recordset_cursors_transaction_inside() {
+        $this->init_db(3);
+
+        // Transaction inside the recordset processing.
+        $rs = $this->specialdb->get_recordset('silly_test_table', null, 'id');
+        $read = [];
+        foreach ($rs as $rec) {
+            $read[] = $rec->id;
+            $transaction = $this->specialdb->start_delegated_transaction();
+            $transaction->allow_commit();
+        }
+        $this->assertEquals([1, 2, 3, 4, 5, 6, 7], $read);
+        $rs->close();
+    }
+
+    /**
+     * Tests that get_recordset_sql works when using cursors and a transaction outside.
+     */
+    public function test_recordset_cursors_transaction_outside() {
+        $this->init_db(3);
+
+        // Transaction outside the recordset processing.
+        $transaction = $this->specialdb->start_delegated_transaction();
+        $rs = $this->specialdb->get_recordset('silly_test_table', null, 'id');
+        $read = [];
+        foreach ($rs as $rec) {
+            $read[] = $rec->id;
+        }
+        $this->assertEquals([1, 2, 3, 4, 5, 6, 7], $read);
+        $rs->close();
+        $transaction->allow_commit();
+    }
+
+    /**
+     * Tests that get_recordset_sql works when using cursors and a transaction overlapping.
+     */
+    public function test_recordset_cursors_transaction_overlapping_before() {
+        $this->init_db(3);
+
+        // Transaction outside the recordset processing.
+        $transaction = $this->specialdb->start_delegated_transaction();
+        $rs = $this->specialdb->get_recordset('silly_test_table', null, 'id');
+        $transaction->allow_commit();
+        $read = [];
+        foreach ($rs as $rec) {
+            $read[] = $rec->id;
+        }
+        $this->assertEquals([1, 2, 3, 4, 5, 6, 7], $read);
+        $rs->close();
+    }
+
+    /**
+     * Tests that get_recordset_sql works when using cursors and a transaction overlapping.
+     */
+    public function test_recordset_cursors_transaction_overlapping_after() {
+        $this->init_db(3);
+
+        // Transaction outside the recordset processing.
+        $rs = $this->specialdb->get_recordset('silly_test_table', null, 'id');
+        $transaction = $this->specialdb->start_delegated_transaction();
+        $read = [];
+        foreach ($rs as $rec) {
+            $read[] = $rec->id;
+        }
+        $this->assertEquals([1, 2, 3, 4, 5, 6, 7], $read);
+        $rs->close();
+        $transaction->allow_commit();
+    }
+
+    /**
+     * Tests that get_recordset_sql works when using cursors and a transaction that 'fails' and gets
+     * rolled back.
+     */
+    public function test_recordset_cursors_transaction_rollback() {
+        $this->init_db(3);
+
+        try {
+            $rs = $this->specialdb->get_recordset('silly_test_table', null, 'id');
+            $transaction = $this->specialdb->start_delegated_transaction();
+            $this->specialdb->delete_records('silly_test_table', ['id' => 5]);
+            $transaction->rollback(new dml_transaction_exception('rollback please'));
+            $this->fail('should not get here');
+        } catch (dml_transaction_exception $e) {
+            $this->assertContains('rollback please', $e->getMessage());
+        } finally {
+
+            // Rollback should not kill our recordset.
+            $read = [];
+            foreach ($rs as $rec) {
+                $read[] = $rec->id;
+            }
+            $this->assertEquals([1, 2, 3, 4, 5, 6, 7], $read);
+
+            // This would happen in real code (that isn't within the same function) anyway because
+            // it would go out of scope.
+            $rs->close();
+        }
+
+        // OK, transaction aborted, now get the recordset again and check nothing was deleted.
+        $rs = $this->specialdb->get_recordset('silly_test_table', null, 'id');
+        $read = [];
+        foreach ($rs as $rec) {
+            $read[] = $rec->id;
+        }
+        $this->assertEquals([1, 2, 3, 4, 5, 6, 7], $read);
+        $rs->close();
+    }
+
+    /**
+     * Tests that get_recordset_sql works when not using cursors, because a limit is specified.
+     */
+    public function test_recordset_no_cursors_limit() {
+        $this->init_db(3);
+
+        $this->specialdb->set_debug(true);
+        $before = $this->specialdb->perf_get_queries();
+        ob_start();
+        $rs = $this->specialdb->get_recordset_sql(
+                'SELECT * FROM {silly_test_table} ORDER BY id', [], 0, 100);
+        $index = 0;
+        foreach ($rs as $rec) {
+            $index++;
+            $this->assertEquals('record' . $index, $rec->msg);
+        }
+        $this->assertEquals(7, $index);
+        $rs->close();
+        $this->specialdb->set_debug(false);
+        $debugging = ob_get_contents();
+        ob_end_clean();
+
+        // Expect direct request without using cursors.
+        $this->assert_query_regexps(['~SELECT \* FROM~'], $debugging);
+
+        // There should have been 1 query tracked for perf log.
+        $this->assertEquals(1, $this->specialdb->perf_get_queries() - $before);
+    }
+
+    /**
+     * Tests that get_recordset_sql works when not using cursors, because the config setting turns
+     * them off.
+     */
+    public function test_recordset_no_cursors_config() {
+        $this->init_db(0);
+
+        $this->specialdb->set_debug(true);
+        $before = $this->specialdb->perf_get_queries();
+        ob_start();
+        $rs = $this->specialdb->get_recordset_sql('SELECT * FROM {silly_test_table} ORDER BY id');
+        $index = 0;
+        foreach ($rs as $rec) {
+            $index++;
+            $this->assertEquals('record' . $index, $rec->msg);
+        }
+        $this->assertEquals(7, $index);
+        $rs->close();
+        $this->specialdb->set_debug(false);
+        $debugging = ob_get_contents();
+        ob_end_clean();
+
+        // Expect direct request without using cursors.
+        $this->assert_query_regexps(['~SELECT \* FROM~'], $debugging);
+
+        // There should have been 1 query tracked for perf log.
+        $this->assertEquals(1, $this->specialdb->perf_get_queries() - $before);
+    }
+
+    /**
+     * Asserts that database debugging output matches the expected list of SQL queries, specified
+     * as an array of regular expressions.
+     *
+     * @param string[] $expected Expected regular expressions
+     * @param string $debugging Debugging text from the database
+     */
+    protected function assert_query_regexps(array $expected, $debugging) {
+        $lines = explode("\n", $debugging);
+        $index = 0;
+        $params = false;
+        foreach ($lines as $line) {
+            if ($params) {
+                if ($line === ')]') {
+                    $params = false;
+                }
+                continue;
+            }
+            // Skip irrelevant lines.
+            if (preg_match('~^---~', $line)) {
+                continue;
+            }
+            if (preg_match('~^Query took~', $line)) {
+                continue;
+            }
+            if (trim($line) === '') {
+                continue;
+            }
+            // Skip param lines.
+            if ($line === '[array (') {
+                $params = true;
+                continue;
+            }
+            if (!array_key_exists($index, $expected)) {
+                $this->fail('More queries than expected');
+            }
+            $this->assertRegExp($expected[$index++], $line);
+        }
+        if (array_key_exists($index, $expected)) {
+            $this->fail('Fewer queries than expected');
+        }
+    }
+
+}
index df3ecbc..d69094b 100644 (file)
@@ -2431,7 +2431,11 @@ abstract class enrol_plugin {
         $participants->close();
 
         // now clean up all remainders that were not removed correctly
-        $DB->delete_records('groups_members', array('itemid'=>$instance->id, 'component'=>'enrol_'.$name));
+        if ($gms = $DB->get_records('groups_members', array('itemid' => $instance->id, 'component' => 'enrol_' . $name))) {
+            foreach ($gms as $gm) {
+                groups_remove_member($gm->groupid, $gm->userid);
+            }
+        }
         $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>'enrol_'.$name));
         $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
 
index 0e17a81..6b4f1dc 100644 (file)
@@ -4088,7 +4088,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null, $offlin
         $filename = array_pop($args);
 
         if ($filearea === 'badgeimage') {
-            if ($filename !== 'f1' && $filename !== 'f2') {
+            if ($filename !== 'f1' && $filename !== 'f2' && $filename !== 'f3') {
                 send_file_not_found();
             }
             if (!$file = $fs->get_file($context->id, 'badges', 'badgeimage', $badge->id, '/', $filename.'.png')) {
index 15b4ef4..49960e2 100644 (file)
@@ -2001,6 +2001,7 @@ class file_storage {
         foreach ($rs as $filerecord) {
             $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
         }
+        $rs->close();
 
         return $files;
     }
index a1ddedb..da095b9 100644 (file)
@@ -79,7 +79,7 @@ class MoodleQuickForm_date_selector extends MoodleQuickForm_group {
         // Get the calendar type used - see MDL-18375.
         $calendartype = \core_calendar\type_factory::get_calendar_instance();
         $this->_options = array('startyear' => $calendartype->get_min_year(), 'stopyear' => $calendartype->get_max_year(),
-            'defaulttime' => 0, 'timezone' => 99, 'step' => 5, 'optional' => false);
+            'defaulttime' => 0, 'timezone' => 99, 'step' => 1, 'optional' => false);
         // TODO MDL-52313 Replace with the call to parent::__construct().
         HTML_QuickForm_element::__construct($elementName, $elementLabel, $attributes);
         $this->_persistantFreeze = true;
index 7a6644e..311e233 100644 (file)
@@ -81,7 +81,7 @@ class MoodleQuickForm_date_time_selector extends MoodleQuickForm_group {
         // Get the calendar type used - see MDL-18375.
         $calendartype = \core_calendar\type_factory::get_calendar_instance();
         $this->_options = array('startyear' => $calendartype->get_min_year(), 'stopyear' => $calendartype->get_max_year(),
-            'defaulttime' => 0, 'timezone' => 99, 'step' => 5, 'optional' => false);
+            'defaulttime' => 0, 'timezone' => 99, 'step' => 1, 'optional' => false);
 
         // TODO MDL-52313 Replace with the call to parent::__construct().
         HTML_QuickForm_element::__construct($elementName, $elementLabel, $attributes);
index b8512aa..ab8929a 100644 (file)
@@ -77,7 +77,7 @@ class MoodleQuickForm_defaultcustom extends MoodleQuickForm_group {
             'startyear' => $calendartype->get_min_year(),
             'stopyear' => $calendartype->get_max_year(),
             'defaulttime' => 0,
-            'step' => 5,
+            'step' => 1,
             'optional' => false,
         ];
 
index 8893d13..2f36760 100644 (file)
@@ -472,38 +472,54 @@ function groups_get_user_groups($courseid, $userid=0) {
         $userid = $USER->id;
     }
 
-    $sql = "SELECT g.id, gg.groupingid
-              FROM {groups} g
-                   JOIN {groups_members} gm   ON gm.groupid = g.id
-              LEFT JOIN {groupings_groups} gg ON gg.groupid = g.id
-             WHERE gm.userid = ? AND g.courseid = ?";
-    $params = array($userid, $courseid);
+    $cache = cache::make('core', 'user_group_groupings');
 
-    $rs = $DB->get_recordset_sql($sql, $params);
+    // Try to retrieve group ids from the cache.
+    $usergroups = $cache->get($userid);
 
-    if (!$rs->valid()) {
-        $rs->close(); // Not going to iterate (but exit), close rs
-        return array('0' => array());
-    }
+    if ($usergroups === false) {
+        $sql = "SELECT g.id, g.courseid, gg.groupingid
+                  FROM {groups} g
+                  JOIN {groups_members} gm ON gm.groupid = g.id
+             LEFT JOIN {groupings_groups} gg ON gg.groupid = g.id
+                 WHERE gm.userid = ?";
+
+        $rs = $DB->get_recordset_sql($sql, array($userid));
 
-    $result    = array();
-    $allgroups = array();
+        $usergroups = array();
+        $allgroups  = array();
 
-    foreach ($rs as $group) {
-        $allgroups[$group->id] = $group->id;
-        if (is_null($group->groupingid)) {
-            continue;
+        foreach ($rs as $group) {
+            if (!array_key_exists($group->courseid, $allgroups)) {
+                $allgroups[$group->courseid] = array();
+            }
+            $allgroups[$group->courseid][$group->id] = $group->id;
+            if (!array_key_exists($group->courseid, $usergroups)) {
+                $usergroups[$group->courseid] = array();
+            }
+            if (is_null($group->groupingid)) {
+                continue;
+            }
+            if (!array_key_exists($group->groupingid, $usergroups[$group->courseid])) {
+                $usergroups[$group->courseid][$group->groupingid] = array();
+            }
+            $usergroups[$group->courseid][$group->groupingid][$group->id] = $group->id;
         }
-        if (!array_key_exists($group->groupingid, $result)) {
-            $result[$group->groupingid] = array();
+        $rs->close();
+
+        foreach (array_keys($allgroups) as $cid) {
+            $usergroups[$cid]['0'] = array_keys($allgroups[$cid]); // All user groups in the course.
         }
-        $result[$group->groupingid][$group->id] = $group->id;
-    }
-    $rs->close();
 
-    $result['0'] = array_keys($allgroups); // all groups
+        // Cache the data.
+        $cache->set($userid, $usergroups);
+    }
 
-    return $result;
+    if (array_key_exists($courseid, $usergroups)) {
+        return $usergroups[$courseid];
+    } else {
+        return array('0' => array());
+    }
 }
 
 /**
index 7c46fc3..9f22f18 100644 (file)
@@ -3258,6 +3258,7 @@ class global_navigation_for_ajax extends global_navigation {
                 foreach ($categories as $category){
                     $coursesubcategories = array_merge($coursesubcategories, explode('/', trim($category->path, "/")));
                 }
+                $categories->close();
                 $coursesubcategories = array_unique($coursesubcategories);
 
                 // Only add a subcategory if it is part of the path to user's course and
index 941375b..26a3447 100644 (file)
@@ -4464,7 +4464,9 @@ class action_menu implements renderable, templatable {
 
         if ($actionicon instanceof pix_icon) {
             $primary->icon = $actionicon->export_for_pix();
-            $primary->title = !empty($actionicon->attributes['alt']) ? $this->actionicon->attributes['alt'] : '';
+            if (!empty($actionicon->attributes['alt'])) {
+                $primary->title = $actionicon->attributes['alt'];
+            }
         } else {
             $primary->iconraw = $actionicon ? $output->render($actionicon) : '';
         }
index d1f1adc..835c751 100644 (file)
@@ -1499,9 +1499,9 @@ class table_sql extends flexible_table {
      * method or if other_cols returns NULL then put the data straight into the
      * table.
      *
-     * @return void
+     * After calling this function, don't forget to call close_recordset.
      */
-    function build_table() {
+    public function build_table() {
 
         if ($this->rawdata instanceof \Traversable && !$this->rawdata->valid()) {
             return;
@@ -1515,10 +1515,16 @@ class table_sql extends flexible_table {
             $this->add_data_keyed($formattedrow,
                 $this->get_row_class($row));
         }
+    }
 
-        if ($this->rawdata instanceof \core\dml\recordset_walk ||
-                $this->rawdata instanceof moodle_recordset) {
+    /**
+     * Closes recordset (for use after building the table).
+     */
+    public function close_recordset() {
+        if ($this->rawdata && ($this->rawdata instanceof \core\dml\recordset_walk ||
+                $this->rawdata instanceof moodle_recordset)) {
             $this->rawdata->close();
+            $this->rawdata = null;
         }
     }
 
@@ -1629,6 +1635,7 @@ class table_sql extends flexible_table {
         $this->setup();
         $this->query_db($pagesize, $useinitialsbar);
         $this->build_table();
+        $this->close_recordset();
         $this->finish_output();
     }
 }
index 4a8b682..5332008 100644 (file)
@@ -676,6 +676,7 @@ abstract class testing_util {
                     $mysqlsequences[$table] = $info->auto_increment;
                 }
             }
+            $rs->close();
         }
 
         foreach ($data as $table => $records) {
index 25ca9bf..3ee6b81 100644 (file)
Binary files a/lib/yui/build/moodle-core-checknet/moodle-core-checknet-debug.js and b/lib/yui/build/moodle-core-checknet/moodle-core-checknet-debug.js differ
index cd16c7b..0fe879b 100644 (file)
Binary files a/lib/yui/build/moodle-core-checknet/moodle-core-checknet-min.js and b/lib/yui/build/moodle-core-checknet/moodle-core-checknet-min.js differ
index a89d4d1..52d5b54 100644 (file)
Binary files a/lib/yui/build/moodle-core-checknet/moodle-core-checknet.js and b/lib/yui/build/moodle-core-checknet/moodle-core-checknet.js differ
index 04572af..595620e 100644 (file)
@@ -34,6 +34,16 @@ function CheckNet() {
 }
 
 Y.extend(CheckNet, Y.Base, {
+    /**
+     * Zero-based count of alerts displayed.
+     *
+     * @property _alertCount
+     * @type Number
+     * @private
+     * @default 0
+     */
+    _alertCount: 0,
+
     /**
      * A link to the warning dialogue.
      *
@@ -64,7 +74,6 @@ Y.extend(CheckNet, Y.Base, {
     _scheduleCheck: function() {
         // Schedule the next check after five seconds.
         Y.later(this.get('frequency'), this, this._performCheck);
-
         return this;
     },
 
@@ -116,11 +125,15 @@ Y.extend(CheckNet, Y.Base, {
                             } else {
                                 this._alertDialogue.show();
                             }
+                            this._alertCount++;
                         }
                     }
-
-                    // Start the next check.
-                    this._scheduleCheck();
+                    // If max alert not modified in args, check indefinitely.
+                    // Once max alert count iteration is reached, stop checking.
+                    if (this.get('maxalerts') === -1 || (this.get('maxalerts') - 1) >= this._alertCount) {
+                        // Start the next check.
+                        this._scheduleCheck();
+                    }
                 }
             }
         });
@@ -180,6 +193,17 @@ Y.extend(CheckNet, Y.Base, {
                 'networkdropped',
                 'moodle'
             ]
+        },
+
+        /**
+         * Maxiumum count (not zero-based) of alerts to display for a single page load.
+         *
+         * @attribute maxalerts
+         * @type Number
+         * @value -1
+         */
+        maxalerts: {
+            value: -1
         }
     }
 });
index 8f28c29..b7392f1 100644 (file)
@@ -902,15 +902,15 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $this->send_message($sender3, $recipient, 'Notification', 1);
 
         core_message_external::mark_all_notifications_as_read($recipient->id, $sender1->id);
-        $readnotifications = $DB->get_recordset('message_read', ['useridto' => $recipient->id]);
-        $unreadnotifications = $DB->get_recordset('message', ['useridto' => $recipient->id]);
+        $readnotifications = $DB->get_records('message_read', ['useridto' => $recipient->id]);
+        $unreadnotifications = $DB->get_records('message', ['useridto' => $recipient->id]);
 
         $this->assertCount(2, $readnotifications);
         $this->assertCount(4, $unreadnotifications);
 
         core_message_external::mark_all_notifications_as_read($recipient->id, 0);
-        $readnotifications = $DB->get_recordset('message_read', ['useridto' => $recipient->id]);
-        $unreadnotifications = $DB->get_recordset('message', ['useridto' => $recipient->id]);
+        $readnotifications = $DB->get_records('message_read', ['useridto' => $recipient->id]);
+        $unreadnotifications = $DB->get_records('message', ['useridto' => $recipient->id]);
 
         $this->assertCount(6, $readnotifications);
         $this->assertCount(0, $unreadnotifications);
index f1b36a1..245e5b4 100644 (file)
@@ -56,9 +56,8 @@
     padding: 0;
 }
 
-.path-mod-assign .gradingbatchoperationsform .mform fieldset {
-    margin: 0;
-    padding: 0;
+.path-mod-assign textarea.quickgrade {
+    resize: both;
 }
 
 .path-mod-assign td.submissionstatus,
index ef91dc1..f16e8a9 100644 (file)
@@ -566,7 +566,7 @@ function chat_get_latest_message($chatid, $groupid=0) {
 
     $sql = "SELECT *
         FROM {chat_messages_current} WHERE chatid = :chatid $groupselect
-        ORDER BY timestamp DESC";
+        ORDER BY timestamp DESC, id DESC";
 
     // Return the lastest one message.
     return $DB->get_record_sql($sql, $params, true);
index f4d4ffa..94ab2c6 100644 (file)
@@ -504,8 +504,6 @@ class mod_feedback_responses_table extends table_sql {
             }
         }
         $this->build_table_chunk($chunk, $columnsgroups);
-
-        $this->rawdata->close();
     }
 
     /**
@@ -631,6 +629,7 @@ class mod_feedback_responses_table extends table_sql {
         }
         $this->query_db($this->pagesize, false);
         $this->build_table();
+        $this->close_recordset();
         return $this->dataforexternal;
     }
 }
index f7521c0..771fd41 100644 (file)
@@ -168,8 +168,14 @@ class restore_forum_activity_structure_step extends restore_activity_structure_s
         $data->forum = $this->get_new_parentid('forum');
         $data->userid = $this->get_mappingid('user', $data->userid);
 
-        $newitemid = $DB->insert_record('forum_subscriptions', $data);
-        $this->set_mapping('forum_subscription', $oldid, $newitemid, true);
+        // Create only a new subscription if it does not already exist (see MDL-59854).
+        if ($subscription = $DB->get_record('forum_subscriptions',
+                array('forum' => $data->forum, 'userid' => $data->userid))) {
+            $this->set_mapping('forum_subscription', $oldid, $subscription->id, true);
+        } else {
+            $newitemid = $DB->insert_record('forum_subscriptions', $data);
+            $this->set_mapping('forum_subscription', $oldid, $newitemid, true);
+        }
 
     }
 
index f435f00..276ccf7 100644 (file)
@@ -28,11 +28,12 @@ global $CFG;
 require_once($CFG->dirroot . '/mod/forum/lib.php');
 
 class mod_forum_subscriptions_testcase extends advanced_testcase {
-
     /**
      * Test setUp.
      */
     public function setUp() {
+        global $DB;
+
         // We must clear the subscription caches. This has to be done both before each test, and after in case of other
         // tests using these functions.
         \mod_forum\subscriptions::reset_forum_cache();
@@ -973,11 +974,11 @@ class mod_forum_subscriptions_testcase extends advanced_testcase {
         // Reset the subscription cache.
         \mod_forum\subscriptions::reset_forum_cache();
 
-        // Filling the subscription cache should only use a single query.
+        // Filling the subscription cache should use a query.
         $startcount = $DB->perf_get_reads();
         $this->assertNull(\mod_forum\subscriptions::fill_subscription_cache($forum->id));
         $postfillcount = $DB->perf_get_reads();
-        $this->assertEquals(1, $postfillcount - $startcount);
+        $this->assertNotEquals($postfillcount, $startcount);
 
         // Now fetch some subscriptions from that forum - these should use
         // the cache and not perform additional queries.
@@ -1049,7 +1050,7 @@ class mod_forum_subscriptions_testcase extends advanced_testcase {
         $result = \mod_forum\subscriptions::fill_subscription_cache_for_course($course->id, $user->id);
         $this->assertNull($result);
         $postfillcount = $DB->perf_get_reads();
-        $this->assertEquals(1, $postfillcount - $startcount);
+        $this->assertNotEquals($postfillcount, $startcount);
         $this->assertFalse(\mod_forum\subscriptions::fetch_subscription_cache($disallowforum->id, $user->id));
         $this->assertFalse(\mod_forum\subscriptions::fetch_subscription_cache($chooseforum->id, $user->id));
         $this->assertTrue(\mod_forum\subscriptions::fetch_subscription_cache($initialforum->id, $user->id));
@@ -1064,7 +1065,7 @@ class mod_forum_subscriptions_testcase extends advanced_testcase {
             $this->assertTrue(\mod_forum\subscriptions::fetch_subscription_cache($initialforum->id, $user->id));
         }
         $finalcount = $DB->perf_get_reads();
-        $this->assertEquals(count($users), $finalcount - $postfillcount);
+        $this->assertNotEquals($finalcount, $postfillcount);
     }
 
     /**
@@ -1117,7 +1118,7 @@ class mod_forum_subscriptions_testcase extends advanced_testcase {
         $startcount = $DB->perf_get_reads();
         $this->assertNull(\mod_forum\subscriptions::fill_discussion_subscription_cache($forum->id));
         $postfillcount = $DB->perf_get_reads();
-        $this->assertEquals(1, $postfillcount - $startcount);
+        $this->assertNotEquals($postfillcount, $startcount);
 
         // Now fetch some subscriptions from that forum - these should use
         // the cache and not perform additional queries.
@@ -1184,7 +1185,7 @@ class mod_forum_subscriptions_testcase extends advanced_testcase {
             $this->assertInternalType('array', $result);
         }
         $finalcount = $DB->perf_get_reads();
-        $this->assertEquals(20, $finalcount - $startcount);
+        $this->assertNotEquals($finalcount, $startcount);
     }
 
     /**
index d82168d..92c07fb 100644 (file)
@@ -151,6 +151,7 @@ $string['entrydeleted'] = 'Entry deleted';
 $string['entryexported'] = 'Entry successfully exported';
 $string['entryishidden'] = '(this entry is currently hidden)';
 $string['entryleveldefaultsettings'] = 'Entry level default settings';
+$string['entrylink'] = 'Entry link: {$a}';
 $string['entrysaved'] = 'This entry has been saved';
 $string['entryupdated'] = 'This entry has been updated';
 $string['entryusedynalink'] = 'This entry should be automatically linked';
index bad8317..b3c9f92 100644 (file)
@@ -1226,11 +1226,10 @@ function glossary_print_entry_icons($course, $cm, $glossary, $entry, $mode='',$h
 
     $context = context_module::instance($cm->id);
 
-    $output = false;   //To decide if we must really return text in "return". Activate when needed only!
+    $output = false;   // To decide if we must really return text in "return". Activate when needed only!
     $importedentry = ($entry->sourceglossaryid == $glossary->id);
     $ismainglossary = $glossary->mainglossary;
 
-
     $return = '<span class="commands">';
     // Differentiate links for each entry.
     $altsuffix = strip_tags(format_text($entry->concept));
@@ -1241,6 +1240,11 @@ function glossary_print_entry_icons($course, $cm, $glossary, $entry, $mode='',$h
             array('class' => 'glossary-hidden-note'));
     }
 
+    // Entry link.
+    $return .= '<a class="icon" title="' . get_string('entrylink', 'glossary', $altsuffix) . '" ' .
+               ' href="showentry.php?eid=' . $entry->id . '">' .
+               $OUTPUT->pix_icon('e/insert_edit_link', get_string('entrylink', 'glossary', $altsuffix)) . '</a>';
+
     if (has_capability('mod/glossary:approve', $context) && !$glossary->defaultapproval && $entry->approved) {
         $output = true;
         $return .= '<a class="icon" title="' . get_string('disapprove', 'glossary').
@@ -3795,7 +3799,7 @@ function glossary_get_search_terms_sql(array $terms, $fullsearch = true, $glossa
  * @param  array $options Accepts:
  *                        - (bool) includenotapproved. When false, includes the non-approved entries created by
  *                          the current user. When true, also includes the ones that the user has the permission to approve.
- * @return array The first element being the recordset, the second the number of entries.
+ * @return array The first element being the array of results, the second the number of entries.
  * @since Moodle 3.1
  */
 function glossary_get_entries_by_search($glossary, $context, $query, $fullsearch, $order, $sort, $from, $limit,
index ece5651..292d892 100644 (file)
@@ -216,6 +216,10 @@ if ( $allentries ) {
 
         glossary_print_entry($course, $cm, $glossary, $entry, $mode, $hook, 1, $displayformat, true);
     }
+    // The all entries value may be a recordset or an array.
+    if ($allentries instanceof moodle_recordset) {
+        $allentries->close();
+    }
 }
 
 echo $OUTPUT->footer();
index b0ecb65..4ee7cc1 100644 (file)
@@ -521,6 +521,10 @@ if ($allentries) {
         glossary_print_entry($course, $cm, $glossary, $entry, $mode, $hook,1,$displayformat);
         $entriesshown++;
     }
+    // The all entries value may be a recordset or an array.
+    if ($allentries instanceof moodle_recordset) {
+        $allentries->close();
+    }
 }
 if ( !$entriesshown ) {
     echo $OUTPUT->box(get_string("noentries","glossary"), "generalbox boxaligncenter boxwidthwide");
index 9546ea0..5f0eb1e 100644 (file)
@@ -95,6 +95,7 @@ $string['attemptlast'] = 'Last attempt';
 $string['attemptnumber'] = 'Attempt';
 $string['attemptquiznow'] = 'Attempt quiz now';
 $string['attempts'] = 'Attempts';
+$string['attempts_help'] = 'The total number of attempts allowed (not the number of extra attempts).';
 $string['attemptsallowed'] = 'Attempts allowed';
 $string['attemptsdeleted'] = 'Quiz attempts deleted';
 $string['attemptselection'] = 'Select which attempts to analyze per user:';
index 9b2b227..209c318 100644 (file)
@@ -225,9 +225,16 @@ function quiz_delete_override($quiz, $overrideid) {
     $override = $DB->get_record('quiz_overrides', array('id' => $overrideid), '*', MUST_EXIST);
 
     // Delete the events.
-    $events = $DB->get_records('event', array('modulename' => 'quiz',
-            'instance' => $quiz->id, 'groupid' => (int)$override->groupid,
-            'userid' => (int)$override->userid));
+    if (isset($override->groupid)) {
+        // Create the search array for a group override.
+        $eventsearcharray = array('modulename' => 'quiz',
+            'instance' => $quiz->id, 'groupid' => (int)$override->groupid);
+    } else {
+        // Create the search array for a user override.
+        $eventsearcharray = array('modulename' => 'quiz',
+            'instance' => $quiz->id, 'userid' => (int)$override->userid);
+    }
+    $events = $DB->get_records('event', $eventsearcharray);
     foreach ($events as $event) {
         $eventold = calendar_event::load($event);
         $eventold->delete();
index 6a97bd1..0ebe3da 100644 (file)
@@ -37,7 +37,7 @@ require_once($CFG->dirroot . '/mod/quiz/locallib.php');
  */
 class mod_quiz_mod_form extends moodleform_mod {
     /** @var array options to be used with date_time_selector fields in the quiz. */
-    public static $datefieldoptions = array('optional' => true, 'step' => 1);
+    public static $datefieldoptions = array('optional' => true);
 
     protected $_feedbacks;
     protected static $reviewfields = array(); // Initialised in the constructor.
index d97eea2..695f82c 100644 (file)
@@ -200,6 +200,7 @@ class quiz_override_form extends moodleform {
         }
         $mform->addElement('select', 'attempts',
                 get_string('attemptsallowed', 'quiz'), $attemptoptions);
+        $mform->addHelpButton('attempts', 'attempts', 'quiz');
         $mform->setDefault('attempts', $this->quiz->attempts);
 
         // Submit buttons.
index 901ca66..af8a4e6 100644 (file)
@@ -306,6 +306,7 @@ class mod_quiz_attempt_overdue_testcase extends advanced_testcase {
             $count++;
 
         }
+        $attempts->close();
         $this->assertEquals($DB->count_records_select('quiz_attempts', 'timecheckstate IS NOT NULL'), $count);
 
         $attempts = $overduehander->get_list_of_overdue_attempts(0); // before all attempts
@@ -313,6 +314,7 @@ class mod_quiz_attempt_overdue_testcase extends advanced_testcase {
         foreach ($attempts as $attempt) {
             $count++;
         }
+        $attempts->close();
         $this->assertEquals(0, $count);
 
     }
index 156f508..3ab6512 100644 (file)
@@ -284,9 +284,14 @@ if (file_exists($CFG->dirroot.'/mod/scorm/datamodels/'.$scorm->version.'.php'))
 
 // Add the checknet system to keep checking for a connection.
 $PAGE->requires->string_for_js('networkdropped', 'mod_scorm');
-$PAGE->requires->yui_module('moodle-core-checknet', 'M.core.checknet.init', array(array(
+// Build arguments to send to checknet JS.
+$args = array(
     'message' => array('networkdropped', 'mod_scorm'),
-)));
+    'frequency' => 30000, // Frequency of network check.
+    'timeout' => 10000, // Timeout of network check.
+    'maxalerts' => 1 // Max number of alerts to be thrown.
+);
+$PAGE->requires->yui_module('moodle-core-checknet', 'M.core.checknet.init', array($args));
 echo $OUTPUT->footer();
 
 // Set the start time of this SCO.
index 725b531..f3fe838 100644 (file)
@@ -1240,7 +1240,7 @@ function wiki_delete_page_versions($deleteversions, $context = null) {
             list($insql, $param) = $DB->get_in_or_equal($versions);
             $insql .= ' AND pageid = ?';
             array_push($param, $params['pageid']);
-            $oldversions = $DB->get_recordset_select('wiki_versions', 'version ' . $insql, $param);
+            $oldversions = $DB->get_records_select('wiki_versions', 'version ' . $insql, $param);
             $DB->delete_records_select('wiki_versions', 'version ' . $insql, $param);
         }
         foreach ($oldversions as $version) {
index 102531d..8c4142b 100644 (file)
@@ -408,6 +408,7 @@ class view {
         $questions = $DB->get_recordset_sql($this->loadsql, $this->sqlparams, $page * $perpage, $perpage);
         if (!$questions->valid()) {
             // No questions on this page. Reset to page 0.
+            $questions->close();
             $questions = $DB->get_recordset_sql($this->loadsql, $this->sqlparams, 0, $perpage);
         }
         return $questions;
@@ -708,6 +709,7 @@ class view {
             $this->print_table_row($question, $rowcount);
             $rowcount += 1;
         }
+        $questions->close();
         $this->end_table();
         echo "</div>\n";
 
index 59123c4..65fba97 100644 (file)
@@ -828,6 +828,7 @@ function report_security_check_riskbackup($detailed=false) {
                                'contextname'=>$context->get_context_name());
             $users[] = '<li>'.get_string('check_riskbackup_unassign', 'report_security', $a).'</li>';
         }
+        $rs->close();
         if (!empty($users)) {
             $users = '<ul>'.implode('', $users).'</ul>';
             $result->details .= get_string('check_riskbackup_details_users', 'report_security', $users);
index 55206d3..1f0a1be 100644 (file)
@@ -1227,6 +1227,9 @@ class engine extends \core_search\engine {
             }
         }
 
+        // Set timeout as for Solr client.
+        $options['CURLOPT_TIMEOUT'] = !empty($this->config->server_timeout) ? $this->config->server_timeout : '30';
+
         $this->curl->setopt($options);
 
         if (!empty($this->config->server_username) && !empty($this->config->server_password)) {
index 3466576..6d1cf60 100644 (file)
@@ -564,6 +564,14 @@ function user_get_user_details($user, $course = null, array $userfields = array(
         }
     }
 
+    // Clean lang and auth fields for external functions (it may content uninstalled themes or language packs).
+    if (isset($userdetails['lang'])) {
+        $userdetails['lang'] = clean_param($userdetails['lang'], PARAM_LANG);
+    }
+    if (isset($userdetails['theme'])) {
+        $userdetails['theme'] = clean_param($userdetails['theme'], PARAM_THEME);
+    }
+
     return $userdetails;
 }
 
index 5359960..3afffd8 100644 (file)
@@ -222,8 +222,10 @@ class core_user_externallib_testcase extends externallib_advanced_testcase {
             'descriptionformat' => FORMAT_MOODLE,
             'city' => 'Perth',
             'url' => 'http://moodle.org',
-            'country' => 'AU'
-            );
+            'country' => 'AU',
+            'lang' => 'kkl',
+            'theme' => 'kkt',
+        );
         $user1 = self::getDataGenerator()->create_user($user1);
         if (!empty($CFG->usetags)) {
             require_once($CFG->dirroot . '/user/editlib.php');
@@ -328,6 +330,11 @@ class core_user_externallib_testcase extends externallib_advanced_testcase {
                 if (!empty($CFG->usetags) and !empty($generateduser->interests)) {
                     $this->assertEquals(implode(', ', $generateduser->interests), $returneduser['interests']);
                 }
+                // Check empty since incorrect values were used when creating the user.
+                if ($returneduser['id'] == $user1->id) {
+                    $this->assertEmpty($returneduser['lang']);
+                    $this->assertEmpty($returneduser['theme']);
+                }
             }
         }
 
index 0168929..bc5347c 100644 (file)
@@ -720,14 +720,16 @@ class core_userliblib_testcase extends advanced_testcase {
      * calling user_get_user_details() function.
      */
     public function test_user_get_user_details_missing_fields() {
+        global $CFG;
+
         $this->resetAfterTest(true);
         $this->setAdminUser(); // We need capabilities to view the data.
         $user = self::getDataGenerator()->create_user([
                                                           'auth'       => 'auth_something',
                                                           'confirmed'  => '0',
                                                           'idnumber'   => 'someidnumber',
-                                                          'lang'       => 'en_ar',
-                                                          'theme'      => 'mytheme',
+                                                          'lang'       => 'en',
+                                                          'theme'      => $CFG->theme,
                                                           'timezone'   => '50',
                                                           'mailformat' => '0',
                                                       ]);
@@ -737,8 +739,8 @@ class core_userliblib_testcase extends advanced_testcase {
         self::assertSame('auth_something', $got['auth']);
         self::assertSame('0', $got['confirmed']);
         self::assertSame('someidnumber', $got['idnumber']);
-        self::assertSame('en_ar', $got['lang']);
-        self::assertSame('mytheme', $got['theme']);
+        self::assertSame('en', $got['lang']);
+        self::assertSame($CFG->theme, $got['theme']);
         self::assertSame('50', $got['timezone']);
         self::assertSame('0', $got['mailformat']);
     }
index 0df54e4..65ec2cb 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2017112300.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2017112300.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 
index 6d0c3f0..b0b71c3 100644 (file)
@@ -90,7 +90,7 @@ class core_webservice_external extends external_api {
             'firstname' => $USER->firstname,
             'lastname' => $USER->lastname,
             'fullname' => fullname($USER),
-            'lang' => current_language(),
+            'lang' => clean_param(current_language(), PARAM_LANG),
             'userid' => $USER->id,
             'userpictureurl' => $profileimageurl->out(false),
             'siteid' => SITEID
@@ -223,7 +223,7 @@ class core_webservice_external extends external_api {
                 'firstname'      => new external_value(PARAM_TEXT, 'first name'),
                 'lastname'       => new external_value(PARAM_TEXT, 'last name'),
                 'fullname'       => new external_value(PARAM_TEXT, 'user full name'),
-                'lang'           => new external_value(PARAM_LANG, 'user language'),
+                'lang'           => new external_value(PARAM_LANG, 'Current language.'),
                 'userid'         => new external_value(PARAM_INT, 'user id'),
                 'siteurl'        => new external_value(PARAM_RAW, 'site url'),
                 'userpictureurl' => new external_value(PARAM_URL, 'the user profile picture.