Merge branch 'MDL-65779-master' of git://github.com/peterRd/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Tue, 11 Jun 2019 04:07:40 +0000 (12:07 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Tue, 11 Jun 2019 04:07:40 +0000 (12:07 +0800)
127 files changed:
.eslintignore
.stylelintignore
admin/cli/upgrade.php
admin/settings/frontpage.php
admin/settings/security.php
admin/tests/behat/filter_users.feature
admin/tool/analytics/classes/output/models_list.php
admin/tool/analytics/classes/output/renderer.php
admin/tool/analytics/cli/enable_model.php
admin/tool/analytics/cli/evaluate_model.php
admin/tool/analytics/lang/en/deprecated.txt [new file with mode: 0644]
admin/tool/analytics/lang/en/tool_analytics.php
admin/tool/analytics/model.php
admin/tool/lp/tests/externallib_test.php
admin/tool/task/cli/schedule_task.php
admin/upgrade.txt
analytics/upgrade.txt
backup/util/helper/backup_cron_helper.class.php
backup/util/helper/tests/cronhelper_test.php
badges/classes/form/collections.php
blocks/myoverview/templates/course-action-menu.mustache
blocks/timeline/templates/event-list-item.mustache
cohort/tests/externallib_test.php
competency/tests/external_test.php
competency/tests/privacy_test.php
composer.json
composer.lock
enrol/ldap/cli/sync.php [deleted file]
enrol/ldap/lang/en/enrol_ldap.php
enrol/ldap/upgrade.txt
grade/grading/form/rubric/edit.php
install/lang/el_wp/admin.php [new file with mode: 0644]
install/lang/el_wp/moodle.php [moved from lib/sessionkeepalive_ajax.php with 53% similarity]
lang/en/admin.php
lang/en/analytics.php
lang/en/auth.php
lang/en/error.php
lang/en/group.php
lang/en/moodle.php
lib/ajax/service.php
lib/amd/build/ajax.min.js
lib/amd/build/network.min.js [new file with mode: 0644]
lib/amd/build/page_global.min.js
lib/amd/build/templates.min.js
lib/amd/src/ajax.js
lib/amd/src/network.js [new file with mode: 0644]
lib/amd/src/page_global.js
lib/amd/src/templates.js
lib/behat/classes/partial_named_selector.php
lib/classes/lock/db_record_lock_factory.php
lib/classes/session/external.php [new file with mode: 0644]
lib/classes/session/manager.php
lib/classes/uuid.php [new file with mode: 0644]
lib/db/services.php
lib/db/upgrade.php
lib/filelib.php
lib/mlbackend/php/lang/en/mlbackend_php.php
lib/outputlib.php
lib/outputrequirementslib.php
lib/pear/Auth/RADIUS.php [deleted file]
lib/pear/README.txt
lib/pear/README_MOODLE.txt
lib/recaptchalib.php [deleted file]
lib/setuplib.php
lib/testing/generator/data_generator.php
lib/tests/behat/behat_data_generators.php
lib/tests/setuplib_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/yui/build/moodle-core-checknet/assets/checknet.txt [deleted file]
lib/yui/build/moodle-core-checknet/moodle-core-checknet-debug.js [deleted file]
lib/yui/build/moodle-core-checknet/moodle-core-checknet-min.js [deleted file]
lib/yui/build/moodle-core-checknet/moodle-core-checknet.js [deleted file]
lib/yui/src/checknet/assets/checknet.txt [deleted file]
lib/yui/src/checknet/build.json [deleted file]
lib/yui/src/checknet/js/checknet.js [deleted file]
lib/yui/src/checknet/meta/checknet.json [deleted file]
message/classes/api.php
message/lib.php
message/tests/api_test.php
message/tests/behat/mute_conversations.feature [new file with mode: 0644]
message/upgrade.txt
mod/assign/feedback/editpdf/styles.css
mod/assign/feedback/file/importzipform.php
mod/assign/locallib.php
mod/book/tests/behat/display_book_description.feature [new file with mode: 0644]
mod/book/view.php
mod/forum/amd/build/posts_list.min.js
mod/forum/amd/src/posts_list.js
mod/forum/classes/local/exporters/author.php
mod/forum/classes/local/exporters/discussion.php
mod/forum/classes/local/exporters/forum.php
mod/forum/classes/local/exporters/post.php
mod/forum/classes/local/factories/url.php
mod/forum/classes/local/managers/capability.php
mod/forum/classes/local/renderers/discussion.php
mod/forum/classes/local/renderers/discussion_list.php
mod/forum/lang/en/deprecated.txt [new file with mode: 0644]
mod/forum/lang/en/forum.php
mod/forum/styles.css
mod/forum/templates/discussion_list.mustache
mod/forum/templates/forum_discussion.mustache
mod/forum/templates/forum_discussion_post.mustache
mod/forum/templates/inpage_reply.mustache
mod/forum/tests/exporters_post_test.php
mod/forum/tests/externallib_test.php
mod/imscp/tests/behat/display_imscp_description.feature [new file with mode: 0644]
mod/imscp/view.php
mod/lesson/renderer.php
mod/lesson/tests/behat/display_lesson_description.feature [new file with mode: 0644]
mod/lti/amd/build/tool_configure_controller.min.js
mod/lti/amd/src/tool_configure_controller.js
mod/quiz/report/grading/report.php
mod/quiz/report/grading/tests/behat/grading.feature
mod/scorm/player.php
mod/scorm/tests/behat/behat_mod_scorm.php [new file with mode: 0644]
pix/movehere.svg [new file with mode: 0644]
report/insights/lang/en/report_insights.php
report/security/lang/en/report_security.php
report/security/locallib.php
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/drawer.scss
theme/boost/scss/moodle/icons.scss
theme/boost/style/moodle.css
theme/classic/style/moodle.css
user/filters/lib.php
version.php

index a262404..5e12643 100644 (file)
@@ -27,7 +27,6 @@ lib/htmlpurifier/
 lib/jabber/
 lib/minify/matthiasmullie-minify/
 lib/minify/matthiasmullie-pathconverter/
-lib/pear/Auth/RADIUS.php
 lib/pear/Crypt/CHAP.php
 lib/pear/HTML/Common.php
 lib/pear/HTML/QuickForm.php
@@ -42,7 +41,6 @@ lib/yuilib/gallery/
 lib/jquery/
 lib/html2text/
 lib/markdown/
-lib/recaptchalib.php
 lib/xhprof/
 lib/horde/
 lib/requirejs/
index 6bcaefe..a22b9cc 100644 (file)
@@ -28,7 +28,6 @@ lib/htmlpurifier/
 lib/jabber/
 lib/minify/matthiasmullie-minify/
 lib/minify/matthiasmullie-pathconverter/
-lib/pear/Auth/RADIUS.php
 lib/pear/Crypt/CHAP.php
 lib/pear/HTML/Common.php
 lib/pear/HTML/QuickForm.php
@@ -43,7 +42,6 @@ lib/yuilib/gallery/
 lib/jquery/
 lib/html2text/
 lib/markdown/
-lib/recaptchalib.php
 lib/xhprof/
 lib/horde/
 lib/requirejs/
index 8e439c5..d827e6d 100644 (file)
@@ -52,7 +52,8 @@ list($options, $unrecognized) = cli_get_params(
         'allow-unstable'    => false,
         'help'              => false,
         'lang'              => $lang,
-        'verbose-settings'  => false
+        'verbose-settings'  => false,
+        'is-pending'        => false,
     ),
     array(
         'h' => 'help'
@@ -88,6 +89,8 @@ Options:
 --verbose-settings    Show new settings values. By default only the name of
                       new core or plugin settings are displayed. This option
                       outputs the new values as well as the setting name.
+--is-pending          If an upgrade is needed it exits with an error code of
+                      2 so it distinct from other types of errors.
 -h, --help            Print out this help
 
 Example:
@@ -116,6 +119,10 @@ if (!moodle_needs_upgrading()) {
     cli_error(get_string('cliupgradenoneed', 'core_admin', $newversion), 0);
 }
 
+if ($options['is-pending']) {
+    cli_error(get_string('cliupgradepending', 'core_admin'), 2);
+}
+
 // Test environment first.
 list($envstatus, $environment_results) = check_moodle_environment(normalize_version($release), ENV_SELECT_RELEASE);
 if (!$envstatus) {
index d435022..3eed171 100644 (file)
@@ -58,7 +58,7 @@ if (!during_initial_install()) { //do not use during installation
         foreach ($roles as $role) {
             if (empty($role->archetype) or $role->archetype === 'guest' or $role->archetype === 'frontpage' or $role->archetype === 'student') {
                 $options[$role->id] = $role->localname;
-                if ($role->archetype === 'frontpage') {
+                if ($role->archetype === 'frontpage' && !$defaultfrontpageroleid) {
                     $defaultfrontpageroleid = $role->id;
                 }
             }
index a6ac61f..564845b 100644 (file)
@@ -17,7 +17,7 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     $temp->add(new admin_setting_configcheckbox('forcelogin', new lang_string('forcelogin', 'admin'), new lang_string('configforcelogin', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('forceloginforprofiles', new lang_string('forceloginforprofiles', 'admin'), new lang_string('configforceloginforprofiles', 'admin'), 1));
     $temp->add(new admin_setting_configcheckbox('forceloginforprofileimage', new lang_string('forceloginforprofileimage', 'admin'), new lang_string('forceloginforprofileimage_help', 'admin'), 0));
-    $temp->add(new admin_setting_configcheckbox('opentogoogle', new lang_string('opentogoogle', 'admin'), new lang_string('configopentogoogle', 'admin'), 0));
+    $temp->add(new admin_setting_configcheckbox('opentowebcrawlers', new lang_string('opentowebcrawlers', 'admin'), new lang_string('configopentowebcrawlers', 'admin'), 0));
     $temp->add(new admin_setting_configselect('allowindexing', new lang_string('allowindexing', 'admin'), new lang_string('allowindexing_desc', 'admin'),
         0,
         array(0 => new lang_string('allowindexingexceptlogin', 'admin'),
index 75e2b42..34088d5 100644 (file)
@@ -6,11 +6,11 @@ Feature: An administrator can filter user accounts by role, cohort and other pro
 
   Background:
     Given the following "users" exist:
-      | username | firstname | lastname | email | auth | confirmed |
-      | user1 | User | One | one@example.com | manual | 0 |
-      | user2 | User | Two | two@example.com | ldap | 1 |
-      | user3 | User | Three | three@example.com | manual | 1 |
-      | user4 | User | Four | four@example.com | ldap | 0 |
+      | username | firstname | lastname | email | auth | confirmed | lastip |
+      | user1 | User | One | one@example.com | manual | 0 | 127.0.1.1 |
+      | user2 | User | Two | two@example.com | ldap | 1 | 0.0.0.0 |
+      | user3 | User | Three | three@example.com | manual | 1 | 0.0.0.0 |
+      | user4 | User | Four | four@example.com | ldap | 0 | 127.0.1.2 |
     And the following "cohorts" exist:
       | name | idnumber |
       | Cohort 1 | CH1 |
@@ -82,3 +82,25 @@ Feature: An administrator can filter user accounts by role, cohort and other pro
     And I should not see "User Two"
     And I should not see "User Three"
     And I should see "User Four"
+
+  Scenario: Filter user accounts by last IP address
+    When I set the following fields to these values:
+      | id_lastip | 127.0.1.1 |
+    And I press "Add filter"
+    Then I should see "User One"
+    And I should not see "User Two"
+    And I should not see "User Three"
+    And I should not see "User Four"
+    And I press "Remove all filters"
+    And I set the following fields to these values:
+      | id_lastip | 127.0.1.2 |
+    And I press "Add filter"
+    And I should not see "User One"
+    And I should not see "User Two"
+    And I should not see "User Three"
+    And I should see "User Four"
+    And I press "Remove all filters"
+    And I should see "User One"
+    And I should see "User Two"
+    And I should see "User Three"
+    And I should see "User Four"
index 9c918ce..fbbc227 100644 (file)
@@ -156,7 +156,7 @@ class models_list implements \renderable, \templatable {
                     $modeldata->timesplittinghelp = $helpicon->export_for_template($output);
                 } else {
                     // We really want to encourage developers to add help to their time splitting methods.
-                    debugging("The time splitting method '{$modeldata->timesplitting}' should include a '{$identifier}_help'
+                    debugging("The analysis interval '{$modeldata->timesplitting}' should include a '{$identifier}_help'
                         string to describe its purpose.", DEBUG_DEVELOPER);
                 }
             } else {
@@ -216,10 +216,11 @@ class models_list implements \renderable, \templatable {
 
             // Get predictions.
             if (!$onlycli && $modeldata->enabled && !empty($modeldata->timesplitting)) {
-                $urlparams['action'] = 'getpredictions';
+                $urlparams['action'] = 'scheduledanalysis';
                 $url = new \moodle_url('model.php', $urlparams);
-                $icon = new \action_menu_link_secondary($url, new \pix_icon('i/notifications',
-                    get_string('getpredictions', 'tool_analytics')), get_string('getpredictions', 'tool_analytics'));
+                $icon = new \action_menu_link_secondary($url,
+                    new \pix_icon('i/notifications', get_string('executescheduledanalysis', 'tool_analytics')),
+                    get_string('executescheduledanalysis', 'tool_analytics'));
                 $actionsmenu->add($icon);
             }
 
index 67c4406..743844e 100644 (file)
@@ -90,9 +90,9 @@ class renderer extends plugin_renderer_base {
                 $langstrdata = (object)array('name' => $timesplitting->get_name(), 'id' => $timesplittingid);
 
                 if (CLI_SCRIPT) {
-                    $output .= $OUTPUT->heading(get_string('getpredictionsresultscli', 'tool_analytics', $langstrdata), 3);
+                    $output .= $OUTPUT->heading(get_string('scheduledanalysisresultscli', 'tool_analytics', $langstrdata), 3);
                 } else {
-                    $output .= $OUTPUT->heading(get_string('getpredictionsresults', 'tool_analytics', $langstrdata), 3);
+                    $output .= $OUTPUT->heading(get_string('scheduledanalysisresults', 'tool_analytics', $langstrdata), 3);
                 }
             }
 
index e31dfec..ed8e3fc 100644 (file)
@@ -32,20 +32,20 @@ $help = "Enables the provided model.
 Options:
 --modelid           Model id
 --list              List models
---timesplitting     Time splitting method full class name
+--analysisinterval  Time splitting method full class name
 -h, --help          Print out this help
 
 Example:
-\$ php admin/tool/analytics/cli/enable_model.php --modelid=1 --timesplitting=\"\\core\\analytics\\time_splitting\\quarters\"
+\$ php admin/tool/analytics/cli/enable_model.php --modelid=1 --analysisinterval=\"\\core\\analytics\\time_splitting\\quarters\"
 ";
 
 // Now get cli options.
 list($options, $unrecognized) = cli_get_params(
     array(
-        'help'            => false,
-        'list'            => false,
-        'modelid'         => false,
-        'timesplitting'   => false
+        'help'             => false,
+        'list'             => false,
+        'modelid'          => false,
+        'analysisinterval' => false
     ),
     array(
         'h' => 'help',
@@ -62,7 +62,7 @@ if ($options['list'] || $options['modelid'] === false) {
     exit(0);
 }
 
-if ($options['timesplitting'] === false) {
+if ($options['analysisinterval'] === false) {
     echo $help;
     exit(0);
 }
@@ -73,7 +73,7 @@ if ($options['timesplitting'] === false) {
 $model = new \core_analytics\model($options['modelid']);
 
 // Evaluate its suitability to predict accurately.
-$model->enable($options['timesplitting']);
+$model->enable($options['analysisinterval']);
 
 cli_heading(get_string('success'));
 exit(0);
index 8473aba..067320e 100644 (file)
@@ -33,7 +33,7 @@ Options:
 --modelid              Model id
 --list                 List models
 --non-interactive      Not interactive questions
---timesplitting        Restrict the evaluation to 1 single time splitting method (Optional)
+--analysisinterval     Restrict the evaluation to 1 single analysis interval (Optional)
 --filter               Analyser dependant. e.g. A courseid would evaluate the model using a single course (Optional)
 --mode                 'configuration' or 'trainedmodel'. You can only use mode=trainedmodel when the trained" .
     " model was imported" . "
@@ -42,7 +42,7 @@ Options:
 -h, --help             Print out this help
 
 Example:
-\$ php admin/tool/analytics/cli/evaluate_model.php --modelid=1 --timesplitting='\\core\\analytics\\time_splitting\\quarters' --filter=123,321
+\$ php admin/tool/analytics/cli/evaluate_model.php --modelid=1 --analysisinterval='\\core\\analytics\\time_splitting\\quarters' --filter=123,321
 ";
 
 // Now get cli options.
@@ -51,7 +51,7 @@ list($options, $unrecognized) = cli_get_params(
         'help'                  => false,
         'modelid'               => false,
         'list'                  => false,
-        'timesplitting'         => false,
+        'analysisinterval'      => false,
         'mode'                  => 'configuration',
         'reuse-prev-analysed'   => true,
         'non-interactive'       => false,
@@ -87,8 +87,8 @@ if ($options['mode'] !== 'configuration' && $options['mode'] !== 'trainedmodel')
     cli_error('Error: The provided mode is not supported');
 }
 
-if ($options['mode'] == 'trainedmodel' && $options['timesplitting']) {
-    cli_error('Sorry, no time splitting method can be specified when using \'trainedmodel\' mode.');
+if ($options['mode'] == 'trainedmodel' && $options['analysisinterval']) {
+    cli_error('Sorry, no analysis interval can be specified when using \'trainedmodel\' mode.');
 }
 
 // We need admin permissions.
@@ -104,7 +104,7 @@ if ($options['reuse-prev-analysed']) {
 
 $analyseroptions = array(
     'filter' => $options['filter'],
-    'timesplitting' => $options['timesplitting'],
+    'timesplitting' => $options['analysisinterval'],
     'reuseprevanalysed' => $options['reuse-prev-analysed'],
     'mode' => $options['mode'],
 );
diff --git a/admin/tool/analytics/lang/en/deprecated.txt b/admin/tool/analytics/lang/en/deprecated.txt
new file mode 100644 (file)
index 0000000..f308e25
--- /dev/null
@@ -0,0 +1 @@
+getpredictions,tool_analytics
\ No newline at end of file
index 9e7f30d..0979652 100644 (file)
@@ -24,8 +24,9 @@
 
 $string['accuracy'] = 'Accuracy';
 $string['allpredictions'] = 'All predictions';
-$string['alltimesplittingmethods'] = 'All time-splitting methods';
+$string['alltimesplittingmethods'] = 'All analysis intervals';
 $string['analysingsitedata'] = 'Analysing the site';
+$string['analysis'] = 'Analysis';
 $string['analyticmodels'] = 'Analytics models';
 $string['bettercli'] = 'Evaluating models and generating predictions may involve heavy processing. It is recommended to run these actions from the command line.';
 $string['cantguessstartdate'] = 'Can\'t guess the start date';
@@ -33,7 +34,7 @@ $string['cantguessenddate'] = 'Can\'t guess the end date';
 $string['classdoesnotexist'] = 'Class {$a} does not exist';
 $string['clearpredictions'] = 'Clear predictions';
 $string['clearmodelpredictions'] = 'Are you sure you want to clear all "{$a}" predictions?';
-$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['clienablemodel'] = 'You can enable the model by selecting an analysis interval 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['component'] = 'Component';
@@ -41,14 +42,14 @@ $string['componentcore'] = 'Core';
 $string['componentselect'] = 'Select all models provided by the component \'{$a}\'';
 $string['componentselectnone'] = 'Unselect all';
 $string['createmodel'] = 'Create model';
-$string['currenttimesplitting'] = 'Current time-splitting method';
+$string['currenttimesplitting'] = 'Current analysis interval';
 $string['delete'] = 'Delete';
 $string['deletemodelconfirmation'] = 'Are you sure you want to delete "{$a}"? These changes cannot be reverted.';
 $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['edittrainedwarning'] = 'This model has already been trained. Note that changing its indicators or its analysis interval will delete its previous predictions and start generating new predictions.';
 $string['enabled'] = 'Enabled';
-$string['errorcantenablenotimesplitting'] = 'You need to select a time-splitting method before enabling the model';
+$string['errorcantenablenotimesplitting'] = 'You need to select an analysis interval before enabling the model';
 $string['errornoenabledandtrainedmodels'] = 'There are no enabled and trained models to predict.';
 $string['errornoenabledmodels'] = 'There are no enabled models to train.';
 $string['errornoexport'] = 'Only trained models can be exported';
@@ -71,15 +72,13 @@ $string['evaluationmodecoltrainedmodel'] = 'Trained model';
 $string['evaluationmodecolconfiguration'] = 'Configuration';
 $string['evaluationmodeconfiguration'] = 'Evaluate the model configuration';
 $string['evaluationinbatches'] = 'The site contents are calculated and stored in batches. The evaluation process may be stopped at any time. The next time it is run, it will continue from the point when it was stopped.';
+$string['executescheduledanalysis'] = 'Execute scheduled analysis';
 $string['export'] = 'Export';
 $string['exportincludeweights'] = 'Include the weights of the trained model';
 $string['exportmodel'] = 'Export configuration';
 $string['exporttrainingdata'] = 'Export training data';
-$string['getpredictionsresultscli'] = 'Results using {$a->name} (id: {$a->id}) time-splitting method';
-$string['getpredictionsresults'] = 'Results using {$a->name} time-splitting method';
 $string['extrainfo'] = 'Info';
 $string['generalerror'] = 'Evaluation error. Status code {$a}';
-$string['getpredictions'] = 'Get predictions';
 $string['goodmodel'] = 'This is a good model for using to obtain predictions. Enable it to start obtaining predictions.';
 $string['importmodel'] = 'Import model';
 $string['indicators'] = 'Indicators';
@@ -103,12 +102,12 @@ $string['modelid'] = 'Model ID';
 $string['modelinvalidanalysables'] = 'Invalid analysable elements for "{$a}" model';
 $string['modelname'] = 'Model name';
 $string['modelresults'] = '{$a} results';
-$string['modeltimesplitting'] = 'Time splitting';
+$string['modeltimesplitting'] = 'Analysis interval';
 $string['newmodel'] = 'New model';
 $string['nextpage'] = 'Next page';
 $string['nodatatoevaluate'] = 'There is no data to evaluate the model';
-$string['nodatatopredict'] = 'No new elements to get predictions for';
-$string['nodatatotrain'] = 'There is no new data that can be used for training';
+$string['nodatatopredict'] = 'No new elements to get predictions for.';
+$string['nodatatotrain'] = 'There is no new data that can be used for training.';
 $string['noinvalidanalysables'] = 'This site does not contain any invalid analysable element.';
 $string['notdefined'] = 'Not yet defined';
 $string['pluginname'] = 'Analytic models';
@@ -125,12 +124,14 @@ $string['restoredefaultsome'] = 'Succesfully re-created {$a->count} new model(s)
 $string['restoredefaultsubmit'] = 'Restore selected';
 $string['samestartdate'] = 'Current start date is good';
 $string['sameenddate'] = 'Current end date is good';
-$string['selecttimesplittingforevaluation'] = 'Select the time-splitting method you want to use to evaluate the model configuration.';
+$string['scheduledanalysisresults'] = 'Results using {$a->name} analysis interval';
+$string['scheduledanalysisresultscli'] = 'Results using {$a->name} (id: {$a->id}) analysis interval';
+$string['selecttimesplittingforevaluation'] = 'Select the analysis interval you want to use to evaluate the model configuration.';
 $string['target'] = 'Target';
 $string['target_help'] = 'The target is what the model will predict.';
 $string['target_link'] = 'Targets';
-$string['timesplittingnotdefined'] = 'Time splitting is not defined.';
-$string['timesplittingnotdefined_help'] = 'You need to select a time-splitting method before enabling the model.';
+$string['timesplittingnotdefined'] = 'No analysis interval is defined.';
+$string['timesplittingnotdefined_help'] = 'You need to select an analysis interval before enabling the model.';
 $string['trainandpredictmodel'] = 'Training model and calculating predictions';
 $string['trainingprocessfinished'] = 'Training process finished';
 $string['trainingresults'] = 'Training results';
@@ -140,3 +141,6 @@ $string['viewlog'] = 'Evaluation log';
 $string['weeksenddateautomaticallyset'] = 'End date automatically set based on start date and the number of sections';
 $string['weeksenddatedefault'] = 'End date automatically calculated from the course start date.';
 $string['privacy:metadata'] = 'The Analytic models plugin does not store any personal data.';
+
+// Deprecated since Moodle 3.8.
+$string['getpredictions'] = 'Get predictions';
\ No newline at end of file
index c6cc950..ae2c8dc 100644 (file)
@@ -45,8 +45,8 @@ switch ($action) {
     case 'evaluate':
         $title = get_string('evaluatemodel', 'tool_analytics');
         break;
-    case 'getpredictions':
-        $title = get_string('getpredictions', 'tool_analytics');
+    case 'scheduledanalysis':
+        $title = get_string('analysis', 'tool_analytics');
         break;
     case 'log':
         $title = get_string('viewlog', 'tool_analytics');
@@ -200,7 +200,7 @@ switch ($action) {
         echo $renderer->render_evaluate_results($results, $model->get_analyser()->get_logs());
         break;
 
-    case 'getpredictions':
+    case 'scheduledanalysis':
         confirm_sesskey();
 
         if ($onlycli) {
index 450570a..8511c84 100644 (file)
@@ -76,7 +76,7 @@ class tool_lp_external_testcase extends externallib_advanced_testcase {
      * Setup function- we will create a course and add an assign instance to it.
      */
     protected function setUp() {
-        global $DB;
+        global $DB, $CFG;
 
         $this->resetAfterTest(true);
 
@@ -91,9 +91,7 @@ class tool_lp_external_testcase extends externallib_advanced_testcase {
         $catcontext = context_coursecat::instance($category->id);
 
         // Fetching default authenticated user role.
-        $userroles = get_archetype_roles('user');
-        $this->assertCount(1, $userroles);
-        $authrole = array_pop($userroles);
+        $authrole = $DB->get_record('role', array('id' => $CFG->defaultuserroleid));
 
         // Reset all default authenticated users permissions.
         unassign_capability('moodle/competency:competencygrade', $authrole->id);
index cf03700..85d7a4f 100644 (file)
@@ -50,7 +50,7 @@ Options:
 -h, --help            Print out this help
 
 Example:
-\$sudo -u www-data /usr/bin/php admin/tool/task/cli/scheduled_task.php --execute=\\\\core\\\\task\\\\session_cleanup_task
+\$sudo -u www-data /usr/bin/php admin/tool/task/cli/schedule_task.php --execute=\\\\core\\\\task\\\\session_cleanup_task
 
 ";
 
index 638a30c..68f18eb 100644 (file)
@@ -1,5 +1,10 @@
 This files describes API changes in /admin/*.
 
+=== 3.8 ===
+
+* Admin setting "Open to Google" (opentogoogle) has been renamed to the more generic "Open to search engines" (opentowebcrawlers).
+  This is a more accurate representation of what is being set and the config string has also been moved and updated to reflect this.
+
 === 3.7 ===
 
 * Admin setting "Allow blocks to use the dock" (allowblockstodock) has been removed & stings deprecated.
index 2ae0869..692c594 100644 (file)
@@ -1,6 +1,12 @@
 This files describes API changes in analytics sub system,
 information provided here is intended especially for developers.
 
+=== 3.8 ===
+
+* "Time-splitting method" have been replaced by "Analysis interval" for the language strings that are
+  displayed in the Moodle UI. The name of several time-splitting methods have been updated according
+  to the new description of the field.
+
 === 3.7 ===
 
 * \core_analytics\regressor::evaluate_regression and \core_analytics\classifier::evaluate_classification
index 683c446..980baf4 100644 (file)
@@ -68,6 +68,40 @@ abstract class backup_cron_automated_helper {
     /** Automated backup storage in course backup filearea and specified directory */
     const STORAGE_COURSE_AND_DIRECTORY = 2;
 
+    /**
+     * Get the courses to backup.
+     *
+     * When there are multiple courses to backup enforce some order to the record set.
+     * The following is the preference order.
+     * First backup courses that do not have an entry in backup_courses first,
+     * as they are likely new and never been backed up. Do the oldest modified courses first.
+     * Then backup courses that have previously been backed up starting with the oldest next start time.
+     *
+     * @param null|int $now timestamp to use in course selection.
+     * @return moodle_recordset The recordset of matching courses.
+     */
+    protected static function get_courses($now = null) {
+        global $DB;
+        if ($now == null) {
+            $now = time();
+        }
+
+        $sql = 'SELECT c.*,
+                       COALESCE(bc.nextstarttime, 1) nextstarttime
+                  FROM {course} c
+             LEFT JOIN {backup_courses} bc ON bc.courseid = c.id
+                 WHERE bc.nextstarttime IS NULL OR bc.nextstarttime < ?
+              ORDER BY nextstarttime ASC,
+                       c.timemodified DESC';
+
+        $params = array(
+            $now,  // Only get courses where the backup start time is in the past.
+        );
+        $rs = $DB->get_recordset_sql($sql, $params);
+
+        return $rs;
+    }
+
     /**
      * Runs the automated backups if required
      *
@@ -127,7 +161,7 @@ abstract class backup_cron_automated_helper {
                 $showtime = date('r', $nextstarttime);
             }
 
-            $rs = $DB->get_recordset('course');
+            $rs = self::get_courses($now); // Get courses to backup.
             foreach ($rs as $course) {
                 $backupcourse = $DB->get_record('backup_courses', array('courseid' => $course->id));
                 if (!$backupcourse) {
index 274e6dd..c580aa4 100644 (file)
@@ -364,8 +364,107 @@ class backup_cron_helper_testcase extends advanced_testcase {
         // Updated courses should be backed up.
         $this->assertTrue(testable_backup_cron_automated_helper::testable_is_course_modified($course->id, $timepriortobackup));
     }
+
+    /**
+     * Create courses and backup records for tests.
+     *
+     * @return array Created courses.
+     */
+    private function course_setup() {
+        global $DB;
+
+        // Create test courses.
+        $course1 = $this->getDataGenerator()->create_course(array('timecreated' => 1553402000)); // Newest.
+        $course2 = $this->getDataGenerator()->create_course(array('timecreated' => 1552179600));
+        $course3 = $this->getDataGenerator()->create_course(array('timecreated' => 1552179600));
+        $course4 = $this->getDataGenerator()->create_course(array('timecreated' => 1552179600));
+
+        // Create backup course records for the courses that need them.
+        $backupcourse3 = new stdClass;
+        $backupcourse3->courseid = $course3->id;
+        $backupcourse3->laststatus = testable_backup_cron_automated_helper::BACKUP_STATUS_OK;
+        $backupcourse3->nextstarttime = 1554858160;
+        $DB->insert_record('backup_courses', $backupcourse3);
+
+        $backupcourse4 = new stdClass;
+        $backupcourse4->courseid = $course4->id;
+        $backupcourse4->laststatus = testable_backup_cron_automated_helper::BACKUP_STATUS_OK;
+        $backupcourse4->nextstarttime = 1554858160;
+        $DB->insert_record('backup_courses', $backupcourse4);
+
+        return array($course1, $course2, $course3, $course4);
+    }
+
+    /**
+     * Test the selection and ordering of courses to be backed up.
+     */
+    public function test_get_courses() {
+        $this->resetAfterTest();
+
+        list($course1, $course2, $course3, $course4) = $this->course_setup();
+
+        $now = 1559215025;
+
+        // Get the courses in order.
+        $courseset = testable_backup_cron_automated_helper::testable_get_courses($now);
+
+        $coursearray = array();
+        foreach ($courseset as $course) {
+            if ($course->id != SITEID) { // Skip system course for test.
+                $coursearray[] = $course->id;
+            }
+
+        }
+        $courseset->close();
+
+        // First should be course 1, it is the more recently modified without a backup.
+        $this->assertEquals($course1->id, $coursearray[0]);
+
+        // Second should be course 2, it is the next more recently modified without a backup.
+        $this->assertEquals($course2->id, $coursearray[1]);
+
+        // Third should be course 3, it is the course with the oldest backup.
+        $this->assertEquals($course3->id, $coursearray[2]);
+
+        // Fourth should be course 4, it is the course with the newest backup.
+        $this->assertEquals($course4->id, $coursearray[3]);
+    }
+
+    /**
+     * Test the selection and ordering of courses to be backed up.
+     * Where it is not yet time to start backups for courses with existing backups.
+     */
+    public function test_get_courses_starttime() {
+        $this->resetAfterTest();
+
+        list($course1, $course2, $course3, $course4) = $this->course_setup();
+
+        $now = 1554858000;
+
+        // Get the courses in order.
+        $courseset = testable_backup_cron_automated_helper::testable_get_courses($now);
+
+        $coursearray = array();
+        foreach ($courseset as $course) {
+            if ($course->id != SITEID) { // Skip system course for test.
+                $coursearray[] = $course->id;
+            }
+
+        }
+        $courseset->close();
+
+        // Should only be two courses.
+        // First should be course 1, it is the more recently modified without a backup.
+        $this->assertEquals($course1->id, $coursearray[0]);
+
+        // Second should be course 2, it is the next more recently modified without a backup.
+        $this->assertEquals($course2->id, $coursearray[1]);
+    }
+
 }
 
+
+
 /**
  * Provides access to protected methods we want to explicitly test
  *
@@ -397,4 +496,15 @@ class testable_backup_cron_automated_helper extends backup_cron_automated_helper
     public static function testable_is_course_modified($courseid, $since) {
         return parent::is_course_modified($courseid, $since);
     }
+
+    /**
+     * Provides access to protected method get_courses.
+     *
+     * @param int $now Timestamp to use.
+     * @return moodle_recordset The returned courses as a Moodle recordest.
+     */
+    public static function testable_get_courses($now) {
+        return parent::get_courses($now);
+    }
+
 }
index 6f221b7..5a57ba2 100644 (file)
@@ -79,24 +79,25 @@ class collections extends moodleform {
         $hasgroups = false;
         if (!empty($groups)) {
             foreach ($groups as $group) {
-                // Assertions or badges.
                 $count = 0;
-
+                // Handle attributes based on backpack's supported version.
                 if ($sitebackpack->apiversion == OPEN_BADGES_V2) {
+                    // OpenBadges v2 data attributes.
                     if (empty($group->published)) {
                         // Only public collections.
                         continue;
                     }
-                }
-                if (!empty($group->assertions)) {
+
+                    // Get the number of badges associated with this collection from the assertions array returned.
                     $count = count($group->assertions);
-                }
-                if (!empty($group->badges)) {
-                    $count = count($group->badges);
-                }
-                if (!empty($group->groupId)) {
+                } else {
+                    // OpenBadges v1 data attributes.
                     $group->entityId = $group->groupId;
+
+                    // Get the number of badges associated with this collection. In that case, the number is returned directly.
+                    $count = $group->badges;
                 }
+
                 if (!$hasgroups) {
                     $mform->addElement('static', 'selectgroup', '', get_string('selectgroup_start', 'badges'));
                 }
index e1f70d2..2631ea9 100644 (file)
@@ -41,7 +41,6 @@
             data-course-id="{{id}}"
             aria-controls="favorite-icon-{{ id }}-{{ uniqid }}"
             >
-            <span class="text-primary">{{#pix}} i/star, core, {{#str}} favourites, block_myoverview {{/str}} {{/pix}}</span>
             {{#str}} addtofavourites, block_myoverview {{/str}}
             <div class="sr-only">
                 {{#str}} aria:addtofavourites, block_myoverview {{/str}} {{{fullname}}}
@@ -62,7 +61,6 @@
             data-course-id="{{id}}"
             aria-controls="favorite-icon-{{ id }}-{{ uniqid }}"
             >
-            {{#pix}} i/show, core, {{#str}} hidden, block_myoverview {{/str}} {{/pix}}
             {{#str}} show, block_myoverview {{/str}}
             <div class="sr-only">
                 {{#str}} aria:showcourse, block_myoverview, {{fullname}} {{/str}}
@@ -73,7 +71,6 @@
             data-course-id="{{id}}"
             aria-controls="favorite-icon-{{ id }}-{{ uniqid }}"
             >
-            {{#pix}} i/hide, core, {{#str}} hidden, block_myoverview {{/str}} {{/pix}}
             {{#str}} hidecourse, block_myoverview {{/str}}
             <div class="sr-only">
                 {{#str}} aria:hidecourse, block_myoverview, {{fullname}} {{/str}}
index b89e808..73b275e 100644 (file)
         </div>
         <div class="w-100 event-name-container text-truncate line-height-3">
             <a href="{{url}}"
-               title="{{name}}"
-               aria-label='{{#str}} ariaeventlistitem, block_timeline, { "name": "{{name}}", "course": "{{course.fullnamedisplay}}", "date": "{{#userdate}} {{timesort}}, {{#str}} strftimedatetime, core_langconfig {{/str}} {{/userdate}}" } {{/str}}'
-            ><h6 class="event-name text-truncate mb-0">{{{name}}}</h6></a>
-            <small class="text-muted text-truncate mb-0">{{{course.fullnamedisplay}}}</small>
+               title={{#quote}}{{{name}}}{{/quote}}
+               aria-label='{{#str}} ariaeventlistitem, block_timeline, { "name": {{#quote}}{{{name}}}{{/quote}}, "course": {{#quote}}{{{course.fullnamedisplay}}}{{/quote}}, "date": "{{#userdate}} {{timesort}}, {{#str}} strftimedatetime, core_langconfig {{/str}} {{/userdate}}" } {{/str}}'
+            ><h6 class="event-name text-truncate mb-0">{{#quote}}{{{name}}}{{/quote}}</h6></a>
+            <small class="text-muted text-truncate mb-0">{{#quote}}{{{course.fullnamedisplay}}}{{/quote}}</small>
             {{#action.actionable}}
             <h6 class="mb-0 pt-2">
                 <a href="{{{action.url}}}" aria-label="{{{action.name}}}" title="{{{action.name}}}" class="list-group-item-action">{{{action.name}}}</a>
index b8fd082..f5cdd79 100644 (file)
@@ -522,6 +522,7 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
      * Search cohorts.
      */
     public function test_search_cohorts() {
+        global $DB, $CFG;
         $this->resetAfterTest(true);
 
         $creator = $this->getDataGenerator()->create_user();
@@ -537,9 +538,7 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
         $coursecontext = context_course::instance($course->id);
 
         // Fetching default authenticated user role.
-        $userroles = get_archetype_roles('user');
-        $this->assertCount(1, $userroles);
-        $authrole = array_pop($userroles);
+        $authrole = $DB->get_record('role', array('id' => $CFG->defaultuserroleid));
 
         // Reset all default authenticated users permissions.
         unassign_capability('moodle/cohort:manage', $authrole->id);
index 4c83541..1d72b05 100644 (file)
@@ -97,7 +97,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
      * Setup function- we will create a course and add an assign instance to it.
      */
     protected function setUp() {
-        global $DB;
+        global $DB, $CFG;
 
         $this->resetAfterTest(true);
 
@@ -114,9 +114,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
         $othercatcontext = context_coursecat::instance($othercategory->id);
 
         // Fetching default authenticated user role.
-        $userroles = get_archetype_roles('user');
-        $this->assertCount(1, $userroles);
-        $authrole = array_pop($userroles);
+        $authrole = $DB->get_record('role', array('id' => $CFG->defaultuserroleid));
 
         // Reset all default authenticated users permissions.
         unassign_capability('moodle/competency:competencygrade', $authrole->id);
index 05c463e..77c5a5a 100644 (file)
@@ -2576,7 +2576,7 @@ class core_competency_privacy_testcase extends provider_testcase {
      */
     protected function allow_anyone_to_comment_anywhere() {
         global $DB;
-        $roleid = $DB->get_field('role', 'id', ['archetype' => 'user'], MUST_EXIST);
+        $roleid = $DB->get_field('role', 'id', ['shortname' => 'user'], MUST_EXIST);
         assign_capability('moodle/competency:plancomment', CAP_ALLOW, $roleid, SYSCONTEXTID, true);
         assign_capability('moodle/competency:planmanage', CAP_ALLOW, $roleid, SYSCONTEXTID, true);
         assign_capability('moodle/competency:planmanagedraft', CAP_ALLOW, $roleid, SYSCONTEXTID, true);
index 6dc963d..ced40fc 100644 (file)
@@ -4,10 +4,17 @@
     "description": "Moodle - the world's open source learning platform",
     "type": "project",
     "homepage": "https://moodle.org",
+    "repositories": [
+        {
+            "type": "vcs",
+            "url": "https://github.com/moodlehq/php-webdriver.git"
+        }
+    ],
     "require-dev": {
         "phpunit/phpunit": "7.5.*",
         "phpunit/dbunit": "4.0.*",
-        "moodlehq/behat-extension": "3.37.0",
-        "mikey179/vfsstream": "^1.6"
+        "moodlehq/behat-extension": "3.38.0",
+        "mikey179/vfsstream": "^1.6",
+        "instaclick/php-webdriver": "dev-local as 1.x-dev"
     }
 }
index 948ff21..258d0fb 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "3517a4473544055cd8523bb076cad8f6",
+    "content-hash": "26075c0bf968fde4986dd492a3b8889f",
     "packages": [],
     "packages-dev": [
         {
         },
         {
             "name": "instaclick/php-webdriver",
-            "version": "1.4.5",
+            "version": "dev-local",
             "source": {
                 "type": "git",
-                "url": "https://github.com/instaclick/php-webdriver.git",
-                "reference": "6fa959452e774dcaed543faad3a9d1a37d803327"
+                "url": "https://github.com/moodlehq/php-webdriver.git",
+                "reference": "e311e55bf2c4746db9df72707f3cf1a731ad98aa"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/instaclick/php-webdriver/zipball/6fa959452e774dcaed543faad3a9d1a37d803327",
-                "reference": "6fa959452e774dcaed543faad3a9d1a37d803327",
+                "url": "https://api.github.com/repos/moodlehq/php-webdriver/zipball/e311e55bf2c4746db9df72707f3cf1a731ad98aa",
+                "reference": "e311e55bf2c4746db9df72707f3cf1a731ad98aa",
                 "shasum": ""
             },
             "require": {
                     "WebDriver": "lib/"
                 }
             },
-            "notification-url": "https://packagist.org/downloads/",
             "license": [
                 "Apache-2.0"
             ],
                 "webdriver",
                 "webtest"
             ],
-            "time": "2017-06-30T04:02:48+00:00"
+            "support": {
+                "source": "https://github.com/moodlehq/php-webdriver/tree/local"
+            },
+            "time": "2019-06-03T22:55:37+00:00"
         },
         {
             "name": "mikey179/vfsStream",
         },
         {
             "name": "moodlehq/behat-extension",
-            "version": "v3.37.0",
+            "version": "v3.38.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/moodlehq/moodle-behat-extension.git",
         },
         {
             "name": "phpunit/phpunit",
-            "version": "7.5.9",
+            "version": "7.5.12",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "134669cf0eeac3f79bc7f0c793efbc158bffc160"
+                "reference": "9ba59817745b0fe0c1a5a3032dfd4a6d2994ad1c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/134669cf0eeac3f79bc7f0c793efbc158bffc160",
-                "reference": "134669cf0eeac3f79bc7f0c793efbc158bffc160",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9ba59817745b0fe0c1a5a3032dfd4a6d2994ad1c",
+                "reference": "9ba59817745b0fe0c1a5a3032dfd4a6d2994ad1c",
                 "shasum": ""
             },
             "require": {
                 "testing",
                 "xunit"
             ],
-            "time": "2019-04-19T15:50:46+00:00"
+            "time": "2019-05-28T11:59:40+00:00"
         },
         {
             "name": "psr/container",
         },
         {
             "name": "symfony/browser-kit",
-            "version": "v4.2.8",
+            "version": "v4.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/browser-kit.git",
-                "reference": "c09c18cca96d7067152f78956faf55346c338283"
+                "reference": "3fa7d8cbd2e5006038a09b8ef93f3859a89b627e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/c09c18cca96d7067152f78956faf55346c338283",
-                "reference": "c09c18cca96d7067152f78956faf55346c338283",
+                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/3fa7d8cbd2e5006038a09b8ef93f3859a89b627e",
+                "reference": "3fa7d8cbd2e5006038a09b8ef93f3859a89b627e",
                 "shasum": ""
             },
             "require": {
             },
             "require-dev": {
                 "symfony/css-selector": "~3.4|~4.0",
+                "symfony/http-client": "^4.3",
+                "symfony/mime": "^4.3",
                 "symfony/process": "~3.4|~4.0"
             },
             "suggest": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "4.2-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony BrowserKit Component",
             "homepage": "https://symfony.com",
-            "time": "2019-04-07T09:56:43+00:00"
+            "time": "2019-04-15T20:15:25+00:00"
         },
         {
             "name": "symfony/class-loader",
-            "version": "v3.4.27",
+            "version": "v3.4.28",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/class-loader.git",
         },
         {
             "name": "symfony/config",
-            "version": "v3.4.27",
+            "version": "v3.4.28",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/config.git",
         },
         {
             "name": "symfony/css-selector",
-            "version": "v3.4.27",
+            "version": "v3.4.28",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/css-selector.git",
         },
         {
             "name": "symfony/debug",
-            "version": "v3.4.27",
+            "version": "v3.4.28",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/debug.git",
-                "reference": "681afbb26488903c5ac15e63734f1d8ac430c9b9"
+                "reference": "671fc55bd14800668b1d0a3708c3714940e30a8c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/debug/zipball/681afbb26488903c5ac15e63734f1d8ac430c9b9",
-                "reference": "681afbb26488903c5ac15e63734f1d8ac430c9b9",
+                "url": "https://api.github.com/repos/symfony/debug/zipball/671fc55bd14800668b1d0a3708c3714940e30a8c",
+                "reference": "671fc55bd14800668b1d0a3708c3714940e30a8c",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Debug Component",
             "homepage": "https://symfony.com",
-            "time": "2019-04-11T09:48:14+00:00"
+            "time": "2019-05-18T13:32:47+00:00"
         },
         {
             "name": "symfony/dependency-injection",
         },
         {
             "name": "symfony/dom-crawler",
-            "version": "v4.2.8",
+            "version": "v4.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dom-crawler.git",
-                "reference": "53c97769814c80a84a8403efcf3ae7ae966d53bb"
+                "reference": "28edb1d371640654fbfb9df53d70fa03fdf69fb6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/53c97769814c80a84a8403efcf3ae7ae966d53bb",
-                "reference": "53c97769814c80a84a8403efcf3ae7ae966d53bb",
+                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/28edb1d371640654fbfb9df53d70fa03fdf69fb6",
+                "reference": "28edb1d371640654fbfb9df53d70fa03fdf69fb6",
                 "shasum": ""
             },
             "require": {
                 "symfony/polyfill-ctype": "~1.8",
                 "symfony/polyfill-mbstring": "~1.0"
             },
+            "conflict": {
+                "masterminds/html5": "<2.6"
+            },
             "require-dev": {
+                "masterminds/html5": "^2.6",
                 "symfony/css-selector": "~3.4|~4.0"
             },
             "suggest": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "4.2-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony DomCrawler Component",
             "homepage": "https://symfony.com",
-            "time": "2019-02-23T15:17:42+00:00"
+            "time": "2019-04-26T05:53:56+00:00"
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v3.4.27",
+            "version": "v3.4.28",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
         },
         {
             "name": "symfony/filesystem",
-            "version": "v4.2.8",
+            "version": "v4.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/filesystem.git",
-                "reference": "e16b9e471703b2c60b95f14d31c1239f68f11601"
+                "reference": "988ab7d70c267c34efa85772ca20de3fad11c74b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/filesystem/zipball/e16b9e471703b2c60b95f14d31c1239f68f11601",
-                "reference": "e16b9e471703b2c60b95f14d31c1239f68f11601",
+                "url": "https://api.github.com/repos/symfony/filesystem/zipball/988ab7d70c267c34efa85772ca20de3fad11c74b",
+                "reference": "988ab7d70c267c34efa85772ca20de3fad11c74b",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "4.2-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony Filesystem Component",
             "homepage": "https://symfony.com",
-            "time": "2019-02-07T11:40:08+00:00"
+            "time": "2019-05-24T12:50:04+00:00"
         },
         {
             "name": "symfony/polyfill-ctype",
             "time": "2018-12-25T11:19:39+00:00"
         }
     ],
-    "aliases": [],
+    "aliases": [
+        {
+            "alias": "1.x-dev",
+            "alias_normalized": "1.9999999.9999999.9999999-dev",
+            "version": "dev-local",
+            "package": "instaclick/php-webdriver"
+        }
+    ],
     "minimum-stability": "stable",
-    "stability-flags": [],
+    "stability-flags": {
+        "instaclick/php-webdriver": 20
+    },
     "prefer-stable": false,
     "prefer-lowest": false,
     "platform": [],
diff --git a/enrol/ldap/cli/sync.php b/enrol/ldap/cli/sync.php
deleted file mode 100644 (file)
index 8aedc9f..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * CLI sync for full LDAP synchronisation.
- *
- * This script is meant to be called from a cronjob to sync moodle with the LDAP
- * backend in those setups where the LDAP backend acts as 'master' for enrolment.
- *
- * Sample cron entry:
- * # 5 minutes past 4am
- * 5 4 * * * $sudo -u www-data /usr/bin/php /var/www/moodle/enrol/ldap/cli/sync.php
- *
- * Notes:
- *   - it is required to use the web server account when executing PHP CLI scripts
- *   - you need to change the "www-data" to match the apache user account
- *   - use "su" if "sudo" not available
- *   - If you have a large number of users, you may want to raise the memory limits
- *     by passing -d momory_limit=256M
- *   - For debugging & better logging, you are encouraged to use in the command line:
- *     -d log_errors=1 -d error_reporting=E_ALL -d display_errors=0 -d html_errors=0
- *
- * @deprecated since Moodle 3.3 MDL-57631 - please do not use this CLI script any more, use scheduled task instead.
- * @todo       MDL-58268 This will be deleted in Moodle 3.7.
- * @package    enrol_ldap
- * @author     Iñaki Arenaza - based on code by Martin Dougiamas, Martin Langhoff and others
- * @copyright  1999 onwards Martin Dougiamas {@link http://moodle.com}
- * @copyright  2010 Iñaki Arenaza <iarenaza@eps.mondragon.edu>
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-define('CLI_SCRIPT', true);
-
-require(__DIR__.'/../../../config.php');
-require_once("$CFG->libdir/clilib.php");
-
-// Ensure errors are well explained.
-set_debugging(DEBUG_DEVELOPER, true);
-
-cli_problem('[ENROL LDAP] The sync enrolments cron script has been deprecated. Please use the scheduled task instead.');
-
-// Abort execution of the CLI script if the enrol_ldap\task\sync_enrolments is enabled.
-$task = \core\task\manager::get_scheduled_task('enrol_ldap\task\sync_enrolments');
-if (!$task->get_disabled()) {
-    cli_error('[ENROL LDAP] The scheduled task sync_enrolments is enabled, the cron execution has been aborted.');
-}
-
-if (!enrol_is_enabled('ldap')) {
-    cli_error(get_string('pluginnotenabled', 'enrol_ldap'), 2);
-}
-
-/** @var enrol_ldap_plugin $enrol */
-$enrol = enrol_get_plugin('ldap');
-
-$trace = new text_progress_trace();
-
-// Update enrolments -- these handlers should autocreate courses if required.
-$enrol->sync_enrolments($trace);
-
-exit(0);
index 2a95489..89841c9 100644 (file)
@@ -29,7 +29,7 @@ $string['autocreate'] = '<p>Courses can be created automatically if there are en
 $string['autocreate_key'] = 'Auto create';
 $string['autocreation_settings'] = 'Automatic course creation settings';
 $string['autoupdate_settings'] = 'Automatic course update settings';
-$string['autoupdate_settings_desc'] = '<p>Select fields to update when synchronisation script is running (enrol/ldap/cli/sync.php).</p><p>When at least one field is selected an update will occur.</p>';
+$string['autoupdate_settings_desc'] = '<p>Select fields to update when synchronisation scheduled task for LDAP Enrolments is running (enrol_ldap\task\sync_enrolments).</p><p>When at least one field is selected an update will occur.</p>';
 $string['bind_dn'] = 'If you want to use a bind user to search users, specify it here. Someting like \'cn=ldapuser,ou=public,o=org\'';
 $string['bind_dn_key'] = 'Bind user distinguished name';
 $string['bind_pw'] = 'Password for the bind user';
index d00991d..4607ec0 100644 (file)
@@ -1,4 +1,8 @@
 This files describes API changes in the enrol_ldap code.
 
+=== 3.8 ===
+
+* enrol/ldap/cli/sync.php script has been removed. You should use enrol_ldap\task\sync_enrolments task instead.
+
 === 3.3 ===
 * enrol/ldap/cli/sync.php script has been deprecated in favour of enrol_ldap\task\sync_enrolments task.
index c1849f2..15bda1e 100644 (file)
@@ -65,6 +65,10 @@ if ($mform->is_cancelled()) {
     redirect($returnurl, $warning, null, \core\output\notification::NOTIFY_ERROR);
 }
 
+// Try to keep the session alive on this page as it may take some time
+// before significant interaction happens with the server.
+\core\session\manager::keepalive();
+
 echo $OUTPUT->header();
 $mform->display();
-echo $OUTPUT->footer();
\ No newline at end of file
+echo $OUTPUT->footer();
diff --git a/install/lang/el_wp/admin.php b/install/lang/el_wp/admin.php
new file mode 100644 (file)
index 0000000..8e5c8f0
--- /dev/null
@@ -0,0 +1,43 @@
+<?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/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package   installer
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['clianswerno'] = 'ο';
+$string['cliansweryes'] = 'ν';
+$string['cliincorrectvalueerror'] = 'Σφάλμα, λανθασμένη τιμή «{$a->value}» για το «{$a->option}»';
+$string['cliincorrectvalueretry'] = 'Λανθασμένη τιμή. Παρακαλούμε προσπαθήστε ξανά.';
+$string['clitypevalue'] = 'πληκτρολογήστε μια τιμή';
+$string['clitypevaluedefault'] = 'πληκτρολογήστε μια τιμή· πατήστε Enter για να χρησιμοποιήσετε τηνπροεπιλεγμένη τιμή ({$a})';
+$string['cliunknowoption'] = 'Μη αναγνωρίσιμες επιλογές: {$a}<br />
+Παρακαλούμε χρησιμοποιήστε την επιλογή --βοήθεια';
+$string['cliyesnoprompt'] = 'πατήστε y (σημαίνει yes=ναι) ή πατήστε n (σημαίνει no=όχι)';
+$string['environmentrequireinstall'] = 'απαιτείται να εγκατασταθεί/ ενεργοποιηθεί';
+$string['environmentrequireversion'] = 'απαιτείται η έκδοση {$a->needed} ενώ εσείς έχετε την {$a->current}';
similarity index 53%
rename from lib/sessionkeepalive_ajax.php
rename to install/lang/el_wp/moodle.php
index 65d1acf..6375cf9 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Ensure that session is kept alive.
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
  *
- * @copyright 2014 Andrew Nicols
- * @package   core
+ * @package   installer
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-define('AJAX_SCRIPT', true);
-require_once(__DIR__ . '/../config.php');
-
-// Require the session key - want to make sure that this isn't called
-// maliciously to keep a session alive longer than intended.
-if (!confirm_sesskey()) {
-    header('HTTP/1.1 403 Forbidden');
-    print_error('invalidsesskey');
-}
+defined('MOODLE_INTERNAL') || die();
 
-// Update the session.
-\core\session\manager::touch_session(session_id());
+$string['language'] = 'Γλώσσα';
+$string['moodlelogo'] = 'Λογότυπο Moodle';
+$string['next'] = 'Επόμενο';
+$string['previous'] = 'Προηγούμενο';
+$string['reload'] = 'Eπαναφόρτωση';
index 26c8961..06cbc2b 100644 (file)
@@ -132,6 +132,7 @@ $string['cliupgradedefaultheading'] = 'Setting new default values';
 $string['cliupgradedefaultverbose'] = 'New setting: {$a->name}, Default value: {$a->defaultsetting}';
 $string['cliupgradefinished'] = 'Command line upgrade from {$a->oldversion} to {$a->newversion} completed successfully.';
 $string['cliupgradenoneed'] = 'No upgrade needed for the installed version {$a}. Thanks for coming anyway!';
+$string['cliupgradepending'] = 'An upgrade is pending';
 $string['cliyesnoprompt'] = 'type y (means yes) or n (means no)';
 $string['commentsperpage'] = 'Comments displayed per page';
 $string['commonactivitysettings'] = 'Common activity settings';
@@ -298,7 +299,7 @@ $string['confignoreplyaddress'] = 'Emails are sometimes sent out on behalf of a
 $string['confignotifyloginfailures'] = 'Send login failure notification messages to these selected users. This requires an internal logstore (eg Standard Logstore) to be enabled.';
 $string['confignotifyloginthreshold'] = 'If notifications about failed logins are active, how many failed login attempts by one user or one IP address is it worth notifying about?';
 $string['confignotloggedinroleid'] = 'Users who are not logged in to the site will be treated as if they have this role granted to them at the site context.  Guest is almost always what you want here, but you might want to create roles that are less or more restrictive.  Things like creating posts still require the user to log in properly.';
-$string['configopentogoogle'] = 'If you enable this setting, then Google will be allowed to enter your site as a Guest.  In addition, people coming in to your site via a Google search will automatically be logged in as a Guest.  Note that this only provides transparent access to courses that already allow guest access.';
+$string['configopentowebcrawlers'] = 'If you enable this setting, then search engines will be allowed to enter your site as a guest.  In addition, people coming in to your site via a search engine will automatically be logged in as a guest.  Note that this only provides transparent access to courses that already allow guest access.';
 $string['configoverride'] = 'Defined in config.php';
 $string['configpasswordpolicy'] = 'If enabled, user passwords will be checked against the password policy as specified in the settings below. Enabling the password policy will not affect existing users until they decide to, or are required to, change their password.';
 $string['configpasswordresettime'] = 'This specifies the amount of time people have to validate a password reset request before it expires. Usually 30 minutes is a good value.';
@@ -860,7 +861,7 @@ $string['onlynoreply'] = 'Only when from a no-reply address';
 $string['opcacherecommended'] = 'PHP opcode caching improves performance and lowers memory requirements, OPcache extension is recommended and fully supported.';
 $string['opensslrecommended'] = 'Installing the optional OpenSSL library is highly recommended -- it enables Moodle Networking functionality.';
 $string['opensslrequired'] = 'The OpenSSL PHP extension is now required by Moodle to provide stronger cryptographic services.';
-$string['opentogoogle'] = 'Open to Google';
+$string['opentowebcrawlers'] = 'Open to search engines';
 $string['optionalmaintenancemessage'] = 'Optional maintenance message';
 $string['order1'] = 'First';
 $string['order2'] = 'Second';
index 5ba4125..9962737 100644 (file)
@@ -30,8 +30,8 @@ $string['analyticslogstore'] = 'Log store used for analytics';
 $string['analyticslogstore_help'] = 'The log store that will be used by the analytics API to read users\' activity.';
 $string['analyticssettings'] = 'Analytics settings';
 $string['analyticssiteinfo'] = 'Site information';
-$string['defaulttimesplittingmethods'] = 'Default time-splitting methods for model\'s evaluation';
-$string['defaulttimesplittingmethods_help'] = 'The time-splitting method divides the course duration into parts; the predictions engine will run at the end of these parts. The model evaluation process will iterate through these time-splitting methods unless a specific time-splitting method is specified.  (The ability to specify a time-splitting method is only available when evaluating models using the command line script.)';
+$string['defaulttimesplittingmethods'] = 'Default analysis intervals for model\'s evaluation';
+$string['defaulttimesplittingmethods_help'] = 'The analysis interval defines when the system will calculate predictions and the portion of activity logs that will be considered for those predictions.The model evaluation process will iterate through these analysis intervals unless a specific analysis interval is specified.';
 $string['defaultpredictionsprocessor'] = 'Default predictions processor';
 $string['defaultpredictoroption'] = 'Default processor ({$a})';
 $string['disabledmodel'] = 'Disabled model';
@@ -45,12 +45,12 @@ $string['errorimportversionmismatches'] = 'The version of the following componen
 $string['errorimportmissingclasses'] = 'The following analytics components are not available on this site: {$a->missingclasses}.';
 $string['errorinvalidindicator'] = 'Invalid {$a} indicator';
 $string['errorinvalidtarget'] = 'Invalid {$a} target';
-$string['errorinvalidtimesplitting'] = 'Invalid time splitting; please ensure you add the fully qualified class name.';
+$string['errorinvalidtimesplitting'] = 'Invalid analysis interval; please ensure you add the fully qualified class name.';
 $string['errornoexportconfig'] = 'There was a problem exporting the model configuration.';
-$string['errornoexportconfigrequirements'] = 'Only non-static models with time-splitting methods can be exported.';
+$string['errornoexportconfigrequirements'] = 'Only non-static models with an analysis interval can be exported.';
 $string['errornoindicators'] = 'This model does not have any indicators.';
 $string['errornopredictresults'] = 'No results returned from the predictions processor. Check the output directory contents for more information.';
-$string['errornotimesplittings'] = 'This model does not have any time-splitting method.';
+$string['errornotimesplittings'] = 'This model does not have an analysis interval.';
 $string['errornoroles'] = 'Student or teacher roles have not been defined. Define them in the analytics settings page.';
 $string['errornotarget'] = 'This model does not have any target.';
 $string['errorpredictioncontextnotavailable'] = 'This prediction context is no longer available.';
@@ -60,7 +60,7 @@ $string['errorpredictionsprocessor'] = 'Predictions processor error: {$a}';
 $string['errorpredictwrongformat'] = 'The predictions processor return cannot be decoded: "{$a}"';
 $string['errorprocessornotready'] = 'The selected predictions processor is not ready: {$a}';
 $string['errorsamplenotavailable'] = 'The predicted sample is no longer available.';
-$string['errorunexistingtimesplitting'] = 'The selected time-splitting method is not available.';
+$string['errorunexistingtimesplitting'] = 'The selected analysis interval is not available.';
 $string['errorunexistingmodel'] = 'Non-existing model {$a}';
 $string['errorunknownaction'] = 'Unknown action';
 $string['eventpredictionactionstarted'] = 'Prediction process started';
@@ -70,8 +70,8 @@ $string['insightmessagesubject'] = 'New insight for "{$a}"';
 $string['insightinfomessage'] = 'The system generated an insight for you: {$a}';
 $string['insightinfomessagehtml'] = 'The system generated an insight for you.';
 $string['insightinfomessageaction'] = '{$a->text}: {$a->url}';
-$string['invalidtimesplitting'] = 'Model with ID {$a} needs a time-splitting method before it can be used for training.';
-$string['invalidanalysablefortimesplitting'] = 'It cannot be analysed using {$a} time-splitting method.';
+$string['invalidtimesplitting'] = 'Model with ID {$a} needs an analysis interval before it can be used for training.';
+$string['invalidanalysablefortimesplitting'] = 'It cannot be analysed using {$a} analysis interval.';
 $string['levelinstitution'] = 'Level of education';
 $string['levelinstitutionisced0'] = 'Early childhood education (‘less than primary’ for educational attainment)';
 $string['levelinstitutionisced1'] = 'Primary education';
@@ -95,9 +95,8 @@ $string['noevaluationbasedassumptions'] = 'Models based on assumptions cannot be
 $string['nodata'] = 'No data to analyse';
 $string['noinsightsmodel'] = 'This model does not generate insights';
 $string['noinsights'] = 'No insights reported';
-$string['nonewdata'] = 'No new data available';
-$string['nonewranges'] = 'No new predictions yet';
-$string['nonewtimeranges'] = 'No new time ranges; nothing to predict.';
+$string['nonewdata'] = 'No new data available. It will be analysed after the next analysis interval.';
+$string['nonewranges'] = 'No new predictions yet. It will be analysed after the next analysis interval.';
 $string['nopredictionsyet'] = 'No predictions available yet';
 $string['noranges'] = 'No predictions yet';
 $string['notrainingbasedassumptions'] = 'Models based on assumptions do not need training';
@@ -123,7 +122,7 @@ $string['privacy:metadata:analytics:predictions'] = 'Predictions';
 $string['privacy:metadata:analytics:predictions:modelid'] = 'The model ID';
 $string['privacy:metadata:analytics:predictions:contextid'] = 'The context';
 $string['privacy:metadata:analytics:predictions:sampleid'] = 'The sample ID';
-$string['privacy:metadata:analytics:predictions:rangeindex'] = 'The index of the time-splitting method';
+$string['privacy:metadata:analytics:predictions:rangeindex'] = 'The index of the analysis interval';
 $string['privacy:metadata:analytics:predictions:prediction'] = 'The prediction';
 $string['privacy:metadata:analytics:predictions:predictionscore'] = 'The prediction score';
 $string['privacy:metadata:analytics:predictions:calculations'] = 'Indicator calculations';
@@ -137,8 +136,8 @@ $string['privacy:metadata:analytics:predictionactions:actionname'] = 'The action
 $string['privacy:metadata:analytics:predictionactions:timecreated'] = 'When the prediction action was performed';
 $string['processingsitecontents'] = 'Processing site contents';
 $string['successfullyanalysed'] = 'Successfully analysed';
-$string['timesplittingmethod'] = 'Time-splitting method';
-$string['timesplittingmethod_help'] = 'The time-splitting method defines when the system will calculate predictions and the portion of activity logs that will be considered for those predictions. For example, the course duration may be divided into parts, with a prediction generated at the end of each part.';
+$string['timesplittingmethod'] = 'Analysis interval';
+$string['timesplittingmethod_help'] = 'The analysis interval defines when the system will calculate predictions and the portion of activity logs that will be considered for those predictions. For example, the course duration may be divided into parts, with a prediction generated at the end of each part.';
 $string['timesplittingmethod_link'] = 'Time_splitting_methods';
 $string['typeinstitution'] = 'Type of institution';
 $string['typeinstitutionacademic'] = 'Academic';
index 1233c20..0b395a7 100644 (file)
@@ -82,8 +82,6 @@ If you have any questions please contact support on: {$a->supportemail}
 {$a->url}';
 $string['emailupdatesuccess'] = 'Email address of user <em>{$a->fullname}</em> was successfully updated to <em>{$a->email}</em>.';
 $string['emailupdatetitle'] = 'Confirmation of email update at {$a->site}';
-$string['enterthenumbersyouhear'] = 'Enter the numbers you hear';
-$string['enterthewordsabove'] = 'Enter the words above';
 $string['errormaxconsecutiveidentchars'] = 'Passwords must have at most {$a} consecutive identical characters.';
 $string['errorminpassworddigits'] = 'Passwords must have at least {$a} digit(s).';
 $string['errorminpasswordlength'] = 'Passwords must be at least {$a} characters long.';
@@ -100,9 +98,6 @@ $string['forcechangepasswordfirst_help'] = 'Force users to change password on th
 $string['forcechangepassword_help'] = 'Force users to change password on their next login to Moodle.';
 $string['forgottenpassword'] = 'If you enter a URL here, it will be used as the lost password recovery page for this site. This is intended for sites where passwords are handled entirely outside of Moodle. Leave this blank to use the default password recovery.';
 $string['forgottenpasswordurl'] = 'Forgotten password URL';
-$string['getanaudiocaptcha'] = 'Get an audio CAPTCHA';
-$string['getanimagecaptcha'] = 'Get an image CAPTCHA';
-$string['getanothercaptcha'] = 'Get another CAPTCHA';
 $string['getrecaptchaapi'] = 'To use reCAPTCHA you must get an API key from <a href=\'https://www.google.com/recaptcha/admin\'>https://www.google.com/recaptcha/admin</a>';
 $string['guestloginbutton'] = 'Guest login button';
 $string['changepassword'] = 'Change password URL';
index 825a6b3..e0b5519 100644 (file)
@@ -496,6 +496,7 @@ $string['sendmessage'] = 'Send message';
 $string['serverconnection'] = 'Error connecting to the server';
 $string['servicedonotexist'] = 'The service does not exist';
 $string['sessionwaiterr'] = 'Timed out while waiting for session lock.<br />Wait for your current requests to finish and try again later.';
+$string['sessionexpired'] = 'Session expired';
 $string['sessioncookiesdisable'] = 'Incorrect use of require_key_login() - session cookies must be disabled!';
 $string['sessiondiskfull'] = 'The session partition is full. It is not possible to log in at this time. Please notify the server administrator.';
 $string['sessionhandlerproblem'] = 'Session handler is misconfigured';
index 9d448ec..9f32f85 100644 (file)
@@ -196,3 +196,4 @@ $string['toomanygroups'] = 'Insufficient users to populate this number of groups
 $string['usercount'] = 'User count';
 $string['usercounttotal'] = 'User count ({$a})';
 $string['usergroupmembership'] = 'Selected user\'s membership:';
+$string['memberofgroup'] = 'Group member of: {$a}';
index df841d6..6c37eea 100644 (file)
@@ -802,6 +802,7 @@ $string['existingcoursedeleting'] = 'Existing course, deleting it first';
 $string['existingcreators'] = 'Existing course creators';
 $string['existingstudents'] = 'Enrolled students';
 $string['existingteachers'] = 'Existing teachers';
+$string['extendsession'] = 'Extend session';
 $string['expand'] = 'Expand';
 $string['expandall'] = 'Expand all';
 $string['expandcategory'] = 'Expand {$a}';
@@ -1807,7 +1808,8 @@ $string['separateandconnected'] = 'Separate and Connected ways of knowing';
 $string['separateandconnectedinfo'] = 'The scale based on the theory of separate and connected knowing. This theory describes two different ways that we can evaluate and learn about the things we see and hear.<ul><li><strong>Separate knowers</strong> remain as objective as possible without including feelings and emotions. In a discussion with other people, they like to defend their own ideas, using logic to find holes in opponent\'s ideas.</li><li><strong>Connected knowers</strong> are more sensitive to other people. They are skilled at empathy and tend to listen and ask questions until they feel they can connect and "understand things from their point of view". They learn by trying to share the experiences that led to the knowledge they find in other people.</li></ul>';
 $string['servererror'] = 'An error occurred whilst communicating with the server';
 $string['serverlocaltime'] = 'Server\'s local time';
-$string['sessionforceclean'] = 'As a security precaution, user-generated scripts have been disabled within this session.';
+$string['sessionforceclean'] = 'As a security precaution, user-generated scripts have been disabled within this session';
+$string['sessiontimeoutsoon'] = 'Your session is about to timeout. Do you want to extend your current session?';
 $string['setcategorytheme'] = 'Set category theme';
 $string['setpassword'] = 'Set password';
 $string['setpasswordinstructions'] = 'Please enter your new password below, then save changes.';
@@ -1981,24 +1983,24 @@ If \'plain text area\' is selected, a format for text input areas such as HTML o
 The list of available text editors is determined by the site administrator.';
 $string['texteditor'] = 'Use standard web forms';
 $string['textformat'] = 'Plain text format';
-$string['timesplitting:deciles'] = 'Tenths';
-$string['timesplitting:deciles_help'] = 'This time-splitting method divides the course into tenths (10 equal parts), with each prediction being based on the data of only the most recent previous tenth.';
-$string['timesplitting:decilesaccum'] = 'Tenths accumulative';
-$string['timesplitting:decilesaccum_help'] = 'This time-splitting method divides the course into tenths (10 equal parts), with each prediction being based on the data of all previous tenths.';
-$string['timesplitting:nosplitting'] = 'No time splitting';
-$string['timesplitting:nosplitting_help'] = 'No time-splitting method is defined for this model.';
-$string['timesplitting:quarters'] = 'Quarters';
-$string['timesplitting:quarters_help'] = 'This time-splitting method divides the course into quarters (4 equal parts), with each prediction being based on the data of only the most recent previous quarter.';
-$string['timesplitting:quartersaccum'] = 'Quarters accumulative';
-$string['timesplitting:quartersaccum_help'] = 'This time-splitting method divides the course into quarters (4 equal parts), with each prediction being based on the data of all previous quarters.';
-$string['timesplitting:singlerange'] = 'Single range';
-$string['timesplitting:singlerange_help'] = 'This time-splitting method considers the entire course as a single span.';
+$string['timesplitting:deciles'] = 'Last tenth';
+$string['timesplitting:deciles_help'] = 'This analysis interval divides the course into tenths (10 equal parts), with each prediction being based on the data of only the most recent previous tenth.';
+$string['timesplitting:decilesaccum'] = 'All previous tenths';
+$string['timesplitting:decilesaccum_help'] = 'This analysis interval divides the course into tenths (10 equal parts), with each prediction being based on the accumulated data of all previous tenths.';
+$string['timesplitting:nosplitting'] = 'No time limits';
+$string['timesplitting:nosplitting_help'] = 'No analysis interval is defined for this model.';
+$string['timesplitting:quarters'] = 'Last quarter';
+$string['timesplitting:quarters_help'] = 'This analysis interval divides the course into quarters (4 equal parts), with each prediction being based on the data of only the most recent previous quarter.';
+$string['timesplitting:quartersaccum'] = 'All previous quarters';
+$string['timesplitting:quartersaccum_help'] = 'This analysis interval divides the course into quarters (4 equal parts), with each prediction being based on the accumulated data of all previous quarters.';
+$string['timesplitting:singlerange'] = 'From start to end';
+$string['timesplitting:singlerange_help'] = 'This analysis interval considers the entire course as a single span.';
 $string['timesplitting:upcoming3days'] = 'Upcoming 3 days';
-$string['timesplitting:upcoming3days_help'] = 'This time-splitting method generates predictions every 3 days. The indicators calculations will be based on the upcoming 3 days.';
+$string['timesplitting:upcoming3days_help'] = 'This analysis interval generates predictions every 3 days. The indicators calculations will be based on the upcoming 3 days.';
 $string['timesplitting:upcomingfortnight'] = 'Upcoming fortnight';
-$string['timesplitting:upcomingfortnight_help'] = 'This time-splitting method generates predictions every fortnight. The indicators calculations will be based on the upcoming fortnight.';
+$string['timesplitting:upcomingfortnight_help'] = 'This analysis interval generates predictions every fortnight. The indicators calculations will be based on the upcoming fortnight.';
 $string['timesplitting:upcomingweek'] = 'Upcoming week';
-$string['timesplitting:upcomingweek_help'] = 'This time-splitting method generates predictions every week. The indicators calculations will be based on the upcoming week.';
+$string['timesplitting:upcomingweek_help'] = 'This analysis interval generates predictions every week. The indicators calculations will be based on the upcoming week.';
 $string['thanks'] = 'Thanks';
 $string['theme'] = 'Theme';
 $string['themes'] = 'Themes';
index 3298c0e..caaf86c 100644 (file)
 
 define('AJAX_SCRIPT', true);
 
+if (!empty($_GET['nosessionupdate'])) {
+    define('NO_SESSION_UPDATE', true);
+}
+
 require_once(__DIR__ . '/../../config.php');
 require_once($CFG->libdir . '/externallib.php');
 
@@ -63,5 +67,4 @@ foreach ($requests as $request) {
         break;
     }
 }
-
 echo json_encode($responses);
index f8c5453..adc66eb 100644 (file)
Binary files a/lib/amd/build/ajax.min.js and b/lib/amd/build/ajax.min.js differ
diff --git a/lib/amd/build/network.min.js b/lib/amd/build/network.min.js
new file mode 100644 (file)
index 0000000..435d017
Binary files /dev/null and b/lib/amd/build/network.min.js differ
index 838ab91..a02a46c 100644 (file)
Binary files a/lib/amd/build/page_global.min.js and b/lib/amd/build/page_global.min.js differ
index 606dc85..1293f1a 100644 (file)
Binary files a/lib/amd/build/templates.min.js and b/lib/amd/build/templates.min.js differ
index a13cbcf..98e982c 100644 (file)
@@ -40,11 +40,12 @@ define(['jquery', 'core/config', 'core/log', 'core/url'], function($, config, Lo
      */
     var requestSuccess = function(responses) {
         // Call each of the success handlers.
-        var requests = this;
-        var exception = null;
-        var i = 0;
-        var request;
-        var response;
+        var requests = this,
+            exception = null,
+            i = 0,
+            request,
+            response,
+            nosessionupdate;
 
         if (responses.error) {
             // There was an error with the request as a whole.
@@ -69,6 +70,7 @@ define(['jquery', 'core/config', 'core/log', 'core/url'], function($, config, Lo
                     request.deferred.resolve(response.data);
                 } else {
                     exception = response.exception;
+                    nosessionupdate = requests[i].nosessionupdate;
                     break;
                 }
             } else {
@@ -80,7 +82,7 @@ define(['jquery', 'core/config', 'core/log', 'core/url'], function($, config, Lo
         // Something failed, reject the remaining promises.
         if (exception !== null) {
             // Redirect to the login page.
-            if (exception.errorcode === "servicerequireslogin") {
+            if (exception.errorcode === "servicerequireslogin" && !nosessionupdate) {
                 window.location = URL.relativeUrl("/login/index.php");
             } else {
                 requests.forEach(function(request) {
@@ -132,9 +134,12 @@ define(['jquery', 'core/config', 'core/log', 'core/url'], function($, config, Lo
          *                  If false - this function will call the faster nologin ajax script - but
          *                  will fail unless all functions have been marked as 'loginrequired' => false
          *                  in services.php
+         * @param {Boolean} nosessionupdate Optional, defaults to false.
+         *                  If true, the timemodified for the session will not be updated.
+         * @param {Integer} timeout number of milliseconds to wait for a response. Defaults to no limit.
          * @return {Promise[]} Array of promises that will be resolved when the ajax call returns.
          */
-        call: function(requests, async, loginrequired) {
+        call: function(requests, async, loginrequired, nosessionupdate, timeout) {
             $(window).bind('beforeunload', function() {
                 unloading = true;
             });
@@ -150,6 +155,13 @@ define(['jquery', 'core/config', 'core/log', 'core/url'], function($, config, Lo
             if (typeof async === "undefined") {
                 async = true;
             }
+            if (typeof timeout === 'undefined') {
+                timeout = 0;
+            }
+
+            if (typeof nosessionupdate === "undefined") {
+                nosessionupdate = false;
+            }
             for (i = 0; i < requests.length; i++) {
                 var request = requests[i];
                 ajaxRequestData.push({
@@ -157,6 +169,7 @@ define(['jquery', 'core/config', 'core/log', 'core/url'], function($, config, Lo
                     methodname: request.methodname,
                     args: request.args
                 });
+                request.nosessionupdate = nosessionupdate;
                 request.deferred = $.Deferred();
                 promises.push(request.deferred.promise());
                 // Allow setting done and fail handlers as arguments.
@@ -185,7 +198,8 @@ define(['jquery', 'core/config', 'core/log', 'core/url'], function($, config, Lo
                 dataType: 'json',
                 processData: false,
                 async: async,
-                contentType: "application/json"
+                contentType: "application/json",
+                timeout: timeout
             };
 
             var script = 'service.php';
@@ -195,6 +209,10 @@ define(['jquery', 'core/config', 'core/log', 'core/url'], function($, config, Lo
             var url = config.wwwroot + '/lib/ajax/' + script +
                     '?sesskey=' + config.sesskey + '&info=' + requestInfo;
 
+            if (nosessionupdate) {
+                url += '&nosessionupdate=true';
+            }
+
             // Jquery deprecated done and fail with async=false so we need to do this 2 ways.
             if (async) {
                 $.ajax(url, settings)
diff --git a/lib/amd/src/network.js b/lib/amd/src/network.js
new file mode 100644 (file)
index 0000000..5e21a6a
--- /dev/null
@@ -0,0 +1,196 @@
+// 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/>.
+
+/**
+ * Poll the server to keep the session alive.
+ *
+ * @module     core/network
+ * @package    core
+ * @copyright  2019 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/ajax', 'core/config', 'core/notification', 'core/str'],
+        function($, Ajax, Config, Notification, Str) {
+
+    var started = false;
+    var warningDisplayed = false;
+    var keepAliveFrequency = 0;
+    var requestTimeout = 0;
+    var keepAliveMessage = false;
+    var sessionTimeout = false;
+    // 1/10 of session timeout, max of 10 minutes.
+    var checkFrequency = Math.min((Config.sessiontimeout / 10), 600) * 1000;
+    // 1/5 of sessiontimeout.
+    var warningLimit = checkFrequency * 2;
+
+    /**
+     * The session time has expired - we can't extend it now.
+     */
+    var timeoutSessionExpired = function() {
+        sessionTimeout = true;
+    };
+
+    /**
+     * Ping the server to keep the session alive.
+     *
+     * @return {Promise}
+     */
+    var touchSession = function() {
+        var request = {
+            methodname: 'core_session_touch',
+            args: { }
+        };
+
+        if (sessionTimeout) {
+            // We timed out before we extended the session.
+            return Str.get_strings([
+                {key: 'sessionexpired', component: 'error'},
+                {key: 'sessionerroruser', component: 'error'}
+            ]).then(function(strings) {
+                Notification.alert(
+                    strings[0], // Title.
+                    strings[1] // Message.
+                );
+                return true;
+            }).fail(Notification.exception);
+        } else {
+            return Ajax.call([request], true, true, false, requestTimeout)[0].then(function() {
+                if (keepAliveFrequency > 0) {
+                    setTimeout(touchSession, keepAliveFrequency);
+                }
+                return true;
+            }).fail(function() {
+                Notification.alert('', keepAliveMessage);
+            });
+        }
+    };
+
+    /**
+     * Ask the server how much time is remaining in this session and
+     * show confirm/cancel notifications if the session is about to run out.
+     *
+     * @return {Promise}
+     */
+    var checkSession = function() {
+        var request = {
+            methodname: 'core_session_time_remaining',
+            args: { }
+        };
+
+        sessionTimeout = false;
+        return Ajax.call([request], true, true, true)[0].then(function(args) {
+            if (args.userid <= 0) {
+                return false;
+            }
+            if (args.timeremaining < 0) {
+                Str.get_strings([
+                    {key: 'sessionexpired', component: 'error'},
+                    {key: 'sessionerroruser', component: 'error'}
+                ]).then(function(strings) {
+                    Notification.alert(
+                        strings[0], // Title.
+                        strings[1] // Message.
+                    );
+                    return true;
+                }).fail(Notification.exception);
+
+            } else if (args.timeremaining * 1000 < warningLimit && !warningDisplayed) {
+                // If we don't extend the session before the timeout - warn.
+                setTimeout(timeoutSessionExpired, args.timeremaining * 1000);
+                warningDisplayed = true;
+                Str.get_strings([
+                    {key: 'norecentactivity', component: 'moodle'},
+                    {key: 'sessiontimeoutsoon', component: 'moodle'},
+                    {key: 'extendsession', component: 'moodle'},
+                    {key: 'cancel', component: 'moodle'}
+                ]).then(function(strings) {
+                    Notification.confirm(
+                        strings[0], // Title.
+                        strings[1], // Message.
+                        strings[2], // Extend session.
+                        strings[3], // Cancel.
+                        function() {
+                            touchSession();
+                            warningDisplayed = false;
+                            // First wait is half the session timeout.
+                            setTimeout(checkSession, checkFrequency * 5);
+                            return true;
+                        },
+                        function() {
+                            warningDisplayed = false;
+                            setTimeout(checkSession, checkFrequency);
+                        }
+                    );
+                    return true;
+                }).fail(Notification.exception);
+            } else {
+                setTimeout(checkSession, checkFrequency);
+            }
+            return true;
+        });
+        // We do not catch the fails from the above ajax call because they will fail when
+        // we are not logged in - we don't need to take any action then.
+    };
+
+    /**
+     * Start calling a function to check if the session is still alive.
+     */
+    var start = function() {
+        if (keepAliveFrequency > 0) {
+            setTimeout(touchSession, keepAliveFrequency);
+        } else {
+            // First wait is half the session timeout.
+            setTimeout(checkSession, checkFrequency * 5);
+        }
+    };
+
+    /**
+     * Don't allow more than one of these polling loops in a single page.
+     */
+    var init = function() {
+        // We only allow one concurrent instance of this checker.
+        if (started) {
+            return;
+        }
+        started = true;
+
+        start();
+    };
+
+    /**
+     * Start polling with more specific values for the frequency, timeout and message.
+     *
+     * @param {number} freq How ofter to poll the server.
+     * @param {number} timeout The time to wait for each request to the server.
+     * @param {string} message The message to display if the session is going to time out.
+     */
+    var keepalive = function(freq, timeout, message) {
+        // We only allow one concurrent instance of this checker.
+        if (started) {
+            return;
+        }
+        started = true;
+
+        keepAliveFrequency = freq * 1000;
+        keepAliveMessage = message;
+        requestTimeout = timeout * 1000;
+        start();
+    };
+
+    return {
+        keepalive: keepalive,
+        init: init
+    };
+});
index 88360b8..b92c97d 100644 (file)
@@ -26,11 +26,13 @@ define(
     'jquery',
     'core/custom_interaction_events',
     'core/str',
+    'core/network'
 ],
 function(
     $,
     CustomEvents,
-    Str
+    Str,
+    Network
 ) {
 
     /**
@@ -127,6 +129,7 @@ function(
      */
     var init = function() {
         initActionOptionDropdownHandler();
+        Network.init();
     };
 
     return {
index 89212f0..70140ee 100644 (file)
@@ -486,6 +486,7 @@ define([
         content = content
             .replace('"', '\\"')
             .replace(/([\{\}]{2,3})/g, '{{=<% %>=}}$1<%={{ }}=%>')
+            .replace(/(\r\n|\r|\n)/g, '&#x0a;')
             ;
         return '"' + content + '"';
     };
index b632895..3f8c053 100644 (file)
@@ -73,6 +73,7 @@ class behat_partial_named_selector extends \Behat\Mink\Selector\PartialNamedSele
         'xpath_element' => 'xpath_element',
         'form_row' => 'form_row',
         'group_message_header' => 'group_message_header',
+        'group_message' => 'group_message',
     );
 
     /**
@@ -96,6 +97,7 @@ class behat_partial_named_selector extends \Behat\Mink\Selector\PartialNamedSele
         'group_message_tab' => 'group_message_tab',
         'group_message_list_area' => 'group_message_list_area',
         'group_message_message_content' => 'group_message_message_content',
+        'icon_container' => 'icon_container',
         'icon' => 'icon',
         'link' => 'link',
         'link_or_button' => 'link_or_button',
@@ -162,7 +164,7 @@ XPATH
             .//*[@data-region='message-drawer' and contains(., %locator%)]//div[@data-region='content-message-container']
 XPATH
     , 'group_message_header' => <<<XPATH
-        .//*[@data-region='message-drawer']//div[@data-region='header-container' and contains(., %locator%)]
+        .//*[@data-region='message-drawer']//div[@data-region='header-content' and contains(., %locator%)]
 XPATH
     , 'group_message_member' => <<<XPATH
         .//*[@data-region='message-drawer']//div[@data-region='group-info-content-container']
@@ -178,6 +180,9 @@ XPATH
 XPATH
     , 'group_message_message_content' => <<<XPATH
         .//*[@data-region='message-drawer']//*[@data-region='message' and @data-message-id and contains(., %locator%)]
+XPATH
+    , 'icon_container' => <<<XPATH
+        .//span[contains(@data-region, concat(%locator%,'-icon-container'))]
 XPATH
         , 'icon' => <<<XPATH
 .//*[contains(concat(' ', normalize-space(@class), ' '), ' icon ') and ( contains(normalize-space(@title), %locator%))]
index dd2aa75..83321d2 100644 (file)
@@ -103,7 +103,7 @@ class db_record_lock_factory implements lock_factory {
      * to duplicates in a clustered environment (especially on VMs due to poor time precision).
      */
     protected function generate_unique_token() {
-        return generate_uuid();
+        return \core\uuid::generate();
     }
 
     /**
diff --git a/lib/classes/session/external.php b/lib/classes/session/external.php
new file mode 100644 (file)
index 0000000..e6e0f0e
--- /dev/null
@@ -0,0 +1,95 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This class contains a list of webservice functions related to session.
+ *
+ * @package    core
+ * @copyright  2019 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\session;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * This class contains a list of webservice functions related to session.
+ *
+ * @copyright  2019 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+class external extends \external_api {
+
+    /**
+     * Returns description of touch_session() parameters.
+     *
+     * @return external_function_parameters
+     */
+    public static function touch_session_parameters() {
+        return new \external_function_parameters([]);
+    }
+
+    /**
+     * Extend the current session.
+     *
+     * @return array the mapping
+     */
+    public static function touch_session() {
+        \core\session\manager::touch_session(session_id());
+        return true;
+    }
+
+    /**
+     * Returns description of touch_session() result value.
+     *
+     * @return external_description
+     */
+    public static function touch_session_returns() {
+        return new \external_value(PARAM_BOOL, 'result');
+    }
+
+    /**
+     * Returns description of time_remaining() parameters.
+     *
+     * @return external_function_parameters
+     */
+    public static function time_remaining_parameters() {
+        return new \external_function_parameters([]);
+    }
+
+    /**
+     * Extend the current session.
+     *
+     * @return array the mapping
+     */
+    public static function time_remaining() {
+        return \core\session\manager::time_remaining(session_id());
+    }
+
+    /**
+     * Returns description of touch_session() result value.
+     *
+     * @return external_description
+     */
+    public static function time_remaining_returns() {
+        return new \external_single_structure(array (
+                'userid' => new \external_value(PARAM_INTEGER, 'The current user id.'),
+                'timeremaining' => new \external_value(PARAM_INTEGER, 'The number of seconds remaining in this session.')
+        ));
+    }
+}
index 36083dd..8a6f101 100644 (file)
@@ -364,6 +364,9 @@ class manager {
             }
 
             if ($timeout) {
+                if (defined('NO_SESSION_UPDATE') && NO_SESSION_UPDATE) {
+                    return;
+                }
                 session_regenerate_id(true);
                 $_SESSION = array();
                 $DB->delete_records('sessions', array('id'=>$record->id));
@@ -398,7 +401,7 @@ class manager {
                     $updated = true;
                 }
 
-                if ($updated) {
+                if ($updated && (!defined('NO_SESSION_UPDATE') || !NO_SESSION_UPDATE)) {
                     $update->id = $record->id;
                     $DB->update_record('sessions', $update);
                 }
@@ -425,7 +428,7 @@ class manager {
 
         $user = null;
 
-        if (!empty($CFG->opentogoogle)) {
+        if (!empty($CFG->opentowebcrawlers)) {
             if (\core_useragent::is_web_crawler()) {
                 $user = guest_user();
             }
@@ -632,6 +635,31 @@ class manager {
         return self::$handler->session_exists($sid);
     }
 
+    /**
+     * Return the number of seconds remaining in the current session.
+     * @param string $sid
+     */
+    public static function time_remaining($sid) {
+        global $DB, $CFG;
+
+        if (empty($CFG->version)) {
+            // Not installed yet, do not try to access database.
+            return ['userid' => 0, 'timeremaining' => $CFG->sessiontimeout];
+        }
+
+        // Note: add sessions->state checking here if it gets implemented.
+        if (!$record = $DB->get_record('sessions', array('sid' => $sid), 'id, userid, timemodified')) {
+            return ['userid' => 0, 'timeremaining' => $CFG->sessiontimeout];
+        }
+
+        if (empty($record->userid) or isguestuser($record->userid)) {
+            // Ignore guest and not-logged-in timeouts, there is very little risk here.
+            return ['userid' => 0, 'timeremaining' => $CFG->sessiontimeout];
+        } else {
+            return ['userid' => $record->userid, 'timeremaining' => $CFG->sessiontimeout - (time() - $record->timemodified)];
+        }
+    }
+
     /**
      * Fake last access for given session, this prevents session timeout.
      * @param string $sid
@@ -955,9 +983,10 @@ class manager {
      * @param string $identifier The string identifier for the message to show on failure.
      * @param string $component The string component for the message to show on failure.
      * @param int $frequency The update frequency in seconds.
+     * @param int $timeout The timeout of each request in seconds.
      * @throws coding_exception IF the frequency is longer than the session lifetime.
      */
-    public static function keepalive($identifier = 'sessionerroruser', $component = 'error', $frequency = null) {
+    public static function keepalive($identifier = 'sessionerroruser', $component = 'error', $frequency = null, $timeout = 0) {
         global $CFG, $PAGE;
 
         if ($frequency) {
@@ -966,19 +995,15 @@ class manager {
                 throw new \coding_exception('Keepalive frequency is longer than the session lifespan.');
             }
         } else {
-            // A frequency of sessiontimeout / 3 allows for one missed request whilst still preserving the session.
-            $frequency = $CFG->sessiontimeout / 3;
-        }
-
-        // Add the session keepalive script to the list of page output requirements.
-        $sessionkeepaliveurl = new \moodle_url('/lib/sessionkeepalive_ajax.php');
-        $PAGE->requires->string_for_js($identifier, $component);
-        $PAGE->requires->yui_module('moodle-core-checknet', 'M.core.checknet.init', array(array(
-            // The JS config takes this is milliseconds rather than seconds.
-            'frequency' => $frequency * 1000,
-            'message' => array($identifier, $component),
-            'uri' => $sessionkeepaliveurl->out(),
-        )));
+            // A frequency of sessiontimeout / 10 matches the timeouts in core/network amd module.
+            $frequency = $CFG->sessiontimeout / 10;
+        }
+
+        $PAGE->requires->js_call_amd('core/network', 'keepalive', array(
+                $frequency,
+                $timeout,
+                get_string($identifier, $component)
+            ));
     }
 
     /**
diff --git a/lib/classes/uuid.php b/lib/classes/uuid.php
new file mode 100644 (file)
index 0000000..b0984fa
--- /dev/null
@@ -0,0 +1,144 @@
+<?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/>.
+
+/**
+ * V4 UUID generator.
+ *
+ * @package    core
+ * @copyright  2019 Matteo Scaramuccia <moodle@matteoscaramuccia.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core;
+
+use Exception;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * V4 UUID generator class.
+ *
+ * @package    core
+ * @copyright  2019 Matteo Scaramuccia <moodle@matteoscaramuccia.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class uuid {
+    /**
+     * Generate a V4 UUID using PECL UUID extension.
+     * @see https://github.com/php/pecl-networking-uuid PECL uuid
+     * @see https://tools.ietf.org/html/rfc4122
+     *
+     * @return string|bool The UUID when PECL UUID extension is available;
+     *                     otherwise, false.
+     */
+    protected static function generate_uuid_via_pecl_uuid_extension() {
+        $uuid = false;
+
+        // Check if PECL uuid extension has been actually installed.
+        if (function_exists('\uuid_time')) {
+            // Set V4 version.
+            $uuid = \uuid_create(UUID_TYPE_RANDOM);
+        }
+
+        return $uuid;
+    }
+
+    /**
+     * Generate a V4 UUID using PHP 7+ features.
+     *
+     * @see https://www.php.net/manual/en/function.random-bytes.php
+     * @see https://tools.ietf.org/html/rfc4122
+     *
+     * @return string|bool The UUID when random_bytes() function is available;
+     *                     otherwise, false when missing the sources of randomness used by random_bytes().
+     */
+    protected static function generate_uuid_via_random_bytes() {
+        $uuid = false;
+
+        // If none of the sources of randomness are available,
+        // then an Exception will be thrown.
+        try {
+            $data = random_bytes(16);
+            $data[6] = chr((ord($data[6]) & 0x0f) | 0x40); // Set version to 0100.
+            $data[8] = chr((ord($data[8]) & 0x3f) | 0x80); // Set bits 6-7 to 10.
+            $uuid = vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
+        } catch (Exception $e) {
+            // Could not generate a random string. Is this OS secure?
+            $uuid = false;
+        }
+
+        return $uuid;
+    }
+
+    /**
+     * Generate a V4 UUID.
+     *
+     * Unique is hard. Very hard. Attempt to use the PECL UUID function if available, and if not then revert to
+     * constructing the uuid using random_bytes or mt_rand.
+     *
+     * It is important that this token is not solely based on time as this could lead
+     * to duplicates in a clustered environment (especially on VMs due to poor time precision).
+     *
+     * UUIDs are just 128 bits long but with different supported versions (RFC 4122), mainly two:
+     * - V1: the goal is uniqueness, at the cost of anonymity since it is coupled to the host generating it, via its MAC address.
+     * - V4: the goal is randomness, at the cost of (rare) collisions.
+     * Here, the V4 type is the preferred choice.
+     *
+     * The format is:
+     * xxxxxxxx-xxxx-4xxx-Yxxx-xxxxxxxxxxxx
+     * where x is any hexadecimal digit and Y is a random choice from 8, 9, a, or b.
+     *
+     * @see https://tools.ietf.org/html/rfc4122
+     *
+     * @return string The V4 UUID.
+     */
+    public static function generate() {
+        // Try PHP UUID extensions first.
+        $uuid = self::generate_uuid_via_pecl_uuid_extension();
+
+        // Fall back to better random features, when possible.
+        if (empty($uuid)) {
+            $uuid = self::generate_uuid_via_random_bytes();
+        }
+
+        // Finally, create it with the available randomness.
+        if (empty($uuid)) {
+            // Fallback uuid generation based on:
+            // "http://www.php.net/manual/en/function.uniqid.php#94959".
+            $uuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
+
+                // 32 bits for "time_low".
+                mt_rand(0, 0xffff), mt_rand(0, 0xffff),
+
+                // 16 bits for "time_mid".
+                mt_rand(0, 0xffff),
+
+                // 16 bits for "time_hi_and_version",
+                // four most significant bits holds version number 4.
+                mt_rand(0, 0x0fff) | 0x4000,
+
+                // 16 bits, 8 bits for "clk_seq_hi_res",
+                // 8 bits for "clk_seq_low",
+                // two most significant bits holds zero and one for variant DCE1.1.
+                mt_rand(0, 0x3fff) | 0x8000,
+
+                // 48 bits for "node".
+                mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff));
+        }
+
+        return trim($uuid);
+    }
+}
index 708f2cc..ce63f3d 100644 (file)
@@ -677,6 +677,22 @@ $functions = array(
         'loginrequired' => false,
         'ajax' => true,
     ),
+    'core_session_touch' => array(
+        'classname' => 'core\session\external',
+        'methodname' => 'touch_session',
+        'description' => 'Keep the users session alive',
+        'type' => 'read',
+        'loginrequired' => true,
+        'ajax' => true,
+    ),
+    'core_session_time_remaining' => array(
+        'classname' => 'core\session\external',
+        'methodname' => 'time_remaining',
+        'description' => 'Count the seconds remaining in this session',
+        'type' => 'read',
+        'loginrequired' => true,
+        'ajax' => true,
+    ),
     'core_files_get_files' => array(
         'classname' => 'core_files_external',
         'methodname' => 'get_files',
index f304bab..62a7d44 100644 (file)
@@ -3378,5 +3378,21 @@ function xmldb_main_upgrade($oldversion) {
     // Automatically generated Moodle v3.7.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2019060600.02) {
+        // Renaming 'opentogoogle' config to 'opentowebcrawlers'.
+        $opentogooglevalue = get_config('core', 'opentogoogle');
+
+        // Move the value over if it was previously configured.
+        if ($opentogooglevalue !== false) {
+            set_config('opentowebcrawlers', $opentogooglevalue);
+        }
+
+        // Remove the now unused value.
+        unset_config('opentogoogle');
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2019060600.02);
+    }
+
     return true;
 }
index 16d17fb..6512933 100644 (file)
@@ -4237,7 +4237,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null, $offlin
 
         } else if ($filearea == GRADE_FEEDBACK_FILEAREA || $filearea == GRADE_HISTORY_FEEDBACK_FILEAREA) {
             if ($context->contextlevel != CONTEXT_MODULE) {
-                send_file_not_found;
+                send_file_not_found();
             }
 
             require_login($course, false);
index ab85441..45ed645 100644 (file)
@@ -25,7 +25,7 @@
 $string['datasetsizelimited'] = 'Only part of the dataset has been evaluated due to its size. Set $CFG->mlbackend_php_no_memory_limit if you are confident that your system can cope with a {$a} dataset.';
 $string['errorcantloadmodel'] = 'Model file {$a} does not exist. The model should been trained before using it to predict.';
 $string['errorlowscore'] = 'The evaluated model prediction accuracy is not very high, so some predictions may not be accurate. Model score = {$a->score}, minimum score = {$a->minscore}';
-$string['errornotenoughdata'] = 'There is not enough data to evaluate this model using the time-splitting method.';
+$string['errornotenoughdata'] = 'There is not enough data to evaluate this model using the provided analysis interval.';
 $string['errornotenoughdatadev'] = 'The evaluation results varied too much. It is recommended that more data is gathered to ensure the model is valid. Evaluation results standard deviation = {$a->deviation}, maximum recommended standard deviation = {$a->accepteddeviation}';
 $string['errorphp7required'] = 'The PHP machine learning backend requires PHP 7';
 $string['pluginname'] = 'PHP machine learning backend';
index 5a2c831..b568f28 100644 (file)
@@ -1107,6 +1107,8 @@ class theme_config {
             }
         }
 
+        // Allow themes to change the css url to something like theme/mytheme/mycss.php.
+        component_callback('theme_' . $this->name, 'alter_css_urls', [&$urls]);
         return $urls;
     }
 
index c7c0aa1..f299050 100644 (file)
@@ -321,6 +321,7 @@ class page_requirements_manager {
             $this->M_cfg = array(
                 'wwwroot'             => $CFG->wwwroot,
                 'sesskey'             => sesskey(),
+                'sessiontimeout'      => $CFG->sessiontimeout,
                 'themerev'            => theme_get_revision(),
                 'slasharguments'      => (int)(!empty($CFG->slasharguments)),
                 'theme'               => $page->theme->name,
diff --git a/lib/pear/Auth/RADIUS.php b/lib/pear/Auth/RADIUS.php
deleted file mode 100644 (file)
index 297b60d..0000000
+++ /dev/null
@@ -1,1008 +0,0 @@
-<?php
-/* vim: set expandtab tabstop=4 shiftwidth=4: */
-/*
-Copyright (c) 2003, Michael Bretterklieber <michael@bretterklieber.com>
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions
-are met:
-
-1. Redistributions of source code must retain the above copyright
-   notice, this list of conditions and the following disclaimer.
-2. Redistributions in binary form must reproduce the above copyright
-   notice, this list of conditions and the following disclaimer in the
-   documentation and/or other materials provided with the distribution.
-3. The names of the authors may not be used to endorse or promote products
-   derived from this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
-IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
-INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
-BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
-OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
-NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
-EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-This code cannot simply be copied and put under the GNU Public License or
-any other GPL-like (LGPL, GPL2) License.
-
-    $Id$
-*/
-
-require_once('PEAR.php');
-
-/**
- * Client implementation of RADIUS. This are wrapper classes for
- * the RADIUS PECL.
- * Provides RADIUS Authentication (RFC2865) and RADIUS Accounting (RFC2866).
- *
- * @package Auth_RADIUS
- * @author  Michael Bretterklieber <michael@bretterklieber.com>
- * @access  public
- * @version $Revision$
- */
-
-
-/**
- * class Auth_RADIUS
- *
- * Abstract base class for RADIUS
- *
- * @package Auth_RADIUS
- */
-class Auth_RADIUS extends PEAR {
-
-    /**
-     * List of RADIUS servers.
-     * @var  array
-     * @see  addServer(), putServer()
-     */
-    var $_servers  = array();
-
-    /**
-     * Path to the configuration-file.
-     * @var  string
-     * @see  setConfigFile()
-     */
-    var $_configfile = null;
-
-    /**
-     * Resource.
-     * @var  resource
-     * @see  open(), close()
-     */
-    var $res = null;
-
-    /**
-     * Username for authentication and accounting requests.
-     * @var  string
-     */
-    var $username = null;
-
-    /**
-     * Password for plaintext-authentication (PAP).
-     * @var  string
-     */
-    var $password = null;
-
-    /**
-     * List of known attributes.
-     * @var  array
-     * @see  dumpAttributes(), getAttributes()
-     */
-    var $attributes = array();
-
-    /**
-     * List of raw attributes.
-     * @var  array
-     * @see  dumpAttributes(), getAttributes()
-     */
-    var $rawAttributes = array();
-
-    /**
-     * List of raw vendor specific attributes.
-     * @var  array
-     * @see  dumpAttributes(), getAttributes()
-     */
-    var $rawVendorAttributes = array();
-
-    /**
-     * Switch whether we should put standard attributes or not
-     * @var  bool
-     * @see  putStandardAttributes()
-     */
-    var $useStandardAttributes = true;
-
-    /**
-     * Constructor
-     *
-     * Loads the RADIUS PECL/extension
-     *
-     * @return void
-     */
-    public function __construct()
-    {
-        $this->loadExtension('radius');
-    }
-
-    /**
-     */
-    public function loadExtension($ext) {
-        if (extension_loaded($ext)) {
-            return true;
-        }
-        // if either returns true dl() will produce a FATAL error, stop that
-        if (
-            function_exists('dl') === false ||
-            ini_get('enable_dl') != 1 ||
-            ini_get('safe_mode') == 1
-        ) {
-            return false;
-        }
-        if (OS_WINDOWS) {
-            $suffix = '.dll';
-        } elseif (PHP_OS == 'HP-UX') {
-            $suffix = '.sl';
-        } elseif (PHP_OS == 'AIX') {
-            $suffix = '.a';
-        } elseif (PHP_OS == 'OSX') {
-            $suffix = '.bundle';
-        } else {
-            $suffix = '.so';
-        }
-        return @dl('php_'.$ext.$suffix) || @dl($ext.$suffix);
-    }
-
-    /**
-     * Adds a RADIUS server to the list of servers for requests.
-     *
-     * At most 10 servers may be specified.    When multiple servers
-     * are given, they are tried in round-robin fashion until a
-     * valid response is received
-     *
-     * @param  string  $servername   Servername or IP-Address
-     * @param  integer $port         Portnumber
-     * @param  string  $sharedSecret Shared secret
-     * @param  integer $timeout      Timeout for each request
-     * @param  integer $maxtries     Max. retries for each request
-     * @return void
-     */
-    public function addServer($servername = 'localhost', $port = 0, $sharedSecret = 'testing123', $timeout = 3, $maxtries = 3)
-    {
-        $this->_servers[] = array($servername, $port, $sharedSecret, $timeout, $maxtries);
-    }
-
-    /**
-     * Returns an error message, if an error occurred.
-     *
-     * @return string
-     */
-    public function getError()
-    {
-        return radius_strerror($this->res);
-    }
-
-    /**
-     * Sets the configuration-file.
-     *
-     * @param  string  $file Path to the configuration file
-     * @return void
-     */
-    public function setConfigfile($file)
-    {
-        $this->_configfile = $file;
-    }
-
-    /**
-     * Puts an attribute.
-     *
-     * @param  integer $attrib       Attribute-number
-     * @param  mixed   $port         Attribute-value
-     * @param  type    $type         Attribute-type
-     * @return bool  true on success, false on error
-     */
-    public function putAttribute($attrib, $value, $type = null)
-    {
-        if ($type == null) {
-            $type = gettype($value);
-        }
-
-        switch ($type) {
-            case 'integer':
-            case 'double':
-                return radius_put_int($this->res, $attrib, $value);
-
-            case 'addr':
-                return radius_put_addr($this->res, $attrib, $value);
-
-            case 'string':
-            default:
-                return radius_put_attr($this->res, $attrib, $value);
-        }
-
-    }
-
-    /**
-     * Puts a vendor-specific attribute.
-     *
-     * @param  integer $vendor       Vendor (MSoft, Cisco, ...)
-     * @param  integer $attrib       Attribute-number
-     * @param  mixed   $port         Attribute-value
-     * @param  type    $type         Attribute-type
-     * @return bool  true on success, false on error
-     */
-    public function putVendorAttribute($vendor, $attrib, $value, $type = null)
-    {
-
-        if ($type == null) {
-            $type = gettype($value);
-        }
-
-        switch ($type) {
-            case 'integer':
-            case 'double':
-                return radius_put_vendor_int($this->res, $vendor, $attrib, $value);
-
-            case 'addr':
-                return radius_put_vendor_addr($this->res, $vendor,$attrib, $value);
-
-            case 'string':
-            default:
-                return radius_put_vendor_attr($this->res, $vendor, $attrib, $value);
-        }
-
-    }
-
-    /**
-     * Prints known attributes received from the server.
-     *
-     */
-    public function dumpAttributes()
-    {
-        foreach ($this->attributes as $name => $data) {
-            echo "$name:$data<br>\n";
-        }
-    }
-
-    /**
-     * Overwrite this.
-     */
-    public function open()
-    {
-    }
-
-    /**
-     * Overwrite this.
-     */
-    public function createRequest()
-    {
-    }
-
-    /**
-     * Puts standard attributes.
-     */
-    public function putStandardAttributes()
-    {
-        if (!$this->useStandardAttributes) {
-            return;
-        }
-
-        if (isset($_SERVER)) {
-            $var = $_SERVER;
-        } else {
-            $var = $GLOBALS['HTTP_SERVER_VARS'];
-        }
-
-        $this->putAttribute(RADIUS_NAS_IDENTIFIER, isset($var['HTTP_HOST']) ? $var['HTTP_HOST'] : 'localhost');
-        $this->putAttribute(RADIUS_NAS_PORT_TYPE, RADIUS_VIRTUAL);
-        $this->putAttribute(RADIUS_SERVICE_TYPE, RADIUS_FRAMED);
-        $this->putAttribute(RADIUS_FRAMED_PROTOCOL, RADIUS_PPP);
-        $this->putAttribute(RADIUS_CALLING_STATION_ID, isset($var['REMOTE_HOST']) ? $var['REMOTE_HOST'] : '127.0.0.1');
-    }
-
-    /**
-     * Puts custom attributes.
-     */
-    public function putAuthAttributes()
-    {
-        if (isset($this->username)) {
-            $this->putAttribute(RADIUS_USER_NAME, $this->username);
-        }
-    }
-
-    /**
-     * Configures the radius library.
-     *
-     * @param  string  $servername   Servername or IP-Address
-     * @param  integer $port         Portnumber
-     * @param  string  $sharedSecret Shared secret
-     * @param  integer $timeout      Timeout for each request
-     * @param  integer $maxtries     Max. retries for each request
-     * @return bool  true on success, false on error
-     * @see addServer()
-     */
-    public function putServer($servername, $port = 0, $sharedsecret = 'testing123', $timeout = 3, $maxtries = 3)
-    {
-        if (!radius_add_server($this->res, $servername, $port, $sharedsecret, $timeout, $maxtries)) {
-            return false;
-        }
-        return true;
-    }
-
-    /**
-     * Configures the radius library via external configurationfile
-     *
-     * @param  string  $servername   Servername or IP-Address
-     * @return bool  true on success, false on error
-     */
-    public function putConfigfile($file)
-    {
-        if (!radius_config($this->res, $file)) {
-            return false;
-        }
-        return true;
-    }
-
-    /**
-     * Initiates a RADIUS request.
-     *
-     * @return bool  true on success, false on errors
-     */
-    public function start()
-    {
-        if (!$this->open()) {
-            return false;
-        }
-
-        foreach ($this->_servers as $s) {
-            // Servername, port, sharedsecret, timeout, retries
-            if (!$this->putServer($s[0], $s[1], $s[2], $s[3], $s[4])) {
-                return false;
-            }
-        }
-
-        if (!empty($this->_configfile)) {
-            if (!$this->putConfigfile($this->_configfile)) {
-                return false;
-            }
-        }
-
-        $this->createRequest();
-        $this->putStandardAttributes();
-        $this->putAuthAttributes();
-        return true;
-    }
-
-    /**
-     * Sends a prepared RADIUS request and waits for a response
-     *
-     * @return mixed  true on success, false on reject, PEAR_Error on error
-     */
-    public function send()
-    {
-        $req = radius_send_request($this->res);
-        if (!$req) {
-            throw new Auth_RADIUS_Exception('Error sending request: ' . $this->getError());
-        }
-
-        switch($req) {
-            case RADIUS_ACCESS_ACCEPT:
-                if (is_subclass_of($this, 'auth_radius_acct')) {
-                    throw new Auth_RADIUS_Exception('RADIUS_ACCESS_ACCEPT is unexpected for accounting');
-                }
-                return true;
-
-            case RADIUS_ACCESS_REJECT:
-                return false;
-
-            case RADIUS_ACCOUNTING_RESPONSE:
-                if (is_subclass_of($this, 'auth_radius_pap')) {
-                    throw new Auth_RADIUS_Exception('RADIUS_ACCOUNTING_RESPONSE is unexpected for authentication');
-                }
-                return true;
-
-            default:
-                throw new Auth_RADIUS_Exception("Unexpected return value: $req");
-        }
-
-    }
-
-    /**
-     * Reads all received attributes after sending the request.
-     *
-     * This methods stores known attributes in the property attributes,
-     * all attributes (including known attibutes) are stored in rawAttributes
-     * or rawVendorAttributes.
-     * NOTE: call this function also even if the request was rejected, because the
-     * Server returns usualy an errormessage
-     *
-     * @return bool   true on success, false on error
-     */
-    public function getAttributes()
-    {
-
-        while ($attrib = radius_get_attr($this->res)) {
-
-            if (!is_array($attrib)) {
-                return false;
-            }
-
-            $attr = $attrib['attr'];
-            $data = $attrib['data'];
-
-            $this->rawAttributes[$attr] = $data;
-
-            switch ($attr) {
-                case RADIUS_FRAMED_IP_ADDRESS:
-                    $this->attributes['framed_ip'] = radius_cvt_addr($data);
-                    break;
-
-                case RADIUS_FRAMED_IP_NETMASK:
-                    $this->attributes['framed_mask'] = radius_cvt_addr($data);
-                    break;
-
-                case RADIUS_FRAMED_MTU:
-                    $this->attributes['framed_mtu'] = radius_cvt_int($data);
-                    break;
-
-                case RADIUS_FRAMED_COMPRESSION:
-                    $this->attributes['framed_compression'] = radius_cvt_int($data);
-                    break;
-
-                case RADIUS_SESSION_TIMEOUT:
-                    $this->attributes['session_timeout'] = radius_cvt_int($data);
-                    break;
-
-                case RADIUS_IDLE_TIMEOUT:
-                    $this->attributes['idle_timeout'] = radius_cvt_int($data);
-                    break;
-
-                case RADIUS_SERVICE_TYPE:
-                    $this->attributes['service_type'] = radius_cvt_int($data);
-                    break;
-
-                case RADIUS_CLASS:
-                    $this->attributes['class'] = radius_cvt_string($data);
-                    break;
-
-                case RADIUS_FRAMED_PROTOCOL:
-                    $this->attributes['framed_protocol'] = radius_cvt_int($data);
-                    break;
-
-                case RADIUS_FRAMED_ROUTING:
-                    $this->attributes['framed_routing'] = radius_cvt_int($data);
-                    break;
-
-                case RADIUS_FILTER_ID:
-                    $this->attributes['filter_id'] = radius_cvt_string($data);
-                    break;
-
-                case RADIUS_REPLY_MESSAGE:
-                    $this->attributes['reply_message'] = radius_cvt_string($data);
-                    break;
-
-                case RADIUS_VENDOR_SPECIFIC:
-                    $attribv = radius_get_vendor_attr($data);
-                    if (!is_array($attribv)) {
-                        return false;
-                    }
-
-                    $vendor = $attribv['vendor'];
-                    $attrv = $attribv['attr'];
-                    $datav = $attribv['data'];
-
-                    $this->rawVendorAttributes[$vendor][$attrv] = $datav;
-
-                    if ($vendor == RADIUS_VENDOR_MICROSOFT) {
-
-                        switch ($attrv) {
-                            case RADIUS_MICROSOFT_MS_CHAP2_SUCCESS:
-                                $this->attributes['ms_chap2_success'] = radius_cvt_string($datav);
-                                break;
-
-                            case RADIUS_MICROSOFT_MS_CHAP_ERROR:
-                                $this->attributes['ms_chap_error'] = radius_cvt_string(substr($datav,1));
-                                break;
-
-                            case RADIUS_MICROSOFT_MS_CHAP_DOMAIN:
-                                $this->attributes['ms_chap_domain'] = radius_cvt_string($datav);
-                                break;
-
-                            case RADIUS_MICROSOFT_MS_MPPE_ENCRYPTION_POLICY:
-                                $this->attributes['ms_mppe_encryption_policy'] = radius_cvt_int($datav);
-                                break;
-
-                            case RADIUS_MICROSOFT_MS_MPPE_ENCRYPTION_TYPES:
-                                $this->attributes['ms_mppe_encryption_types'] = radius_cvt_int($datav);
-                                break;
-
-                            case RADIUS_MICROSOFT_MS_CHAP_MPPE_KEYS:
-                                $demangled = radius_demangle($this->res, $datav);
-                                $this->attributes['ms_chap_mppe_lm_key'] = substr($demangled, 0, 8);
-                                $this->attributes['ms_chap_mppe_nt_key'] = substr($demangled, 8, RADIUS_MPPE_KEY_LEN);
-                                break;
-
-                            case RADIUS_MICROSOFT_MS_MPPE_SEND_KEY:
-                                $this->attributes['ms_chap_mppe_send_key'] = radius_demangle_mppe_key($this->res, $datav);
-                                break;
-
-                            case RADIUS_MICROSOFT_MS_MPPE_RECV_KEY:
-                                $this->attributes['ms_chap_mppe_recv_key'] = radius_demangle_mppe_key($this->res, $datav);
-                                break;
-
-                            case RADIUS_MICROSOFT_MS_PRIMARY_DNS_SERVER:
-                                $this->attributes['ms_primary_dns_server'] = radius_cvt_string($datav);
-                                break;
-                        }
-                    }
-                    break;
-
-            }
-        }
-
-        return true;
-    }
-
-    /**
-     * Frees resources.
-     *
-     * Calling this method is always a good idea, because all security relevant
-     * attributes are filled with Nullbytes to leave nothing in the mem.
-     *
-     */
-    public function close()
-    {
-        if ($this->res != null) {
-            radius_close($this->res);
-            $this->res = null;
-        }
-        $this->username = str_repeat("\0", strlen($this->username));
-        $this->password = str_repeat("\0", strlen($this->password));
-    }
-
-}
-
-/**
- * class Auth_RADIUS_PAP
- *
- * Class for authenticating using PAP (Plaintext)
- *
- * @package Auth_RADIUS
- */
-class Auth_RADIUS_PAP extends Auth_RADIUS
-{
-
-    /**
-     * Constructor
-     *
-     * @param  string  $username   Username
-     * @param  string  $password   Password
-     * @return void
-     */
-    public function __construct($username = null, $password = null)
-    {
-        parent::__construct();
-        $this->username = $username;
-        $this->password = $password;
-    }
-
-    /**
-     * Creates a RADIUS resource
-     *
-     * Creates a RADIUS resource for authentication. This should be the first
-     * call before you make any other things with the library.
-     *
-     * @return bool   true on success, false on error
-     */
-    function open()
-    {
-        $this->res = radius_auth_open();
-        if (!$this->res) {
-            return false;
-        }
-        return true;
-    }
-
-    /**
-     * Creates an authentication request
-     *
-     * Creates an authentication request.
-     * You MUST call this method before you can put any attribute
-     *
-     * @return bool   true on success, false on error
-     */
-    function createRequest()
-    {
-        if (!radius_create_request($this->res, RADIUS_ACCESS_REQUEST)) {
-            return false;
-        }
-        return true;
-    }
-
-    /**
-     * Put authentication specific attributes
-     *
-     * @return void
-     */
-    function putAuthAttributes()
-    {
-        if (isset($this->username)) {
-            $this->putAttribute(RADIUS_USER_NAME, $this->username);
-        }
-        if (isset($this->password)) {
-            $this->putAttribute(RADIUS_USER_PASSWORD, $this->password);
-        }
-    }
-
-}
-
-/**
- * class Auth_RADIUS_CHAP_MD5
- *
- * Class for authenticating using CHAP-MD5 see RFC1994.
- * Instead og the plaintext password the challenge and
- * the response are needed.
- *
- * @package Auth_RADIUS
- */
-class Auth_RADIUS_CHAP_MD5 extends Auth_RADIUS_PAP
-{
-    /**
-     * 8 Bytes binary challenge
-     * @var  string
-     */
-    var $challenge = null;
-
-    /**
-     * 16 Bytes MD5 response binary
-     * @var  string
-     */
-    var $response = null;
-
-    /**
-     * Id of the authentication request. Should incremented after every request.
-     * @var  integer
-     */
-    var $chapid = 1;
-
-    /**
-     * Constructor
-     *
-     * @param  string  $username   Username
-     * @param  string  $challenge  8 Bytes Challenge (binary)
-     * @param  integer $chapid     Requestnumber
-     * @return void
-     */
-    function __construct($username = null, $challenge = null, $chapid = 1)
-    {
-        parent::__construct();
-        $this->username = $username;
-        $this->challenge = $challenge;
-        $this->chapid = $chapid;
-    }
-
-    /**
-     * Put CHAP-MD5 specific attributes
-     *
-     * For authenticating using CHAP-MD5 via RADIUS you have to put the challenge
-     * and the response. The chapid is inserted in the first byte of the response.
-     *
-     * @return void
-     */
-    function putAuthAttributes()
-    {
-        if (isset($this->username)) {
-            $this->putAttribute(RADIUS_USER_NAME, $this->username);
-        }
-        if (isset($this->response)) {
-            $response = pack('C', $this->chapid) . $this->response;
-            $this->putAttribute(RADIUS_CHAP_PASSWORD, $response);
-        }
-        if (isset($this->challenge)) {
-            $this->putAttribute(RADIUS_CHAP_CHALLENGE, $this->challenge);
-        }
-    }
-
-    /**
-     * Frees resources.
-     *
-     * Calling this method is always a good idea, because all security relevant
-     * attributes are filled with Nullbytes to leave nothing in the mem.
-     */
-    public function close()
-    {
-        parent::close();
-        $this->challenge =  str_repeat("\0", strlen($this->challenge));
-        $this->response =  str_repeat("\0", strlen($this->response));
-    }
-
-}
-
-/**
- * class Auth_RADIUS_MSCHAPv1
- *
- * Class for authenticating using MS-CHAPv1 see RFC2433
- *
- * @package Auth_RADIUS
- */
-class Auth_RADIUS_MSCHAPv1 extends Auth_RADIUS_CHAP_MD5
-{
-    /**
-     * LAN-Manager-Response
-     * @var  string
-     */
-    var $lmResponse = null;
-
-    /**
-     * Wether using deprecated LM-Responses or not.
-     * 0 = use LM-Response, 1 = use NT-Response
-     * @var  bool
-     */
-    var $flags = 1;
-
-    /**
-     * Put MS-CHAPv1 specific attributes
-     *
-     * For authenticating using MS-CHAPv1 via RADIUS you have to put the challenge
-     * and the response. The response has this structure:
-     * struct rad_mschapvalue {
-     *   u_char ident;
-     *   u_char flags;
-     *   u_char lm_response[24];
-     *   u_char response[24];
-     * };
-     *
-     * @return void
-     */
-    function putAuthAttributes()
-    {
-        if (isset($this->username)) {
-            $this->putAttribute(RADIUS_USER_NAME, $this->username);
-        }
-        if (isset($this->response) || isset($this->lmResponse)) {
-            $lmResp = isset($this->lmResponse) ? $this->lmResponse : str_repeat ("\0", 24);
-            $ntResp = isset($this->response)   ? $this->response :   str_repeat ("\0", 24);
-            $resp = pack('CC', $this->chapid, $this->flags) . $lmResp . $ntResp;
-            $this->putVendorAttribute(RADIUS_VENDOR_MICROSOFT, RADIUS_MICROSOFT_MS_CHAP_RESPONSE, $resp);
-        }
-        if (isset($this->challenge)) {
-            $this->putVendorAttribute(RADIUS_VENDOR_MICROSOFT, RADIUS_MICROSOFT_MS_CHAP_CHALLENGE, $this->challenge);
-        }
-    }
-}
-
-/**
- * class Auth_RADIUS_MSCHAPv2
- *
- * Class for authenticating using MS-CHAPv2 see RFC2759
- *
- * @package Auth_RADIUS
- */
-class Auth_RADIUS_MSCHAPv2 extends Auth_RADIUS_MSCHAPv1
-{
-    /**
-     * 16 Bytes binary challenge
-     * @var  string
-     */
-    var $challenge = null;
-
-    /**
-     * 16 Bytes binary Peer Challenge
-     * @var  string
-     */
-    var $peerChallenge = null;
-
-    /**
-     * Put MS-CHAPv2 specific attributes
-     *
-     * For authenticating using MS-CHAPv1 via RADIUS you have to put the challenge
-     * and the response. The response has this structure:
-     * struct rad_mschapv2value {
-     *   u_char ident;
-     *   u_char flags;
-     *   u_char pchallenge[16];
-     *   u_char reserved[8];
-     *   u_char response[24];
-     * };
-     * where pchallenge is the peer challenge. Like for MS-CHAPv1 we set the flags field to 1.
-     * @return void
-     */
-    function putAuthAttributes()
-    {
-        if (isset($this->username)) {
-            $this->putAttribute(RADIUS_USER_NAME, $this->username);
-        }
-        if (isset($this->response) && isset($this->peerChallenge)) {
-            // Response: chapid, flags (1 = use NT Response), Peer challenge, reserved, Response
-            $resp = pack('CCa16a8a24',$this->chapid , 1, $this->peerChallenge, str_repeat("\0", 8), $this->response);
-            $this->putVendorAttribute(RADIUS_VENDOR_MICROSOFT, RADIUS_MICROSOFT_MS_CHAP2_RESPONSE, $resp);
-        }
-        if (isset($this->challenge)) {
-            $this->putVendorAttribute(RADIUS_VENDOR_MICROSOFT, RADIUS_MICROSOFT_MS_CHAP_CHALLENGE, $this->challenge);
-        }
-    }
-
-    /**
-     * Frees resources.
-     *
-     * Calling this method is always a good idea, because all security relevant
-     * attributes are filled with Nullbytes to leave nothing in the mem.
-     *
-     * @access public
-     */
-    function close()
-    {
-        parent::close();
-        $this->peerChallenge = str_repeat("\0", strlen($this->peerChallenge));
-    }
-}
-
-/**
- * class Auth_RADIUS_Acct
- *
- * Class for RADIUS accounting
- *
- * @package Auth_RADIUS
- */
-class Auth_RADIUS_Acct extends Auth_RADIUS
-{
-    /**
-     * Defines where the Authentication was made, possible values are:
-     * RADIUS_AUTH_RADIUS, RADIUS_AUTH_LOCAL, RADIUS_AUTH_REMOTE
-     * @var  integer
-     */
-    var $authentic = null;
-
-    /**
-     * Defines the type of the accounting request, on of:
-     * RADIUS_START, RADIUS_STOP, RADIUS_ACCOUNTING_ON, RADIUS_ACCOUNTING_OFF
-     * @var  integer
-     */
-    var $status_type = null;
-
-    /**
-     * The time the user was logged in in seconds
-     * @var  integer
-     */
-    var $session_time = null;
-
-    /**
-     * A uniq identifier for the session of the user, maybe the PHP-Session-Id
-     * @var  string
-     */
-    var $session_id = null;
-
-    /**
-     * Constructor
-     *
-     * Generates a predefined session_id. We use the Remote-Address, the PID, and the Current user.
-     * @return void
-     */
-    function __construct()
-    {
-        parent::__construct();
-
-        if (isset($_SERVER)) {
-            $var = $_SERVER;
-        } else {
-            $var = $GLOBALS['HTTP_SERVER_VARS'];
-        }
-
-        $this->session_id = sprintf("%s:%d-%s", isset($var['REMOTE_ADDR']) ? $var['REMOTE_ADDR'] : '127.0.0.1' , getmypid(), get_current_user());
-    }
-
-    /**
-     * Creates a RADIUS resource
-     *
-     * Creates a RADIUS resource for accounting. This should be the first
-     * call before you make any other things with the library.
-     *
-     * @return bool   true on success, false on error
-     */
-    function open()
-    {
-        $this->res = radius_acct_open();
-        if (!$this->res) {
-            return false;
-        }
-        return true;
-    }
-
-    /**
-     * Creates an accounting request
-     *
-     * Creates an accounting request.
-     * You MUST call this method before you can put any attribute.
-     *
-     * @return bool   true on success, false on error
-     */
-    function createRequest()
-    {
-        if (!radius_create_request($this->res, RADIUS_ACCOUNTING_REQUEST)) {
-            return false;
-        }
-        return true;
-    }
-
-    /**
-     * Put attributes for accounting.
-     *
-     * Here we put some accounting values. There many more attributes for accounting,
-     * but for web-applications only certain attributes make sense.
-     * @return void
-     */
-    function putAuthAttributes()
-    {
-        $this->putAttribute(RADIUS_ACCT_SESSION_ID, $this->session_id);
-        $this->putAttribute(RADIUS_ACCT_STATUS_TYPE, $this->status_type);
-        if (isset($this->session_time) && $this->status_type == RADIUS_STOP) {
-            $this->putAttribute(RADIUS_ACCT_SESSION_TIME, $this->session_time);
-        }
-        if (isset($this->authentic)) {
-            $this->putAttribute(RADIUS_ACCT_AUTHENTIC, $this->authentic);
-        }
-
-    }
-
-}
-
-/**
- * class Auth_RADIUS_Acct_Start
- *
- * Class for RADIUS accounting. Its usualy used, after the user has logged in.
- *
- * @package Auth_RADIUS
- */
-class Auth_RADIUS_Acct_Start extends Auth_RADIUS_Acct
-{
-    /**
-     * Defines the type of the accounting request.
-     * It is set to RADIUS_START by default in this class.
-     * @var  integer
-     */
-    var $status_type = RADIUS_START;
-}
-
-/**
- * class Auth_RADIUS_Acct_Start
- *
- * Class for RADIUS accounting. Its usualy used, after the user has logged out.
- *
- * @package Auth_RADIUS
- */
-class Auth_RADIUS_Acct_Stop extends Auth_RADIUS_Acct
-{
-    /**
-     * Defines the type of the accounting request.
-     * It is set to RADIUS_STOP by default in this class.
-     * @var  integer
-     */
-    var $status_type = RADIUS_STOP;
-}
-
-if (!defined('RADIUS_UPDATE')) {
-    define('RADIUS_UPDATE', 3);
-}
-
-/**
- * class Auth_RADIUS_Acct_Update
- *
- * Class for interim RADIUS accounting updates.
- *
- * @package Auth_RADIUS
- */
-class Auth_RADIUS_Acct_Update extends Auth_RADIUS_Acct
-{
-    /**
-     * Defines the type of the accounting request.
-     * It is set to RADIUS_UPDATE by default in this class.
-     * @var  integer
-     */
-    var $status_type = RADIUS_UPDATE;
-}
-
-class Auth_RADIUS_Exception extends Exception {}
\ No newline at end of file
index de71c62..42af0e9 100644 (file)
@@ -45,11 +45,6 @@ In detail, the libraries added here are:
     - by Elizabeth Smith, Arpad Ray, Joshua Eichorn, David Coallier and Laurent Yaish
     - License: LGPL
     - http://pear.php.net/package/HTML_AJAX/
-- PEAR Auth_RADIUS:
-    - Current version: 1.1.0 (2015-02-10)
-    - by Michael Bretterklieber
-    - License: BSD
-    - http://pear.php.net/package/Auth_RADIUS
 - PEAR Crypt_CHAP:
     - Current Version: 1.0.1 (2007-03-14)
     - by Michael Bretterklieber
index 05043ac..fb38615 100644 (file)
@@ -1,18 +1,6 @@
 MOODLE-SPECIFIC PEAR MODIFICATIONS
 ==================================
 
-Auth/RADIUS
-===========
-
-1/ Changed static call to correct alternative (MDL-38373):
-    - From: PEAR::loadExtension('radius'); (in global scope)
-    - To: $this->loadExtension('radius'); (in constructor)
-2/ Upgraded to version 1.1.0 (see MDL-51523).
-   Changes made to the lib/pear/Auth/RADIUS.php file that was downloaded.
-    - Added "require_once('PEAR.php')".
-    - Changed the 'Auth_RADIUS' class so that it extends the 'PEAR' class.
-    - Changed the function 'loadExtension' to public.
-
 XML/Parser
 =================
 1/ changed ereg_ to preg_
diff --git a/lib/recaptchalib.php b/lib/recaptchalib.php
deleted file mode 100644 (file)
index 79eb4a3..0000000
+++ /dev/null
@@ -1,343 +0,0 @@
-<?php
-
-/**
- * This is a PHP library that handles calling reCAPTCHA.
- *    - Documentation and latest version
- *          {@link http://code.google.com/apis/recaptcha/docs/php.html}
- *    - Get a reCAPTCHA API Key
- *          {@link https://www.google.com/recaptcha/admin/create}
- *    - Discussion group
- *          {@link http://groups.google.com/group/recaptcha}
- *
- * Copyright (c) 2007 reCAPTCHA -- {@link http://www.google.com/recaptcha}
- * AUTHORS:
- *   Mike Crawford
- *   Ben Maurer
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- *
- * @package moodlecore
- * @copyright (c) 2007 reCAPTCHA -- {@link http://www.google.com/recaptcha}
- */
-
-/**
- * The reCAPTCHA server URL's
- */
-define("RECAPTCHA_API_SERVER", "http://www.google.com/recaptcha/api");
-define("RECAPTCHA_API_SECURE_SERVER", "https://www.google.com/recaptcha/api");
-define("RECAPTCHA_VERIFY_SERVER", "www.google.com");
-
-/**
- * Encodes the given data into a query string format
- * @param $data - array of string elements to be encoded
- * @return string - encoded request
- */
-function _recaptcha_qsencode ($data) {
-        $req = "";
-        foreach ( $data as $key => $value )
-                $req .= $key . '=' . urlencode( $value ) . '&';
-
-        // Cut the last '&'
-        $req=substr($req,0,strlen($req)-1);
-        return $req;
-}
-
-
-
-/**
- * Submits an HTTP POST to a reCAPTCHA server
- *
- * @global object
- * @param string $host
- * @param string $path
- * @param array $data
- * @param int port
- * @return array response
- */
-function _recaptcha_http_post($host, $path, $data, $port = 80, $https=false) {
-        global $CFG;
-        $protocol = 'http';
-        if ($https) {
-            $protocol = 'https';
-        }
-
-        require_once $CFG->libdir . '/filelib.php';
-
-        $req = _recaptcha_qsencode ($data);
-
-        $headers = array();
-        $headers['Host'] = $host;
-        $headers['Content-Type'] = 'application/x-www-form-urlencoded';
-        $headers['Content-Length'] = strlen($req);
-        $headers['User-Agent'] = 'reCAPTCHA/PHP';
-
-        $results = download_file_content("$protocol://" . $host . $path, $headers, $data, false, 300, 20, true);
-
-        if ($results) {
-            return array(1 => $results);
-        } else {
-            return false;
-        }
-}
-
-/**
- * Return the recaptcha challenge and image and javascript urls
- *
- * @param  string $server    server url
- * @param  string $pubkey    public key
- * @param  string $errorpart error part to append
- * @return array the challenge hash, image and javascript url
- * @since  Moodle 3.2
- */
-function recaptcha_get_challenge_hash_and_urls($server, $pubkey, $errorpart = '') {
-    global $CFG;
-
-    require_once($CFG->libdir . '/filelib.php');
-    $html = download_file_content($server . '/noscript?k=' . $pubkey . $errorpart, null, null, false, 300, 20, true);
-    preg_match('/image\?c\=([A-Za-z0-9\-\_]*)\"/', $html, $matches);
-    $challengehash = $matches[1];
-    $imageurl = $server . '/image?c=' . $challengehash;
-
-    $jsurl = $server . '/challenge?k=' . $pubkey . $errorpart;
-
-    return array($challengehash, $imageurl, $jsurl);
-}
-
-
-/**
- * Gets the challenge HTML (javascript and non-javascript version).
- * This is called from the browser, and the resulting reCAPTCHA HTML widget
- * is embedded within the HTML form it was called from.
- *
- * @global object
- * @param string $pubkey A public key for reCAPTCHA
- * @param string $error The error given by reCAPTCHA (optional, default is null)
- * @param boolean $use_ssl Should the request be made over ssl? (optional, default is false)
-
- * @return string - The HTML to be embedded in the user's form.
- */
-function recaptcha_get_html ($pubkey, $error = null, $use_ssl = false) {
-    global $PAGE;
-
-    $recaptchatype = optional_param('recaptcha', 'image', PARAM_TEXT);
-
-    if ($pubkey == null || $pubkey == '') {
-               die ("To use reCAPTCHA you must get an API key from <a href='https://www.google.com/recaptcha/admin/create'>https://www.google.com/recaptcha/admin/create</a>");
-    }
-
-    if ($use_ssl) {
-        $server = RECAPTCHA_API_SECURE_SERVER;
-    } else {
-        $server = RECAPTCHA_API_SERVER;
-    }
-
-    $errorpart = "";
-    if ($error) {
-        $errorpart = "&amp;error=" . $error;
-    }
-
-    list($challengehash, $imageurl, $jsurl) = recaptcha_get_challenge_hash_and_urls($server, $pubkey, $errorpart);
-
-    $strincorrectpleasetryagain = get_string('incorrectpleasetryagain', 'auth');
-    $strenterthewordsabove = get_string('enterthewordsabove', 'auth');
-    $strenterthenumbersyouhear = get_string('enterthenumbersyouhear', 'auth');
-    $strgetanothercaptcha = get_string('getanothercaptcha', 'auth');
-    $strgetanaudiocaptcha = get_string('getanaudiocaptcha', 'auth');
-    $strgetanimagecaptcha = get_string('getanimagecaptcha', 'auth');
-
-    $return = html_writer::script('', $jsurl);
-    $return .= '<noscript>
-        <div id="recaptcha_widget_noscript">
-        <div id="recaptcha_image_noscript"><img src="' . $imageurl . '" alt="reCAPTCHA"/></div>';
-
-    if ($error == 'incorrect-captcha-sol') {
-        $return .= '<div class="recaptcha_only_if_incorrect_sol" style="color:red">' . $strincorrectpleasetryagain . '</div>';
-    }
-
-    if ($recaptchatype == 'image') {
-        $return .= '<span class="recaptcha_only_if_image">' . $strenterthewordsabove . '</span>';
-    } elseif ($recaptchatype == 'audio') {
-        $return .= '<span class="recaptcha_only_if_audio">' . $strenterthenumbersyouhear . '</span>';
-    }
-
-    $return .= '<input type="text" id="recaptcha_response_field_noscript" name="recaptcha_response_field" />';
-    $return .= '<input type="hidden" id="recaptcha_challenge_field_noscript" name="recaptcha_challenge_field" value="' . $challengehash . '" />';
-    $return .= '<div><a href="signup.php">' . $strgetanothercaptcha . '</a></div>';
-
-    // Disabling audio recaptchas for now: not language-independent
-    /*
-    if ($recaptchatype == 'image') {
-        $return .= '<div class="recaptcha_only_if_image"><a href="signup.php?recaptcha=audio">' . $strgetanaudiocaptcha . '</a></div>';
-    } elseif ($recaptchatype == 'audio') {
-        $return .= '<div class="recaptcha_only_if_audio"><a href="signup.php?recaptcha=image">' . $strgetanimagecaptcha . '</a></div>';
-    }
-    */
-
-    $return .= '
-        </div>
-    </noscript>';
-
-    return $return;
-}
-
-
-
-
-/**
- * A ReCaptchaResponse is returned from recaptcha_check_answer()
- *
- * @package moodlecore
- * @copyright (c) 2007 reCAPTCHA -- {@link http://www.google.com/recaptcha}
- */
-class ReCaptchaResponse {
-        var $is_valid;
-        var $error;
-}
-
-
-/**
-  * Calls an HTTP POST function to verify if the user's guess was correct
-  * @param string $privkey
-  * @param string $remoteip
-  * @param string $challenge
-  * @param string $response
-  * @return ReCaptchaResponse
-  */
-function recaptcha_check_answer ($privkey, $remoteip, $challenge, $response, $https=false)
-{
-    if ($privkey == null || $privkey == '') {
-               die ("To use reCAPTCHA you must get an API key from <a href='https://www.google.com/recaptcha/admin/create'>https://www.google.com/recaptcha/admin/create</a>");
-    }
-
-    if ($remoteip == null || $remoteip == '') {
-        die ("For security reasons, you must pass the remote ip to reCAPTCHA");
-    }
-
-        //discard spam submissions
-        if ($challenge == null || strlen($challenge) == 0 || $response == null || strlen($response) == 0) {
-                $recaptcha_response = new ReCaptchaResponse();
-                $recaptcha_response->is_valid = false;
-                $recaptcha_response->error = 'incorrect-captcha-sol';
-                return $recaptcha_response;
-        }
-
-        $response = _recaptcha_http_post(RECAPTCHA_VERIFY_SERVER, "/recaptcha/api/verify",
-                                         array (
-                                                'privatekey' => $privkey,
-                                                'remoteip' => $remoteip,
-                                                'challenge' => $challenge,
-                                                'response' => $response
-                                                ),
-                                         $https
-                                        );
-
-        $answers = explode ("\n", $response [1]);
-        $recaptcha_response = new ReCaptchaResponse();
-
-        if (trim ($answers [0]) == 'true') {
-                $recaptcha_response->is_valid = true;
-        }
-        else {
-                $recaptcha_response->is_valid = false;
-                $recaptcha_response->error = $answers [1];
-        }
-        return $recaptcha_response;
-
-}
-
-/**
- * gets a URL where the user can sign up for reCAPTCHA. If your application
- * has a configuration page where you enter a key, you should provide a link
- * using this function.
- * @param string $domain The domain where the page is hosted
- * @param string $appname The name of your application
- */
-function recaptcha_get_signup_url ($domain = null, $appname = null) {
-       return "https://www.google.com/recaptcha/admin/create?" .  _recaptcha_qsencode (array ('domains' => $domain, 'app' => $appname));
-}
-
-function _recaptcha_aes_pad($val) {
-    $block_size = 16;
-    $numpad = $block_size - (strlen ($val) % $block_size);
-    return str_pad($val, strlen ($val) + $numpad, chr($numpad));
-}
-
-/* Mailhide related code */
-
-function _recaptcha_aes_encrypt($val,$ky) {
-    if (! function_exists ("mcrypt_encrypt")) {
-        die ("To use reCAPTCHA Mailhide, you need to have the mcrypt php module installed.");
-    }
-    $mode=MCRYPT_MODE_CBC;
-    $enc=MCRYPT_RIJNDAEL_128;
-    $val=_recaptcha_aes_pad($val);
-    return mcrypt_encrypt($enc, $ky, $val, $mode, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0");
-}
-
-
-function _recaptcha_mailhide_urlbase64 ($x) {
-    return strtr(base64_encode ($x), '+/', '-_');
-}
-
-/* gets the reCAPTCHA Mailhide url for a given email, public key and private key */
-function recaptcha_mailhide_url($pubkey, $privkey, $email) {
-    if ($pubkey == '' || $pubkey == null || $privkey == "" || $privkey == null) {
-        die ("To use reCAPTCHA Mailhide, you have to sign up for a public and private key, " .
-                    "you can do so at <a href='http://www.google.com/recaptcha/mailhide/apikey'>http://www.google.com/recaptcha/mailhide/apikey</a>");
-    }
-
-
-    $ky = pack('H*', $privkey);
-    $cryptmail = _recaptcha_aes_encrypt ($email, $ky);
-
-       return "http://www.google.com/recaptcha/mailhide/d?k=" . $pubkey . "&c=" . _recaptcha_mailhide_urlbase64 ($cryptmail);
-}
-
-/**
- * gets the parts of the email to expose to the user.
- * eg, given johndoe@example,com return ["john", "example.com"].
- * the email is then displayed as john...@example.com
- */
-function _recaptcha_mailhide_email_parts ($email) {
-    $arr = preg_split("/@/", $email );
-
-    if (strlen ($arr[0]) <= 4) {
-        $arr[0] = substr ($arr[0], 0, 1);
-    } else if (strlen ($arr[0]) <= 6) {
-        $arr[0] = substr ($arr[0], 0, 3);
-    } else {
-        $arr[0] = substr ($arr[0], 0, 4);
-    }
-    return $arr;
-}
-
-/**
- * Gets html to display an email address given a public an private key.
- * to get a key, go to:
- *
- * http://www.google.com/recaptcha/mailhide/apikey
- */
-function recaptcha_mailhide_html($pubkey, $privkey, $email) {
-    $emailparts = _recaptcha_mailhide_email_parts ($email);
-    $url = recaptcha_mailhide_url ($pubkey, $privkey, $email);
-
-    return htmlentities($emailparts[0]) . "<a href='" . htmlentities ($url) .
-        "' onclick=\"window.open('" . htmlentities ($url) . "', '', 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=0,width=500,height=300'); return false;\" title=\"Reveal this e-mail address\">...</a>@" . htmlentities ($emailparts [1]);
-
-}
index c7b74ef..5db32ce 100644 (file)
@@ -610,49 +610,24 @@ function get_exception_info($ex) {
 }
 
 /**
- * Generate a uuid.
+ * Generate a V4 UUID.
  *
- * Unique is hard. Very hard. Attempt to use the PECL UUID functions if available, and if not then revert to
+ * Unique is hard. Very hard. Attempt to use the PECL UUID function if available, and if not then revert to
  * constructing the uuid using mt_rand.
  *
  * It is important that this token is not solely based on time as this could lead
  * to duplicates in a clustered environment (especially on VMs due to poor time precision).
  *
+ * @see https://tools.ietf.org/html/rfc4122
+ *
+ * @deprecated since Moodle 3.8 MDL-61038 - please do not use this function any more.
+ * @see \core\uuid::generate()
+ *
  * @return string The uuid.
  */
 function generate_uuid() {
-    $uuid = '';
-
-    if (function_exists("uuid_create")) {
-        $context = null;
-        uuid_create($context);
-
-        uuid_make($context, UUID_MAKE_V4);
-        uuid_export($context, UUID_FMT_STR, $uuid);
-    } else {
-        // Fallback uuid generation based on:
-        // "http://www.php.net/manual/en/function.uniqid.php#94959".
-        $uuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
-
-            // 32 bits for "time_low".
-            mt_rand(0, 0xffff), mt_rand(0, 0xffff),
-
-            // 16 bits for "time_mid".
-            mt_rand(0, 0xffff),
-
-            // 16 bits for "time_hi_and_version",
-            // four most significant bits holds version number 4.
-            mt_rand(0, 0x0fff) | 0x4000,
-
-            // 16 bits, 8 bits for "clk_seq_hi_res",
-            // 8 bits for "clk_seq_low",
-            // two most significant bits holds zero and one for variant DCE1.1.
-            mt_rand(0, 0x3fff) | 0x8000,
-
-            // 48 bits for "node".
-            mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff));
-    }
-    return trim($uuid);
+    debugging('generate_uuid() is deprecated. Please use \core\uuid::generate() instead.', DEBUG_DEVELOPER);
+    return \core\uuid::generate();
 }
 
 /**
@@ -1513,7 +1488,7 @@ function make_unique_writable_directory($basedir, $exceptiononerror = true) {
 
     do {
         // Generate a new (hopefully unique) directory name.
-        $uniquedir = $basedir . DIRECTORY_SEPARATOR . generate_uuid();
+        $uniquedir = $basedir . DIRECTORY_SEPARATOR . \core\uuid::generate();
     } while (
             // Ensure that basedir is still writable - if we do not check, we could get stuck in a loop here.
             is_writable($basedir) &&
index 314ae0c..cda6868 100644 (file)
@@ -253,7 +253,10 @@ EOD;
         }
 
         $record['timemodified'] = $record['timecreated'];
-        $record['lastip'] = '0.0.0.0';
+
+        if (!isset($record['lastip'])) {
+            $record['lastip'] = '0.0.0.0';
+        }
 
         if ($record['deleted']) {
             $delname = $record['email'].'.'.time();
index e87f6c4..e468ae1 100644 (file)
@@ -213,6 +213,16 @@ class behat_data_generators extends behat_base {
             'required' => array('user', 'group', 'message'),
             'switchids' => array('user' => 'userid', 'group' => 'groupid')
         ),
+        'muted group conversations' => array(
+            'datagenerator' => 'mute_group_conversations',
+            'required' => array('user', 'group', 'course'),
+            'switchids' => array('user' => 'userid', 'group' => 'groupid', 'course' => 'courseid')
+        ),
+        'muted private conversations' => array(
+            'datagenerator' => 'mute_private_conversations',
+            'required' => array('user', 'contact'),
+            'switchids' => array('user' => 'userid', 'contact' => 'contactid')
+        ),
         'language customisations' => array(
             'datagenerator' => 'customlang',
             'required' => array('component', 'stringid', 'value'),
@@ -1032,4 +1042,42 @@ class behat_data_generators extends behat_base {
         }
         \core_message\api::set_favourite_conversation($conversationid, $data['userid']);
     }
+
+    /**
+     * Mute an existing group conversation for user
+     *
+     * @param array $data
+     * @return void
+     */
+    protected function process_mute_group_conversations(array $data) {
+        if (groups_is_member($data['groupid'], $data['userid'])) {
+            $context = context_course::instance($data['courseid']);
+            $conversation = \core_message\api::get_conversation_by_area(
+                'core_group',
+                'groups',
+                $data['groupid'],
+                $context->id
+            );
+            if ($conversation) {
+                \core_message\api::mute_conversation($data['userid'], $conversation->id);
+            }
+        }
+    }
+
+    /**
+     * Mute a private conversation for user
+     *
+     * @param array $data
+     * @return void
+     */
+    protected function process_mute_private_conversations(array $data) {
+        if (!$conversationid = \core_message\api::get_conversation_between_users([$data['userid'], $data['contactid']])) {
+            $conversation = \core_message\api::create_conversation(
+                \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+                [$data['userid'], $data['contactid']]
+            );
+            $conversationid = $conversation->id;
+        }
+        \core_message\api::mute_conversation($data['userid'], $conversationid);
+    }
 }
index e38124e..6e3ce5b 100644 (file)
@@ -25,7 +25,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-
 /**
  * Unit tests for setuplib.php
  *
@@ -476,4 +475,65 @@ class core_setuplib_testcase extends advanced_testcase {
     public function test_get_real_size($input, $expectedbytes) {
         $this->assertEquals($expectedbytes, get_real_size($input));
     }
+
+    /**
+     * Validate the given V4 UUID.
+     *
+     * @param string $value The candidate V4 UUID
+     * @return bool True if valid; otherwise, false.
+     */
+    protected static function is_valid_uuid_v4($value) {
+        // Version 4 UUIDs have the form xxxxxxxx-xxxx-4xxx-Yxxx-xxxxxxxxxxxx
+        // where x is any hexadecimal digit and Y is one of 8, 9, aA, or bB.
+        // First, the size is 36 (32 + 4 dashes).
+        if (strlen($value) != 36) {
+            return false;
+        }
+        // Finally, check the format.
+        $uuidv4pattern = '/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i';
+        return (preg_match($uuidv4pattern, $value) === 1);
+    }
+
+    /**
+     * Test the \core\uuid::generate_uuid_via_pecl_uuid_extension() function.
+     */
+    public function test_core_uuid_generate_uuid_via_pecl_uuid_extension() {
+        if (!extension_loaded('uuid')) {
+            $this->markTestSkipped("PHP 'uuid' extension not loaded.");
+        }
+        if (!function_exists('uuid_time')) {
+            $this->markTestSkipped("PHP PECL 'uuid' extension not loaded.");
+        }
+
+        // The \core\uuid::generate_uuid_via_pecl_uuid_extension static method is protected. Use Reflection to call the method.
+        $method = new ReflectionMethod('\core\uuid', 'generate_uuid_via_pecl_uuid_extension');
+        $method->setAccessible(true);
+        $uuid = $method->invoke(null);
+        $this->assertTrue(self::is_valid_uuid_v4($uuid), "Invalid v4 uuid: '$uuid'");
+    }
+
+    /**
+     * Test the \core\uuid::generate_uuid_via_random_bytes() function.
+     */
+    public function test_core_uuid_generate_uuid_via_random_bytes() {
+        try {
+            random_bytes(1);
+        } catch (Exception $e) {
+            $this->markTestSkipped('No source of entropy for random_bytes. ' . $e->getMessage());
+        }
+
+        // The \core\uuid::generate_uuid_via_random_bytes static method is protected. Use Reflection to call the method.
+        $method = new ReflectionMethod('\core\uuid', 'generate_uuid_via_random_bytes');
+        $method->setAccessible(true);
+        $uuid = $method->invoke(null);
+        $this->assertTrue(self::is_valid_uuid_v4($uuid), "Invalid v4 uuid: '$uuid'");
+    }
+
+    /**
+     * Test the \core\uuid::generate() function.
+     */
+    public function test_core_uuid_generate() {
+        $uuid = \core\uuid::generate();
+        $this->assertTrue(self::is_valid_uuid_v4($uuid), "Invalid v4 UUID: '$uuid'");
+    }
 }
index 85d046b..ed721af 100644 (file)
     <version>1.1.0</version>
     <licenseversion></licenseversion>
   </library>
-  <library>
-    <location>pear/Auth/RADIUS.php</location>
-    <name>Pear_Auth_Radius</name>
-    <license>BSD</license>
-    <version>1.1.0</version>
-    <licenseversion></licenseversion>
-  </library>
   <library>
     <location>pear/Crypt/CHAP.php</location>
     <name>Pear_Crypt_CHAP</name>
     <version>1.7.0</version>
     <licenseversion></licenseversion>
   </library>
-  <library>
-    <location>recaptchalib.php</location>
-    <name>ReCAPTCHA</name>
-    <license>MIT</license>
-    <version>1.10</version>
-    <licenseversion></licenseversion>
-  </library>
   <library>
     <location>xhprof</location>
     <name>XHProf</name>
index 30dbd05..1f59d6f 100644 (file)
@@ -1,6 +1,11 @@
 This files describes API changes in core libraries and APIs,
 information provided here is intended especially for developers.
 
+=== 3.8 ===
+* The yui checknet module is removed. Call \core\session\manager::keepalive instead.
+* The generate_uuid() function has been deprecated. Please use \core\uuid::generate() instead.
+* Remove lib/pear/auth/RADIUS.php (MDL-65746)
+
 === 3.7 ===
 * Nodes in the navigation api can have labels for each group. See set/get_collectionlabel().
 * The method core_user::is_real_user() now returns false for userid = 0 parameter
diff --git a/lib/yui/build/moodle-core-checknet/assets/checknet.txt b/lib/yui/build/moodle-core-checknet/assets/checknet.txt
deleted file mode 100644 (file)
index d00491f..0000000
Binary files a/lib/yui/build/moodle-core-checknet/assets/checknet.txt and /dev/null differ
diff --git a/lib/yui/build/moodle-core-checknet/moodle-core-checknet-debug.js b/lib/yui/build/moodle-core-checknet/moodle-core-checknet-debug.js
deleted file mode 100644 (file)
index 3ee6b81..0000000
Binary files a/lib/yui/build/moodle-core-checknet/moodle-core-checknet-debug.js and /dev/null differ
diff --git a/lib/yui/build/moodle-core-checknet/moodle-core-checknet-min.js b/lib/yui/build/moodle-core-checknet/moodle-core-checknet-min.js
deleted file mode 100644 (file)
index 0fe879b..0000000
Binary files a/lib/yui/build/moodle-core-checknet/moodle-core-checknet-min.js and /dev/null differ
diff --git a/lib/yui/build/moodle-core-checknet/moodle-core-checknet.js b/lib/yui/build/moodle-core-checknet/moodle-core-checknet.js
deleted file mode 100644 (file)
index 52d5b54..0000000
Binary files a/lib/yui/build/moodle-core-checknet/moodle-core-checknet.js and /dev/null differ
diff --git a/lib/yui/src/checknet/assets/checknet.txt b/lib/yui/src/checknet/assets/checknet.txt
deleted file mode 100644 (file)
index d00491f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-1
diff --git a/lib/yui/src/checknet/build.json b/lib/yui/src/checknet/build.json
deleted file mode 100644 (file)
index 406832d..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-    "name": "moodle-core-checknet",
-    "builds": {
-        "moodle-core-checknet": {
-            "jsfiles": [
-                "checknet.js"
-            ]
-        }
-    }
-}
diff --git a/lib/yui/src/checknet/js/checknet.js b/lib/yui/src/checknet/js/checknet.js
deleted file mode 100644 (file)
index 595620e..0000000
+++ /dev/null
@@ -1,215 +0,0 @@
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * A utility to check whether the connection to the Moodle server is still
- * active.
- *
- * @module     moodle-core-checknet
- * @package    core
- * @copyright  2014 Andrew Nicols <andrew@nicols.co.uk>
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- * @main       moodle-core-checknet
- */
-
-/**
- * @namespace M.core
- * @class checknet
- */
-
-function CheckNet() {
-    CheckNet.superclass.constructor.apply(this, arguments);
-}
-
-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.
-     *
-     * @property _alertDialogue
-     * @type M.core.dialogue
-     * @private
-     * @default null
-     */
-    _alertDialogue: null,
-
-    /**
-     * Setup the checking mechanism.
-     *
-     * @method initializer
-     */
-    initializer: function() {
-        // Perform our first check.
-        this._scheduleCheck();
-    },
-
-    /**
-     * Schedule a check of the checknet file.
-     *
-     * @method _scheduleCheck
-     * @chainable
-     * @private
-     */
-    _scheduleCheck: function() {
-        // Schedule the next check after five seconds.
-        Y.later(this.get('frequency'), this, this._performCheck);
-        return this;
-    },
-
-    /**
-     * Perform an immediate check of the checknet file.
-     *
-     * @method _performCheck
-     * @private
-     */
-    _performCheck: function() {
-        Y.io(this.get('uri'), {
-            data: {
-                // Add the session key.
-                sesskey: M.cfg.sesskey,
-                // Add a query string to prevent older versions of IE from using the cache.
-                time: new Date().getTime()
-            },
-            timeout: this.get('timeout'),
-            headers: {
-                'Cache-Control': 'no-cache',
-                'Expires': '-1'
-            },
-            context: this,
-            on: {
-                complete: function(tid, response) {
-                    // Check for failure conditions.
-                    // We check for a valid status here because if the user is moving away from the page at the time we
-                    // run this callback we do not want to display the error.
-                    if (response && typeof response.status !== "undefined") {
-                        var code = parseInt(response.status, 10);
-
-                        if (code === 200) {
-                            // This is a valid attempt - clear any existing warning dialogue and destroy it.
-                            if (this._alertDialogue) {
-                                this._alertDialogue.destroy();
-                                this._alertDialogue = null;
-                            }
-                        } else if (code >= 300 && code <= 399) {
-                            // This is a cached status - warn developers, but otherwise ignore.
-                            Y.log("A cached copy of the checknet status file was returned so it's reliablity cannot be guaranteed",
-                                'warn',
-                                'moodle-mod_scorm-checknet');
-                        } else {
-                            if (this._alertDialogue === null || this._alertDialogue.get('destroyed')) {
-                                // Only create a new dialogue if it isn't already displayed.
-                                this._alertDialogue = new M.core.alert({
-                                    message: M.util.get_string.apply(this, this.get('message'))
-                                });
-                            } else {
-                                this._alertDialogue.show();
-                            }
-                            this._alertCount++;
-                        }
-                    }
-                    // 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();
-                    }
-                }
-            }
-        });
-    }
-}, {
-    NAME: 'checkNet',
-    ATTRS: {
-        /**
-         * The file to check access against.
-         *
-         * @attribute uri
-         * @type String
-         * @default M.cfg.wwwroot + '/lib/yui/build/moodle-core-checknet/assets/checknet.txt'
-         */
-        uri: {
-            value: M.cfg.wwwroot + '/lib/yui/build/moodle-core-checknet/assets/checknet.txt'
-        },
-
-        /**
-         * The timeout (in milliseconds) before the checker should give up and display a warning.
-         *
-         * @attribute timeout
-         * @type Number
-         * @value 4000
-         */
-        timeout: {
-            value: 4000
-        },
-
-        /**
-         * The frequency (in milliseconds) that checks should be run.
-         * A new check is not begun until the previous check has completed.
-         *
-         * @attribute frequency
-         * @writeOnce
-         * @type Number
-         * @value 10000
-         */
-        frequency: {
-            value: 10000
-        },
-
-        /**
-         * The message which should be displayed upon a test failure.
-         *
-         * The array values are passed directly to M.util.get_string() and arguments should match accordingly.
-         *
-         * @attribute message
-         * @type Array
-         * @value [
-         *  'networkdropped',
-         *  'moodle'
-         * ]
-         */
-        message: {
-            value: [
-                '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
-        }
-    }
-});
-
-M.core = M.core || {};
-M.core.checknet = M.core.checknet || {};
-M.core.checknet.init = function(config) {
-    return new CheckNet(config);
-};
diff --git a/lib/yui/src/checknet/meta/checknet.json b/lib/yui/src/checknet/meta/checknet.json
deleted file mode 100644 (file)
index a63efb0..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-{
-    "moodle-core-checknet": {
-        "requires": [
-            "base-base",
-            "moodle-core-notification-alert",
-            "io-base"
-        ]
-    }
-}
index a47abea..661c571 100644 (file)
@@ -2386,10 +2386,14 @@ class api {
     public static function get_conversation_between_users(array $userids) {
         global $DB;
 
-        $conversations = self::get_individual_conversations_between_users([$userids]);
-        $conversation = $conversations[0];
+        if (empty($userids)) {
+            return false;
+        }
+
+        $hash = helper::get_conversation_hash($userids);
 
-        if ($conversation) {
+        if ($conversation = $DB->get_record('message_conversations', ['type' => self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+                'convhash' => $hash])) {
             return $conversation->id;
         }
 
@@ -2415,12 +2419,16 @@ class api {
      *
      * Where null is returned for the pairing of [3, 4] since no record exists.
      *
+     * @deprecated since 3.8
      * @param array $useridsets An array of arrays where the inner array is the set of user ids
      * @return stdClass[] Array of conversation records
      */
     public static function get_individual_conversations_between_users(array $useridsets) : array {
         global $DB;
 
+        debugging('\core_message\api::get_individual_conversations_between_users is deprecated and no longer used',
+            DEBUG_DEVELOPER);
+
         if (empty($useridsets)) {
             return [];
         }
index 9493f9e..83dd501 100644 (file)
@@ -487,15 +487,15 @@ function get_message_output_default_preferences() {
 function translate_message_default_setting($plugindefault, $processorname) {
     // Preset translation arrays
     $permittedvalues = array(
-        0x04 => 'disallowed',
-        0x08 => 'permitted',
-        0x0c => 'forced',
+        MESSAGE_DISALLOWED => 'disallowed',
+        MESSAGE_PERMITTED  => 'permitted',
+        MESSAGE_FORCED     => 'forced',
     );
 
     $loggedinstatusvalues = array(
         0x00 => null, // use null if loggedin/loggedoff is not defined
-        0x01 => 'loggedin',
-        0x02 => 'loggedoff',
+        MESSAGE_DEFAULT_LOGGEDIN  => 'loggedin',
+        MESSAGE_DEFAULT_LOGGEDOFF => 'loggedoff',
     );
 
     // define the default setting
index bc63342..5d13640 100644 (file)
@@ -5749,6 +5749,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
      */
     public function test_get_individual_conversations_between_users_no_user_sets() {
         $this->assertEmpty(\core_message\api::get_individual_conversations_between_users([]));
+        $this->assertDebuggingCalled();
     }
 
     /**
@@ -5763,6 +5764,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
             [null],
             \core_message\api::get_individual_conversations_between_users([[$user1->id, $user2->id]])
         );
+        $this->assertDebuggingCalled();
     }
 
     /**
@@ -5783,6 +5785,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
             [$user2->id, $user3->id],
             [$user1->id, $user3->id]
         ]);
+        $this->assertDebuggingCalled();
 
         $result = array_map(function($result) {
             if ($result) {
@@ -5817,6 +5820,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
             [$user2->id, $user3->id],
             [$user1->id, $user3->id]
         ]);
+        $this->assertDebuggingCalled();
 
         $result = array_map(function($result) {
             if ($result) {
@@ -5851,6 +5855,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
             [$user2->id, $user3->id],
             [$user1->id, $user3->id]
         ]);
+        $this->assertDebuggingCalled();
 
         $result = array_map(function($result) {
             if ($result) {
@@ -5870,6 +5875,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
             [$user1->id, $user2->id],
             [$user1->id, $user3->id]
         ]);
+        $this->assertDebuggingCalled();
 
         $result = array_map(function($result) {
             if ($result) {
diff --git a/message/tests/behat/mute_conversations.feature b/message/tests/behat/mute_conversations.feature
new file mode 100644 (file)
index 0000000..4a40e60
--- /dev/null
@@ -0,0 +1,94 @@
+@core @core_message @javascript
+Feature: Mute and unmute conversations
+  In order to manage my conversations
+  As a user
+  I need to be able to mute and unmute conversations
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1        | 0        | 1         |
+    And the following "users" exist:
+      | username | firstname | lastname | email                |
+      | student1 | Student   | 1        | student1@example.com |
+      | student2 | Student   | 2        | student2@example.com |
+    And the following "course enrolments" exist:
+      | user     | course | role |
+      | student1 | C1     | student |
+      | student2 | C1     | student |
+    And the following "groups" exist:
+      | name    | course | idnumber | enablemessaging |
+      | Group 1 | C1     | G1       | 1               |
+    And the following "group members" exist:
+      | user     | group | course |
+      | student1 | G1    | C1     |
+      | student2 | G1    | C1     |
+    And the following config values are set as admin:
+      | messaging | 1 |
+    And the following "private messages" exist:
+      | user     | contact  | message |
+      | student1 | student2 | Hi!     |
+
+  Scenario: Mute a group conversation
+    Given I log in as "student1"
+    When I open messaging
+    And I open the "Group" conversations list
+    Then "Group 1" "group_message" should exist
+    And "muted" "icon_container" in the "Group 1" "group_message" should not be visible
+    And I select "Group 1" conversation in messaging
+    And "muted" "icon_container" in the "Group 1" "group_message_header" should not be visible
+    And I open contact menu
+    And I click on "Mute" "link" in the "[data-region='header-container']" "css_element"
+    And "muted" "icon_container" in the "Group 1" "group_message_header" should be visible
+    And I go back in "view-conversation" message drawer
+    And "muted" "icon_container" in the "Group 1" "group_message" should be visible
+
+  Scenario: Mute a private conversation
+    When I log in as "student1"
+    And I open messaging
+    Then I should see "Private"
+    And I open the "Private" conversations list
+    And I should see "Student 2"
+    And "muted" "icon_container" in the "Student 2" "group_message" should not be visible
+    And I select "Student 2" conversation in messaging
+    And "muted" "icon_container" in the "[data-action='view-contact']" "css_element" should not be visible
+    And I open contact menu
+    And I click on "Mute" "link" in the "[data-region='header-container']" "css_element"
+    And "muted" "icon_container" in the "[data-action='view-contact']" "css_element" should be visible
+    And I go back in "view-conversation" message drawer
+    And "muted" "icon_container" in the "Student 2" "group_message" should be visible
+
+  Scenario: Unmute a group conversation
+    Given the following "muted group conversations" exist:
+      | user     | group | course |
+      | student1 | G1    | C1     |
+    When I log in as "student1"
+    And I open messaging
+    And I open the "Group" conversations list
+    Then "Group 1" "group_message" should exist
+    And "muted" "icon_container" in the "Group 1" "group_message" should be visible
+    And I select "Group 1" conversation in messaging
+    And "muted" "icon_container" in the "Group 1" "group_message_header" should be visible
+    And I open contact menu
+    And I click on "Unmute" "link" in the "[data-region='header-container']" "css_element"
+    And "muted" "icon_container" in the "Group 1" "group_message_header" should not be visible
+    And I go back in "view-conversation" message drawer
+    And "muted" "icon_container" in the "Group 1" "group_message" should not be visible
+
+  Scenario: Unmute a private conversation
+    Given the following "muted private conversations" exist:
+      | user     | contact  |
+      | student1 | student2 |
+    When I log in as "student1"
+    And I open messaging
+    Then I should see "Private"
+    And I open the "Private" conversations list
+    And I should see "Student 2"
+    And "muted" "icon_container" in the "Student 2" "group_message" should be visible
+    And I select "Student 2" conversation in messaging
+    And "muted" "icon_container" in the "[data-action='view-contact']" "css_element" should be visible
+    And I open contact menu
+    And I click on "Unmute" "link" in the "[data-region='header-container']" "css_element"
+    And "muted" "icon_container" in the "[data-action='view-contact']" "css_element" should not be visible
+    And I go back in "view-conversation" message drawer
+    And "muted" "icon_container" in the "Student 2" "group_message" should not be visible
index b0142f1..041fa20 100644 (file)
@@ -1,6 +1,11 @@
 This files describes API changes in /message/ messaging system,
 information provided here is intended especially for developers.
 
+=== 3.8 ===
+
+* The following methods have been deprecated and should not be used any more:
+  - \core_message\api::get_individual_conversations_between_users()
+
 === 3.7 ===
 
 * The message/index.php page used to support viewing another user's messages (if you had the right capabilities) by
index 48d87f8..573fab2 100644 (file)
@@ -358,6 +358,7 @@ ul.assignfeedback_editpdf_menu {
 
 .assignfeedback_editpdf_widget .commentdrawable.commentcollapsed {
     z-index: auto;
+    width: 24px;
 }
 
 .assignfeedback_editpdf_widget .commentdrawable.commentcollapsed textarea,
index 433d205..2e6840e 100644 (file)
@@ -80,7 +80,7 @@ class assignfeedback_file_import_zip_form extends moodleform implements renderab
             if ($importer->is_valid_filename_for_import($assignment, $unzippedfile, $participants, $user, $plugin, $filename)) {
                 if ($importer->is_file_modified($assignment, $user, $plugin, $filename, $unzippedfile)) {
                     // Get a string we can show to identify this user.
-                    $userdesc = fullname($user);
+                    $userdesc = fullname($user, has_capability('moodle/site:viewfullnames', $assignment->get_context()));
                     $path = pathinfo($filename);
                     if ($assignment->is_blind_marking()) {
                         $userdesc = get_string('hiddenuser', 'assign') .
index a9f0174..bef787c 100644 (file)
@@ -3517,7 +3517,8 @@ class assign {
                     $prefix = str_replace('_', ' ', $groupname . get_string('participant', 'assign'));
                     $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($userid));
                 } else {
-                    $prefix = str_replace('_', ' ', $groupname . fullname($student));
+                    $fullname = fullname($student, has_capability('moodle/site:viewfullnames', $this->get_context()));
+                    $prefix = str_replace('_', ' ', $groupname . $fullname);
                     $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($userid));
                 }
 
diff --git a/mod/book/tests/behat/display_book_description.feature b/mod/book/tests/behat/display_book_description.feature
new file mode 100644 (file)
index 0000000..08f977b
--- /dev/null
@@ -0,0 +1,54 @@
+@mod @mod_book
+Feature: Display the book description in the book and optionally in the course
+  In order to display the the book description in the course
+  As a teacher
+  I need to enable the 'Display description on course page' setting.
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1 | topics |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add a "Book" to section "1" and I fill the form with:
+      | Name | Test book |
+      | Description | A book about dreams! |
+    And I follow "Test book"
+    And I should see "Add new chapter"
+    And I set the following fields to these values:
+      | Chapter title | Dummy first chapter |
+      | Content | Dream is the start of a journey |
+    And I press "Save changes"
+
+  Scenario: Description is displayed in the book
+    Given I am on "Course 1" course homepage
+    When I follow "Test book"
+    Then I should see "A book about dreams!"
+
+  Scenario: Show book description in the course homepage
+    Given I am on "Course 1" course homepage
+    And I follow "Test book"
+    And I navigate to "Edit settings" in current page administration
+    And the following fields match these values:
+      | Display description on course page | |
+    And I set the following fields to these values:
+      | Display description on course page | 1 |
+    And I press "Save and return to course"
+    When I am on "Course 1" course homepage
+    Then I should see "A book about dreams!"
+
+  Scenario: Hide book description in the course homepage
+    Given I am on "Course 1" course homepage
+    And I follow "Test book"
+    And I navigate to "Edit settings" in current page administration
+    And the following fields match these values:
+      | Display description on course page | |
+    And I press "Save and return to course"
+    When I am on "Course 1" course homepage
+    Then I should not see "A book about dreams!"
index f0ad1b9..2b2c2d1 100644 (file)
@@ -213,6 +213,11 @@ book_view($book, $chapter, $islastchapter, $course, $cm, $context);
 echo $OUTPUT->header();
 echo $OUTPUT->heading(format_string($book->name));
 
+// Info box.
+if ($book->intro) {
+    echo $OUTPUT->box(format_module_intro('book', $book, $cm->id), 'generalbox', 'intro');
+}
+
 $navclasses = book_get_nav_classes();
 
 if ($book->navstyle) {
index df43413..b888a7f 100644 (file)
Binary files a/mod/forum/amd/build/posts_list.min.js and b/mod/forum/amd/build/posts_list.min.js differ
index 5e24d7a..3fbc593 100644 (file)
@@ -59,7 +59,7 @@ define([
                 postid: $(currentRoot).data('post-id'),
                 "reply_url": $(e.currentTarget).attr('href'),
                 sesskey: M.cfg.sesskey,
-                parentsubject: currentSubject.html(),
+                parentsubject: currentSubject.data('replySubject'),
                 canreplyprivately: $(e.currentTarget).data('can-reply-privately'),
                 postformat: InPageReply.CONTENT_FORMATS.MOODLE
             };
index 7ac4822..7beb5d5 100644 (file)
@@ -140,15 +140,26 @@ class author extends exporter {
         $author = $this->author;
         $authorcontextid = $this->authorcontextid;
         $urlfactory = $this->related['urlfactory'];
+        $context = $this->related['context'];
 
         if ($this->canview) {
-            $groups = array_map(function($group) {
-                $imageurl = get_group_picture_url($group, $group->courseid);
+            $groups = array_map(function($group) use ($urlfactory, $context) {
+                $imageurl = null;
+                $groupurl = null;
+                if (!$group->hidepicture) {
+                    $imageurl = get_group_picture_url($group, $group->courseid, true);
+                }
+                if (course_can_view_participants($context)) {
+                    $groupurl = $urlfactory->get_author_group_url($group);
+                }
+
                 return [
                     'id' => $group->id,
                     'name' => $group->name,
                     'urls' => [
-                        'image' => $imageurl ? $imageurl->out(false) : null
+                        'image' => $imageurl ? $imageurl->out(false) : null,
+                        'group' => $groupurl ? $groupurl->out(false) : null
+
                     ]
                 ];
             }, $this->authorgroups);
index 16d682a..a008de4 100644 (file)
@@ -179,7 +179,7 @@ class discussion extends exporter {
                 ];
 
                 if (!$group->hidepicture) {
-                    $url = get_group_picture_url($group, $forum->get_course_id());
+                    $url = get_group_picture_url($group, $forum->get_course_id(), true);
                     if (!empty($url)) {
                         $groupdata['urls']['picture'] = $url;
                     }
index 40d4dd6..ad39615 100644 (file)
@@ -119,6 +119,7 @@ class forum extends exporter {
             'capabilities' => [
                 'viewdiscussions' => $capabilitymanager->can_view_discussions($user),
                 'create' => $capabilitymanager->can_create_discussions($user, $currentgroup),
+                'selfenrol' => $capabilitymanager->can_self_enrol($user),
                 'subscribe' => $capabilitymanager->can_subscribe_to_forum($user),
             ],
             'urls' => [
index 4528c60..0d23672 100644 (file)
@@ -92,6 +92,7 @@ class post extends exporter {
         return [
             'id' => ['type' => PARAM_INT],
             'subject' => ['type' => PARAM_TEXT],
+            'replysubject' => ['type' => PARAM_TEXT],
             'message' => ['type' => PARAM_RAW],
             'messageformat' => ['type' => PARAM_INT],
             'author' => ['type' => author_exporter::read_properties_definition()],
@@ -146,6 +147,11 @@ class post extends exporter {
                         'null' => NULL_ALLOWED,
                         'description' => 'Whether the user can reply to the post',
                     ],
+                    'selfenrol' => [
+                        'type' => PARAM_BOOL,
+                        'null' => NULL_ALLOWED,
+                        'description' => 'Whether the user can self enrol into the course',
+                    ],
                     'export' => [
                         'type' => PARAM_BOOL,
                         'null' => NULL_ALLOWED,
@@ -360,6 +366,7 @@ class post extends exporter {
         $canreply = $capabilitymanager->can_reply_to_post($user, $discussion, $post);
         $canexport = $capabilitymanager->can_export_post($user, $post);
         $cancontrolreadstatus = $capabilitymanager->can_manually_control_post_read_status($user);
+        $canselfenrol = $capabilitymanager->can_self_enrol($user);
         $canreplyprivately = $capabilitymanager->can_reply_privately_to_post($user, $post);
 
         $urlfactory = $this->related['urlfactory'];
@@ -369,7 +376,7 @@ class post extends exporter {
         $editurl = $canedit ? $urlfactory->get_edit_post_url_from_post($forum, $post) : null;
         $deleteurl = $candelete ? $urlfactory->get_delete_post_url_from_post($post) : null;
         $spliturl = $cansplit ? $urlfactory->get_split_discussion_at_post_url_from_post($post) : null;
-        $replyurl = $canreply ? $urlfactory->get_reply_to_post_url_from_post($post) : null;
+        $replyurl = $canreply || $canselfenrol ? $urlfactory->get_reply_to_post_url_from_post($post) : null;
         $exporturl = $canexport ? $urlfactory->get_export_post_url_from_post($post) : null;
         $markasreadurl = $cancontrolreadstatus ? $urlfactory->get_mark_post_as_read_url_from_post($post) : null;
         $markasunreadurl = $cancontrolreadstatus ? $urlfactory->get_mark_post_as_unread_url_from_post($post) : null;
@@ -401,9 +408,16 @@ class post extends exporter {
             }
         }
 
+        $replysubject = $subject;
+        $strre = get_string('re', 'forum');
+        if (!(substr($replysubject, 0, strlen($strre)) == $strre)) {
+            $replysubject = "{$strre} {$replysubject}";
+        }
+
         return [
             'id' => $post->get_id(),
             'subject' => $subject,
+            'replysubject' => $replysubject,
             'message' => $message,
             'messageformat' => $post->get_message_format(),
             'author' => $exportedauthor,
@@ -424,7 +438,8 @@ class post extends exporter {
                 'reply' => $canreply,
                 'export' => $canexport,
                 'controlreadstatus' => $cancontrolreadstatus,
-                'canreplyprivately' => $canreplyprivately
+                'canreplyprivately' => $canreplyprivately,
+                'selfenrol' => $canselfenrol
             ],
             'urls' => [
                 'view' => $viewurl ? $viewurl->out(false) : null,
index ab0fc75..cfb6b4b 100644 (file)
@@ -427,6 +427,18 @@ class url {
         return $userpicture->get_url($PAGE);
     }
 
+    /**
+     * Get the url to view an author's group.
+     *
+     * @param \stdClass $group The group
+     * @return moodle_url
+     */
+    public function get_author_group_url(\stdClass $group) : moodle_url {
+        return new moodle_url('/user/index.php', [
+                'id' => $group->courseid,
+                'group' => $group->id
+        ]);
+    }
     /**
      * Get the url to mark a discussion as read.
      *
index 76ba7c1..ece049e 100644 (file)
@@ -607,4 +607,30 @@ class capability {
     public function can_manage_tags(stdClass $user) : bool {
         return has_capability('moodle/tag:manage', context_system::instance(), $user);
     }
+
+    /**
+     * Checks whether the user can self enrol into the course.
+     * Mimics the checks on the add button in deprecatedlib/forum_print_latest_discussions
+     *
+     * @param stdClass $user
+     * @return bool
+     */
+    public function can_self_enrol(stdClass $user) : bool {
+        $canstart = false;
+
+        if ($this->forum->get_type() != 'news') {
+            if (isguestuser($user) or !isloggedin()) {
+                $canstart = true;
+            }
+
+            if (!is_enrolled($this->context) and !is_viewing($this->context)) {
+                 // Allow guests and not-logged-in to see the button - they are prompted to log in after clicking the link,
+                 // Normal users with temporary guest access see this button too, they are asked to enrol instead,
+                 // Do not show the button to users with suspended enrolments here.
+                $canstart = enrol_selfenrol_available($this->forum->get_course_id());
+            }
+        }
+
+        return $canstart;
+    }
 }
index 084122a..7559b2b 100644 (file)
@@ -183,9 +183,15 @@ class discussion {
             $exporteddiscussion = $this->get_exported_discussion($user);
         }
 
+        $hasanyactions = false;
+        $hasanyactions = $hasanyactions || $capabilitymanager->can_favourite_discussion($user);
+        $hasanyactions = $hasanyactions || $capabilitymanager->can_pin_discussions($user);
+        $hasanyactions = $hasanyactions || $capabilitymanager->can_manage_forum($user);
+
         $exporteddiscussion = array_merge($exporteddiscussion, [
             'notifications' => $this->get_notifications($user),
             'html' => [
+                'hasanyactions' => $hasanyactions,
                 'posts' => $this->postsrenderer->render($user, [$this->forum], [$this->discussion], $posts),
                 'modeselectorform' => $this->get_display_mode_selector_html($displaymode),
                 'subscribe' => null,
index ef136a3..24879b1 100644 (file)
@@ -160,8 +160,15 @@ class discussion_list {
         // Get all forum discussion summaries.
         $discussions = mod_forum_get_discussion_summaries($forum, $user, $groupid, $sortorder, $pageno, $pagesize);
 
+        $capabilitymanager = $this->capabilitymanager;
+        $hasanyactions = false;
+        $hasanyactions = $hasanyactions || $capabilitymanager->can_favourite_discussion($user);
+        $hasanyactions = $hasanyactions || $capabilitymanager->can_pin_discussions($user);
+        $hasanyactions = $hasanyactions || $capabilitymanager->can_manage_forum($user);
+
         $forumview = [
             'forum' => (array) $forumexporter->export($this->renderer),
+            'hasanyactions' => $hasanyactions,
             'groupchangemenu' => groups_print_activity_menu(
                 $cm,
                 $this->urlfactory->get_forum_view_url_from_forum($forum),
diff --git a/mod/forum/lang/en/deprecated.txt b/mod/forum/lang/en/deprecated.txt
new file mode 100644 (file)
index 0000000..dd59859
--- /dev/null
@@ -0,0 +1 @@
+inpagereplysubject,mod_forum
index 12b7824..89a544f 100644 (file)
@@ -323,7 +323,6 @@ $string['invalidforcesubscribe'] = 'Invalid force subscription mode';
 $string['invalidforumid'] = 'Forum ID was incorrect';
 $string['invalidparentpostid'] = 'Parent post ID was incorrect';
 $string['invalidpostid'] = 'Invalid post ID - {$a}';
-$string['inpagereplysubject'] = 'Re: {$a}';
 $string['lastpost'] = 'Last post';
 $string['learningforums'] = 'Learning forums';
 $string['lockdiscussionafter'] = 'Lock discussions after period of inactivity';
@@ -673,3 +672,6 @@ $string['yournewtopic'] = 'Your new discussion topic';
 $string['yourreply'] = 'Your reply';
 $string['forumsubjectdeleted'] = 'This forum post has been removed';
 $string['forumbodydeleted'] = 'The content of this forum post has been removed and can no longer be accessed.';
+
+// Deprecated since Moodle 3.8.
+$string['inpagereplysubject'] = 'Re: {$a}';
index c7f31dd..6343e1d 100644 (file)
     margin: 5px 0;
 }
 
-.discussion-list .userpicture {
+.discussion-list .userpicture,
+.discussion-list .grouppicture {
     height: 35px;
     width: 35px;
 }
index 60e3704..93632b4 100644 (file)
             </div>
         </div>
     {{/forum.capabilities.create}}
+    {{^forum.capabilities.create}}
+        {{#forum.capabilities.selfenrol}}
+            <div class="p-t-1 p-b-1">
+                <a class="btn btn-primary" href="{{forum.urls.create}}">
+                    {{$discussion_create_text}}
+                        {{#str}}addanewdiscussion, forum{{/str}}
+                    {{/discussion_create_text}}
+                </a>
+            </div>
+        {{/forum.capabilities.selfenrol}}
+    {{/forum.capabilities.create}}
+
 
     {{#state.hasdiscussions}}
         {{$discussion_top_pagination}}
                             {{/state.sortorder.iscreateddesc}}
                         </th>
                         <th scope="col">&nbsp;</th>
-                        {{#forum.capabilities.subscribe}}
-                            <th scope="col" class="discussionsubscription"></th>
-                        {{/forum.capabilities.subscribe}}
+                        <th scope="col" class="discussionsubscription"></th>
                     </tr>
                 </thead>
                 {{/discussion_list_header}}
                                                 class="rounded-circle userpicture"
                                                 src="{{urls.profileimage}}"
                                                 alt="{{#str}}pictureof, moodle, {{fullname}}{{/str}}"
+                                                title="{{#str}}pictureof, moodle, {{fullname}}{{/str}}"
                                             >
                                         </div>
                                         <div class="align-middle p-2">
                                 {{/firstpostauthor}}
                             </td>
                             {{#forum.state.groupmode}}
-                                <td scope="col" class="group">
+                                <td scope="col" class="group align-middle">
                                     {{#discussion.group}}
                                         {{#urls.picture}}
                                             {{#urls.userlist}}
-                                                <a href="{{{urls.userlist}}}">
-                                                    <img class="border rounded h-auto rounded-circle" src="{{{urls.picture}}}">
+                                                <a href="{{{urls.userlist}}}" role="button" aria-label='{{#str}} memberofgroup, group, {{name}}{{/str}}'>
+                                                    <img alt="{{#str}} pictureof, core, {{name}} {{/str}}"
+                                                         aria-hidden="true"
+                                                         class="border rounded h-auto rounded-circle grouppicture"
+                                                         src="{{{urls.picture}}}"
+                                                         title="{{#str}} pictureof, core, {{name}} {{/str}}">
                                                 </a>
                                             {{/urls.userlist}}
                                             {{^urls.userlist}}
-                                                <img class="border rounded h-auto rounded-circle" src="{{{urls.picture}}}">
+                                                <img alt="{{#str}} pictureof, core, {{name}} {{/str}}"
+                                                     class="border rounded h-auto rounded-circle grouppicture"
+                                                     src="{{{urls.picture}}}"
+                                                     title="{{#str}} pictureof, core, {{name}} {{/str}}">
                                             {{/urls.userlist}}
                                         {{/urls.picture}}
                                         {{^urls.picture}}
                                             {{#urls.userlist}}
-                                                <a href="{{{urls.userlist}}}">{{name}}</a>
+                                                <a href="{{{urls.userlist}}}" aria-label='{{#str}} memberofgroup, group, {{name}}{{/str}}'>{{name}}</a>
                                             {{/urls.userlist}}
                                             {{^urls.userlist}}
                                                 {{name}}
                                                     class="rounded-circle userpicture"
                                                     src="{{latestpostauthor.urls.profileimage}}"
                                                     alt="{{#str}}pictureof, moodle, {{latestpostauthor.fullname}}{{/str}}"
+                                                    title="{{#str}}pictureof, moodle, {{latestpostauthor.fullname}}{{/str}}"
                                                 >
                                             </a>
                                         </div>
                                         <div class="pt-1 mt-2  {{^discussion.locked}}hidden{{/discussion.locked}}" data-region="locked-icon">
                                             <span class="btn" >{{#pix}}i/lock, core, {{#str}}locked, forum{{/str}}{{/pix}}</span>
                                         </div>
+                                        {{#forum.capabilities.subscribe}}
                                         <div>
                                             {{> mod_forum/discussion_subscription_toggle}}
                                         </div>
+                                        {{/forum.capabilities.subscribe}}
+                                        {{#hasanyactions}}
                                         <div class="mt-3" data-container='discussion-tools'>
                                             {{> mod_forum/forum_action_menu}}
                                         </div>
+                                        {{/hasanyactions}}
                                     </div>
                                 {{/discussion}}
                             </td>
index f4d37ff..8d661ff 100644 (file)
@@ -32,6 +32,7 @@
 
 <div id="discussion-container-{{uniqid}}" data-content="forum-discussion">
 {{#html}}
+    {{#hasanyactions}}
     <div class="d-flex flex-wrap flex-row-reverse m-b-1 text-right" data-container="discussion-tools">
 
         <div class="pl-1">
@@ -41,6 +42,7 @@
         </div>
         <div class="pl-1">{{{subscribe}}}</div>
     </div>
+    {{/hasanyactions}}
     {{{neighbourlinks}}}
 
     <div class="d-flex flex-wrap mb-1">
index e5a8e1d..5023af8 100644 (file)
@@ -58,6 +58,7 @@
                                     src="{{{.}}}"
                                     alt="{{#str}} pictureof, core, {{author.fullname}} {{/str}}"
                                     aria-hidden="true"
+                                    title="{{#str}} pictureof, core, {{author.fullname}} {{/str}}"
                                 >
                             {{/urls.profileimage}}
                         </div>
                     {{#parentauthorname}}
                         <span class="sr-only">{{#str}} inreplyto, mod_forum, {{.}} {{/str}}</span>
                     {{/parentauthorname}}
-                    <h3 class="h6 font-weight-bold mb-0" data-region-content="forum-post-core-subject">{{$subject}}{{{subject}}}{{/subject}}</h3>
+                    <h3 {{!
+                        }}class="h6 font-weight-bold mb-0" {{!
+                        }}data-region-content="forum-post-core-subject" {{!
+                        }}data-reply-subject="{{replysubject}}" {{!
+                        }}>{{$subject}}{{{subject}}}{{/subject}}</h3>
                     {{^isdeleted}}
                         <address tabindex="-1">
                             {{#html.authorsubheading}}{{{.}}}{{/html.authorsubheading}}
                     {{#author}}
                         <div class="mr-2 author-groups-container" style="width: 45px; flex-shrink: 0">
                             {{#groups}}
-                                {{#urls.image}}
-                                    <img
-                                        class="rounded-circle w-100"
-                                        src="{{{.}}}"
-                                        alt="{{#str}} pictureof, core, {{name}} {{/str}}"
-                                        aria-hidden="true"
-                                    >
-                                {{/urls.image}}
+                                {{#urls.group}}
+                                    {{#urls.image}}
+                                        <a href="{{urls.group}}" role="button" aria-label='{{#str}} memberofgroup, group, {{name}}{{/str}}'>
+                                            <img
+                                                 class="rounded-circle w-100"
+                                                 src="{{{.}}}"
+                                                 alt="{{#str}} pictureof, core, {{name}} {{/str}}"
+                                                 aria-hidden="true"
+                                                 title="{{#str}} pictureof, core, {{name}} {{/str}}"
+                                            >
+                                        </a>
+                                    {{/urls.image}}
+                                {{/urls.group}}
+                                {{^urls.group}}
+                                    {{#urls.image}}
+                                        <img class="rounded-circle w-100"
+                                            src="{{{.}}}"
+                                            alt="{{#str}} pictureof, core, {{name}} {{/str}}"
+                                            title="{{#str}} pictureof, core, {{name}} {{/str}}"
+                                        >
+                                    {{/urls.image}}
+                                {{/urls.group}}
                             {{/groups}}
                         </div>
                     {{/author}}
                                                     </a>
                                                 {{/replyoutput}}
                                             {{/reply}}
+                                            {{^reply}}
+                                                {{#selfenrol}}
+                                                    {{$replyoutput}}
+                                                        <a
+                                                                href="{{{urls.reply}}}"
+                                                                class="btn btn-link"
+                                                                data-post-id="{{id}}"
+                                                                data-can-reply-privately="{{canreplyprivately}}"
+                                                                title="{{#str}} reply, mod_forum {{/str}}"
+                                                        >
+                                                            {{#str}} reply, mod_forum {{/str}}
+                                                        </a>
+                                                    {{/replyoutput}}
+                                                {{/selfenrol}}
+                                            {{/reply}}
                                             {{#export}}
                                                 <a
                                                     data-region="post-action"
index 8a99d1d..5ceb5c9 100644 (file)
@@ -41,7 +41,7 @@
                     <textarea rows="5" name="post" title="post" class="w-100" placeholder="{{#str}} replyplaceholder, forum {{/str}}"></textarea>
                     <input type="hidden" name="postformat" value="{{postformat}}"/>
                 </span>
-                <input type="hidden" name="subject" value="{{#str}} inpagereplysubject, forum, {{parentsubject}} {{/str}}"/>
+                <input type="hidden" name="subject" value="{{parentsubject}}"/>
                 <input type="hidden" name="reply" value="{{postid}}"/>
                 <input type="hidden" name="sesskey" value="{{sesskey}}"/>
             </div>
index 425abc7..67cbcdf 100644 (file)
@@ -104,6 +104,7 @@ class mod_forum_exporters_post_testcase extends advanced_testcase {
         $canexport = true;
         $cancontrolreadstatus = true;
         $canreplyprivately = true;
+        $canenrol = true;
         $capabilitymanager = new test_capability_manager(
             $canview,
             $canedit,
@@ -112,7 +113,8 @@ class mod_forum_exporters_post_testcase extends advanced_testcase {
             $canreply,
             $canexport,
             $cancontrolreadstatus,
-            $canreplyprivately
+            $canreplyprivately,
+            $canenrol
         );
         $managerfactory = \mod_forum\local\container::get_manager_factory();
         $entityfactory = \mod_forum\local\container::get_entity_factory();
@@ -157,6 +159,7 @@ class mod_forum_exporters_post_testcase extends advanced_testcase {
         $this->assertEquals($cansplit, $exportedpost->capabilities['split']);
         $this->assertEquals($canreply, $exportedpost->capabilities['reply']);
         $this->assertEquals($canexport, $exportedpost->capabilities['export']);
+        $this->assertEquals($canenrol, $exportedpost->capabilities['selfenrol']);
         $this->assertEquals($cancontrolreadstatus, $exportedpost->capabilities['controlreadstatus']);
         $this->assertNotEmpty($exportedpost->urls['view']);
         $this->assertNotEmpty($exportedpost->urls['viewisolated']);
@@ -416,6 +419,8 @@ class test_capability_manager extends capability_manager {
     private $controlreadstatus;
     /** @var bool $controlreadstatus Value for can_reply_privately_to_post */
     private $canreplyprivatelytopost;
+    /** @var bool $canenrol Value for can_self_enrol */
+    private $canenrol;
 
     /**
      * Constructor.
@@ -436,7 +441,8 @@ class test_capability_manager extends capability_manager {
         bool $reply = true,
         bool $export = true,
         bool $controlreadstatus = true,
-        bool $canreplyprivatelytopost = true
+        bool $canreplyprivatelytopost = true,
+        bool $canenrol = true
     ) {
         $this->view = $view;
         $this->edit = $edit;
@@ -446,6 +452,7 @@ class test_capability_manager extends capability_manager {
         $this->export = $export;
         $this->controlreadstatus = $controlreadstatus;
         $this->canreplyprivatelytopost = $canreplyprivatelytopost;
+        $this->canenrol = $canenrol;
     }
 
     /**
@@ -538,4 +545,13 @@ class test_capability_manager extends capability_manager {
     public function can_reply_privately_to_post(stdClass $user, post_entity $post) : bool {
         return $this->canreplyprivatelytopost;
     }
+
+    /**
+     * Override can_self_enrol
+     * @param stdClass $user
+     * @return bool
+     */
+    public function can_self_enrol(stdClass $user) : bool {
+        return $this->canenrol;
+    }
 }
index 5e903ac..8b6fd97 100644 (file)
@@ -668,6 +668,7 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
             'hasparent' => true,
             'timecreated' => $discussion1reply2->created,
             'subject' => $discussion1reply2->subject,
+            'replysubject' => get_string('re', 'mod_forum') . " {$discussion1reply2->subject}",
             'message' => file_rewrite_pluginfile_urls($discussion1reply2->message, 'pluginfile.php',
                     $forum1context->id, 'mod_forum', 'post', $discussion1reply2->id),
             'messageformat' => 1,   // This value is usually changed by external_format_text() function.
@@ -692,7 +693,8 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
                 'reply' => 1,
                 'export' => 0,
                 'controlreadstatus' => 0,
-                'canreplyprivately' => 0
+                'canreplyprivately' => 0,
+                'selfenrol' => 0
             ],
             'urls' => [
                 'view' => $urlfactory->get_view_post_url_from_post_id($discussion1reply2->discussion, $discussion1reply2->id),
@@ -721,6 +723,7 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
             'hasparent' => true,
             'timecreated' => $discussion1reply1->created,
             'subject' => $discussion1reply1->subject,
+            'replysubject' => get_string('re', 'mod_forum') . " {$discussion1reply1->subject}",
             'message' => file_rewrite_pluginfile_urls($discussion1reply1->message, 'pluginfile.php',
                     $forum1context->id, 'mod_forum', 'post', $discussion1reply1->id),
             'messageformat' => 1,   // This value is usually changed by external_format_text() function.
@@ -745,7 +748,8 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
                 'reply' => 1,
                 'export' => 0,
                 'controlreadstatus' => 0,
-                'canreplyprivately' => 0
+                'canreplyprivately' => 0,
+                'selfenrol' => 0
             ],
             'urls' => [
                 'view' => $urlfactory->get_view_post_url_from_post_id($discussion1reply1->discussion, $discussion1reply1->id),
diff --git a/mod/imscp/tests/behat/display_imscp_description.feature b/mod/imscp/tests/behat/display_imscp_description.feature
new file mode 100644 (file)
index 0000000..c0024bb
--- /dev/null
@@ -0,0 +1,71 @@
+@mod @mod_imscp
+Feature: Display the IMS content package description in the IMSCP and optionally in the course
+  In order to display the the IMS content package description description in the course
+  As a teacher
+  I need to enable the 'Display description on course page' setting.
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1 | topics |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+
+  @javascript
+  Scenario: Description is displayed in the IMS content package
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add a "IMS content package" to section "1"
+    And I set the following fields to these values:
+      | Name | Test IMS content package |
+      | Description | Test IMS content package description |
+    And I upload "mod/imscp/tests/packages/singlescobasic.zip" file to "Package file" filemanager
+    And I click on "Save and display" "button"
+    And I am on "Course 1" course homepage
+    And I should see "Test IMS content package"
+    When I follow "Test IMS content package"
+    Then I should see "Test IMS content package description"
+
+  @javascript
+  Scenario: Show IMS description in the course homepage
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add a "IMS content package" to section "1"
+    And I set the following fields to these values:
+      | Name | Test IMS content package |
+      | Description | Test IMS content package description |
+    And I upload "mod/imscp/tests/packages/singlescobasic.zip" file to "Package file" filemanager
+    And I click on "Save and display" "button"
+    And I am on "Course 1" course homepage
+    And I follow "Test IMS content package"
+    And I navigate to "Edit settings" in current page administration
+    And the following fields match these values:
+      | Display description on course page | |
+    And I set the following fields to these values:
+      | Display description on course page | 1 |
+    And I press "Save and return to course"
+    When I am on "Course 1" course homepage
+    Then I should see "Test IMS content package description"
+
+  @javascript
+  Scenario: Hide IMS description in the course homepage
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add a "IMS content package" to section "1"
+    And I set the following fields to these values:
+      | Name | Test IMS content package |
+      | Description | Test IMS content package description |
+    And I upload "mod/imscp/tests/packages/singlescobasic.zip" file to "Package file" filemanager
+    And I click on "Save and display" "button"
+    And I am on "Course 1" course homepage
+    And I follow "Test IMS content package"
+    And I navigate to "Edit settings" in current page administration
+    And the following fields match these values:
+      | Display description on course page | |
+    And I press "Save and return to course"
+    When I am on "Course 1" course homepage
+    Then I should not see "Test IMS content package description"
index f47f6cc..87aefe6 100644 (file)
@@ -70,6 +70,10 @@ if (!$imscp->structure) {
 
 echo $OUTPUT->header();
 echo $OUTPUT->heading(format_string($imscp->name));
+// Info box.
+if ($imscp->intro) {
+    echo $OUTPUT->box(format_module_intro('imscp', $imscp, $cm->id), 'generalbox', 'intro');
+}
 
 imscp_print_content($imscp, $cm, $course);
 
index 212fcb8..2ee7d14 100644 (file)
@@ -37,7 +37,7 @@ class mod_lesson_renderer extends plugin_renderer_base {
      * @return string
      */
     public function header($lesson, $cm, $currenttab = '', $extraeditbuttons = false, $lessonpageid = null, $extrapagetitle = null) {
-        global $CFG;
+        global $CFG, $OUTPUT;
 
         $activityname = format_string($lesson->name, true, $lesson->course);
         if (empty($extrapagetitle)) {
@@ -49,7 +49,7 @@ class mod_lesson_renderer extends plugin_renderer_base {
         // Build the buttons
         $context = context_module::instance($cm->id);
 
-    /// Header setup
+        // Header setup.
         $this->page->set_title($title);
         $this->page->set_heading($this->page->course->fullname);
         lesson_add_header_buttons($cm, $context, $extraeditbuttons, $lessonpageid);
@@ -57,7 +57,10 @@ class mod_lesson_renderer extends plugin_renderer_base {
 
         if (has_capability('mod/lesson:manage', $context)) {
             $output .= $this->output->heading_with_help($activityname, 'overview', 'lesson');
-
+            // Info box.
+            if ($lesson->intro) {
+                $output .= $OUTPUT->box(format_module_intro('lesson', $lesson, $cm->id), 'generalbox', 'intro');
+            }
             if (!empty($currenttab)) {
                 ob_start();
                 include($CFG->dirroot.'/mod/lesson/tabs.php');
@@ -66,6 +69,10 @@ class mod_lesson_renderer extends plugin_renderer_base {
             }
         } else {
             $output .= $this->output->heading($activityname);
+            // Info box.
+            if ($lesson->intro) {
+                $output .= $OUTPUT->box(format_module_intro('lesson', $lesson, $cm->id), 'generalbox', 'intro');
+            }
         }
 
         foreach ($lesson->messages as $message) {
diff --git a/mod/lesson/tests/behat/display_lesson_description.feature b/mod/lesson/tests/behat/display_lesson_description.feature
new file mode 100644 (file)
index 0000000..07b3540
--- /dev/null
@@ -0,0 +1,50 @@
+@mod @mod_lesson
+Feature: Display the lesson description in the lesson and optionally in the course
+  In order to display the the lesson description description in the course
+  As a teacher
+  I need to enable the 'Display description on course page' setting.
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1 | topics |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add a "Lesson" to section "1"
+    And I set the following fields to these values:
+      | Name | Test lesson |
+      | Description | Test lesson description |
+    And I click on "Save and display" "button"
+
+  Scenario: Description is displayed in the Lesson
+    Given I am on "Course 1" course homepage
+    When I follow "Test lesson"
+    Then I should see "Test lesson description"
+
+  Scenario: Show lesson description in the course homepage
+    Given I am on "Course 1" course homepage
+    And I follow "Test lesson"
+    And I navigate to "Edit settings" in current page administration
+    And the following fields match these values:
+      | Display description on course page | |
+    And I set the following fields to these values:
+      | Display description on course page | 1 |
+    And I press "Save and return to course"
+    When I am on "Course 1" course homepage
+    Then I should see "Test lesson description"
+
+  Scenario: Hide lesson description in the course homepage
+    Given I am on "Course 1" course homepage
+    And I follow "Test lesson"
+    And I navigate to "Edit settings" in current page administration
+    And the following fields match these values:
+      | Display description on course page | |
+    And I press "Save and return to course"
+    When I am on "Course 1" course homepage
+    Then I should not see "Test lesson description"
index 2be6141..655e33e 100644 (file)
Binary files a/mod/lti/amd/build/tool_configure_controller.min.js and b/mod/lti/amd/build/tool_configure_controller.min.js differ
index a8fed07..40dc885 100644 (file)
@@ -162,9 +162,12 @@ define(['jquery', 'core/ajax', 'core/notification', 'core/templates', 'mod_lti/e
     var showCartridgeRegistration = function(url) {
         hideExternalRegistration();
         hideRegistrationChoices();
-        getCartridgeRegistrationContainer().removeClass('hidden');
-        getCartridgeRegistrationContainer().find(SELECTORS.CARTRIDGE_REGISTRATION_FORM).attr('data-cartridge-url', url);
-        screenReaderAnnounce(getCartridgeRegistrationContainer());
+        // Don't save the key and secret from the last tool.
+        var container = getCartridgeRegistrationContainer();
+        container.find('input').val('');
+        container.removeClass('hidden');
+        container.find(SELECTORS.CARTRIDGE_REGISTRATION_FORM).attr('data-cartridge-url', url);
+        screenReaderAnnounce(container);
     };
 
     /**
index c4e843f..39c0c40 100644 (file)
@@ -551,6 +551,9 @@ class quiz_grading_report extends quiz_default_report {
                 case "studentfirstname":
                     $orderby = "u.firstname, u.lastname";
                     break;
+                case "idnumber":
+                    $orderby = "u.idnumber";
+                    break;
             }
         }
 
index 840902b..e2d793b 100644 (file)
@@ -1,4 +1,4 @@
-@mod @mod_quiz
+@mod @mod_quiz @quiz @quiz_grading
 Feature: Basic use of the Manual grading report
   In order to easily find students attempts that need manual grading
   As a teacher
@@ -50,8 +50,15 @@ Feature: Basic use of the Manual grading report
     And "Short answer 001" row "To grade" column of "questionstograde" table should contain "0"
     And "Short answer 001" row "Already graded" column of "questionstograde" table should contain "0"
 
-    # Adjust the mark for Student1.
+    # Go to the grading page.
     And I click on "update grades" "link" in the "Short answer 001" "table_row"
+    And I should see "Grading attempts 1 to 1 of 1"
+
+    # Test the display options.
+    And I set the field "Order attempts" to "By student id number"
+    And I press "Change options"
+
+    # Adjust the mark for Student1.
     And I set the field "Comment" to "I have adjusted your mark to 0.6"
     And I set the field "Mark" to "0.6"
     And I press "Save and go to next page"
index 07bf5b5..f0cc78e 100644 (file)
@@ -282,16 +282,9 @@ if (file_exists($CFG->dirroot.'/mod/scorm/datamodels/'.$scorm->version.'.php'))
     include_once($CFG->dirroot.'/mod/scorm/datamodels/scorm_12.php');
 }
 
-// Add the checknet system to keep checking for a connection.
-$PAGE->requires->string_for_js('networkdropped', 'mod_scorm');
-// 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));
+// Add the keepalive system to keep checking for a connection.
+\core\session\manager::keepalive('networkdropped', 'mod_scorm', 30, 10);
+
 echo $OUTPUT->footer();
 
 // Set the start time of this SCO.
diff --git a/mod/scorm/tests/behat/behat_mod_scorm.php b/mod/scorm/tests/behat/behat_mod_scorm.php
new file mode 100644 (file)
index 0000000..ed7e16f
--- /dev/null
@@ -0,0 +1,54 @@
+<?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/>.
+
+/**
+ * Steps definitions related to the SCORM activity module.
+ *
+ * @package    mod_scorm
+ * @category   test
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
+
+require_once(__DIR__ . '/../../../../lib/behat/behat_base.php');
+
+use Behat\Behat\Hook\Scope\AfterScenarioScope;
+
+/**
+ * Steps definitions related to the SCORM activity module.
+ *
+ * @package    mod_scorm
+ * @category   test
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_mod_scorm extends behat_base {
+
+    /**
+     * Restart the Seleium Session after each mod_scorm Scenario.
+     *
+     * This prevents issues with the scorm player's onbeforeunload event, and cached SCORM content being served to the
+     * browser in subsequent tests.
+     *
+     * @AfterScenario @mod_scorm
+     * @param AfterScenarioScope $scope The scenario scope
+     */
+    public function reset_after_scorm(AfterScenarioScope $scope) {
+        $this->getSession()->stop();
+    }
+}
diff --git a/pix/movehere.svg b/pix/movehere.svg
new file mode 100644 (file)
index 0000000..ce2d5d9
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="80" height="16" viewBox="0 0 80 16" preserveAspectRatio="xMinYMid meet"><style>.st0{fill:none;stroke:#000;stroke-width:2;stroke-miterlimit:10}</style><path d="M25.4 1.5h3.5v1h-3.5zM24.4 1.5h1v3h-1zM25.4 14.5v-4h-1v4h.5zM30.4 1.5h3v1h-3zM34.9 1.5h3v1h-3zM24.4 6h1v3h-1zM66.4 1.5h3v1h-3zM57.4 1.5h3v1h-3zM61.9 1.5h3v1h-3zM70.9 1.5h3v1h-3zM48.4 1.5h3v1h-3zM52.9 1.5h3v1h-3zM43.9 1.5h3v1h-3zM39.4 1.5h3v1h-3zM33.9 13.5h3v1h-3zM69.9 13.5h3v1h-3zM77.4 13.5h-3v1h3V14zM65.4 13.5h3v1h-3zM75.4 1.5v1h2V5h1V1.5zM60.9 13.5h3v1h-3z"/><path d="M25 13.5h-.5v1h3.4v-1zM77.4 6.5h1v3h-1zM77.4 13.9v.6h1V11h-1zM38.4 13.5h3v1h-3zM29.4 13.5h3v1h-3zM42.9 13.5h3v1h-3zM56.4 13.5h3v1h-3zM47.4 13.5h3v1h-3zM51.9 13.5h3v1h-3z"/><path class="st0" d="M13 4l4 4-4 4m4-4H2.8"/></svg>
index e21e8db..646a6e6 100644 (file)
@@ -43,7 +43,7 @@ $string['predictioncalculations'] = 'Indicator calculations';
 $string['predictiondetails'] = 'Prediction details';
 $string['nodetailsavailable'] = 'No prediction details are relevant.';
 $string['timecreated'] = 'Time predicted';
-$string['timerange'] = 'Time range';
+$string['timerange'] = 'Analysis interval';
 $string['timerangewithdata'] = '{$a->timestart} to {$a->timeend}';
 $string['selectotherinsights'] = 'Select other insights...';
 $string['privacy:metadata'] = 'The Insights plugin does not store any personal data.';
index 4ff2984..56d6e0d 100644 (file)
@@ -61,11 +61,11 @@ $string['check_frontpagerole_error'] = 'Incorrectly defined frontpage role "{$a}
 $string['check_frontpagerole_name'] = 'Frontpage role';
 $string['check_frontpagerole_notset'] = 'Frontpage role is not set.';
 $string['check_frontpagerole_ok'] = 'Frontpage role definition is OK.';
-$string['check_google_details'] = '<p>The Open to Google setting enables search engines to enter courses with guest access. There is no point in enabling this setting if guest login is not allowed.</p>';
-$string['check_google_error'] = 'Search engine access is allowed but guest access is disabled.';
-$string['check_google_info'] = 'Search engines may enter as guests.';
-$string['check_google_name'] = 'Open to Google';
-$string['check_google_ok'] = 'Search engine access is not enabled.';
+$string['check_crawlers_details'] = '<p>The "Open to search engines" setting enables search engines to enter courses with guest access. There is no point in enabling this setting if guest login is not allowed.</p>';
+$string['check_crawlers_error'] = 'Search engine access is allowed but guest access is disabled.';
+$string['check_crawlers_info'] = 'Search engines may enter as guests.';
+$string['check_crawlers_name'] = 'Open to search engines';
+$string['check_crawlers_ok'] = 'Search engine access is not enabled.';
 $string['check_guestrole_details'] = '<p>The guest role is used for guests, not logged in users and temporary guest course access. Please make sure no risky capabilities are allowed in this role.</p>
 <p>The only supported legacy type for guest role is <em>Guest</em>.</p>';
 $string['check_guestrole_error'] = 'The guest role "{$a}" is incorrectly defined!';
index e696b83..8358e4a 100644 (file)
@@ -47,7 +47,7 @@ function report_security_get_issue_list() {
         'report_security_check_embed',
         'report_security_check_mediafilterswf',
         'report_security_check_openprofiles',
-        'report_security_check_google',
+        'report_security_check_crawlers',
         'report_security_check_passwordpolicy',
         'report_security_check_emailchangeconfirmation',
         'report_security_check_cookiesecur