Merge branch 'MDL-55609-master' of git://github.com/andrewnicols/moodle
authorDavid Monllao <davidm@moodle.com>
Tue, 12 Jun 2018 08:07:35 +0000 (10:07 +0200)
committerDavid Monllao <davidm@moodle.com>
Tue, 12 Jun 2018 08:07:35 +0000 (10:07 +0200)
65 files changed:
admin/tool/dataprivacy/classes/task/initiate_data_request_task.php
admin/tool/dataprivacy/classes/task/process_data_request_task.php
admin/tool/dataprivacy/createdatarequest_form.php
admin/user.php
backup/util/ui/backup_ui_stage.class.php
backup/util/ui/base_moodleform.class.php
backup/util/ui/renderer.php
composer.json
composer.lock
course/classes/management_renderer.php
course/renderer.php
index.php
lang/en/admin.php
lang/en/backup.php
lang/en/moodle.php
lib/classes/oauth2/api.php
lib/classes/output/icon_system_fontawesome.php
lib/classes/user.php
lib/classes/useragent.php
lib/dml/mysqli_native_moodle_database.php
lib/filestorage/stored_file.php
lib/moodlelib.php
lib/outputrenderers.php
lib/phpunit/classes/hint_resultprinter.php
lib/tests/useragent_test.php
login/index.php
login/lib.php
media/player/videojs/classes/plugin.php
message/templates/message_area_contact.mustache
mod/assign/feedback/editpdf/classes/renderer.php
mod/assign/feedback/editpdf/lang/en/assignfeedback_editpdf.php
mod/assign/feedback/editpdf/styles.css
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/annotationhighlight.js
mod/assign/feedback/editpdf/yui/src/editor/js/editor.js
mod/assign/renderable.php
mod/assign/renderer.php
mod/assign/styles.css
mod/chat/lib.php
mod/chat/locallib.php
mod/chat/report.php
mod/chat/tests/externallib_test.php
mod/chat/tests/lib_test.php
mod/folder/renderer.php
mod/forum/lib.php
mod/forum/styles.css
pix/t/online.png [new file with mode: 0644]
pix/t/online.svg [new file with mode: 0644]
privacy/classes/local/request/contextlist_base.php
privacy/tests/contextlist_base_test.php
question/type/multianswer/renderer.php
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/message.scss
theme/boost/scss/moodle/modules.scss
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/message.less
theme/bootstrapbase/less/moodle/modules.less
theme/bootstrapbase/style/moodle.css
theme/upgrade.txt
user/classes/participants_table.php
user/lib.php
version.php

index 2ee4a0a..70402f4 100644 (file)
@@ -54,7 +54,7 @@ class initiate_data_request_task extends adhoc_task {
     public function execute() {
         global $CFG;
 
-        require_once($CFG->dirroot . '/admin/tool/dataprivacy/lib.php');
+        require_once($CFG->dirroot . "/{$CFG->admin}/tool/dataprivacy/lib.php");
 
         if (!isset($this->get_custom_data()->requestid)) {
             throw new coding_exception('The custom data \'requestid\' is required.');
index 6a93217..c58f574 100644 (file)
@@ -57,7 +57,7 @@ class process_data_request_task extends adhoc_task {
     public function execute() {
         global $CFG, $PAGE, $SITE;
 
-        require_once($CFG->dirroot . '/admin/tool/dataprivacy/lib.php');
+        require_once($CFG->dirroot . "/{$CFG->admin}/tool/dataprivacy/lib.php");
 
         if (!isset($this->get_custom_data()->requestid)) {
             throw new coding_exception('The custom data \'requestid\' is required.');
index 680b93a..4c94a8a 100644 (file)
@@ -44,19 +44,28 @@ class tool_dataprivacy_data_request_form extends moodleform {
     /**
      * Form definition.
      *
-     * @throws HTML_QuickForm_Error
      * @throws coding_exception
-     * @throws dml_exception
      */
     public function definition() {
-        global $DB, $USER;
+        global $USER;
         $mform =& $this->_form;
 
         $this->manage = $this->_customdata['manage'];
         if ($this->manage) {
             $options = [
                 'ajax' => 'tool_dataprivacy/form-user-selector',
-                'multiple' => false
+                'valuehtmlcallback' => function($value) {
+                    global $OUTPUT;
+
+                    $allusernames = get_all_user_name_fields(true);
+                    $fields = 'id, email, ' . $allusernames;
+                    $user = \core_user::get_user($value, $fields);
+                    $useroptiondata = [
+                        'fullname' => fullname($user),
+                        'email' => $user->email
+                    ];
+                    return $OUTPUT->render_from_template('tool_dataprivacy/form-user-selector-suggestion', $useroptiondata);
+                }
             ];
             $mform->addElement('autocomplete', 'userid', get_string('requestfor', 'tool_dataprivacy'), [], $options);
             $mform->addRule('userid', null, 'required', null, 'client');
index 0caf98f..f6ab61f 100644 (file)
@@ -19,6 +19,7 @@
     $suspend      = optional_param('suspend', 0, PARAM_INT);
     $unsuspend    = optional_param('unsuspend', 0, PARAM_INT);
     $unlock       = optional_param('unlock', 0, PARAM_INT);
+    $resendemail  = optional_param('resendemail', 0, PARAM_INT);
 
     admin_externalpage_setup('editusers');
 
@@ -37,6 +38,7 @@
     $strunsuspend = get_string('unsuspenduser', 'admin');
     $strunlock = get_string('unlockaccount', 'admin');
     $strconfirm = get_string('confirm');
+    $strresendemail = get_string('resendemail');
 
     $returnurl = new moodle_url('/admin/user.php', array('sort' => $sort, 'dir' => $dir, 'perpage' => $perpage, 'page'=>$page));
 
             redirect($returnurl, get_string('usernotconfirmed', '', fullname($user, true)));
         }
 
+    } else if ($resendemail && confirm_sesskey()) {
+        if (!$user = $DB->get_record('user', ['id' => $resendemail, 'mnethostid' => $CFG->mnet_localhost_id, 'deleted' => 0])) {
+            print_error('nousers');
+        }
+
+        // Prevent spamming users who are already confirmed.
+        if ($user->confirmed) {
+            print_error('alreadyconfirmed');
+        }
+
+        $returnmsg = get_string('emailconfirmsentsuccess');
+        $messagetype = \core\output\notification::NOTIFY_SUCCESS;
+        if (!send_confirmation_email($user)) {
+            $returnmsg = get_string('emailconfirmsentfailure');
+            $messagetype = \core\output\notification::NOTIFY_ERROR;
+        }
+
+        redirect($returnurl, $returnmsg, null, $messagetype);
     } else if ($delete and confirm_sesskey()) {              // Delete a selected user, after confirmation
         require_capability('moodle/user:delete', $sitecontext);
 
                 } else {
                     $lastcolumn = "<span class=\"dimmed_text\">".get_string('confirm')."</span>";
                 }
+
+                $lastcolumn .= ' | ' . html_writer::link(new moodle_url($returnurl,
+                    [
+                        'resendemail' => $user->id,
+                        'sesskey' => sesskey()
+                    ]
+                ), $strresendemail);
             }
 
             if ($user->lastaccess) {
index ec1b3b1..1e1caf8 100644 (file)
@@ -183,7 +183,11 @@ class backup_ui_stage_initial extends backup_ui_stage {
             foreach ($tasks as &$task) {
                 // For the initial stage we are only interested in the root settings.
                 if ($task instanceof backup_root_task) {
-                    $form->add_heading('rootsettings', get_string('rootsettings', 'backup'));
+                    if ($this->ui instanceof import_ui) {
+                        $form->add_heading('rootsettings', get_string('importrootsettings', 'backup'));
+                    } else {
+                        $form->add_heading('rootsettings', get_string('rootsettings', 'backup'));
+                    }
                     $settings = $task->get_settings();
                     // First add all settings except the filename setting.
                     foreach ($settings as &$setting) {
@@ -469,7 +473,11 @@ class backup_ui_stage_confirmation extends backup_ui_stage {
             foreach ($tasks as $task) {
                 if ($task instanceof backup_root_task) {
                     // If its a backup root add a root settings heading to group nicely.
-                    $form->add_heading('rootsettings', get_string('rootsettings', 'backup'));
+                    if ($this->ui instanceof import_ui) {
+                        $form->add_heading('rootsettings', get_string('importrootsettings', 'backup'));
+                    } else {
+                        $form->add_heading('rootsettings', get_string('rootsettings', 'backup'));
+                    }
                 } else if (!$courseheading) {
                     // We haven't already add a course heading.
                     $form->add_heading('coursesettings', get_string('includeditems', 'backup'));
index 4685a66..32ac8fb 100644 (file)
@@ -383,7 +383,13 @@ abstract class base_moodleform extends moodleform {
         $this->require_definition_after_data();
 
         $config = new stdClass;
-        $config->title = get_string('confirmcancel', 'backup');
+        if ($this->uistage->get_ui() instanceof import_ui) {
+            $config->title = get_string('confirmcancelimport', 'backup');
+        } else if ($this->uistage->get_ui() instanceof restore_ui) {
+            $config->title = get_string('confirmcancelrestore', 'backup');
+        } else {
+            $config->title = get_string('confirmcancel', 'backup');
+        }
         $config->question = get_string('confirmcancelquestion', 'backup');
         $config->yesLabel = get_string('confirmcancelyes', 'backup');
         $config->noLabel = get_string('confirmcancelno', 'backup');
index f656577..23462cf 100644 (file)
@@ -125,7 +125,13 @@ class core_backup_renderer extends plugin_renderer_base {
         $html .= html_writer::end_tag('div');
 
         $html .= html_writer::start_tag('div', array('class' => 'backup-section settings-section'));
-        $html .= $this->output->heading(get_string('backupsettings', 'backup'), 2, array('class' => 'header'));
+        if ($this instanceof import_ui_stage_inital) {
+            $html .= $this->output->heading(get_string('importrootsettings', 'backup'), 2, array('class' => 'header'));
+        } else if ($this instanceof restore_ui_stage_settings) {
+            $html .= $this->output->heading(get_string('restorerootsettings', 'backup'), 2, array('class' => 'header'));
+        } else {
+            $html .= $this->output->heading(get_string('backupsettings', 'backup'), 2, array('class' => 'header'));
+        }
         foreach ($details->root_settings as $label => $value) {
             if ($label == 'filename' or $label == 'user_files') {
                 continue;
index 3c6ddd6..5b526cc 100644 (file)
@@ -7,7 +7,7 @@
     "require-dev": {
         "phpunit/phpunit": "6.5.*",
         "phpunit/dbUnit": "3.0.*",
-        "moodlehq/behat-extension": "3.35.1",
+        "moodlehq/behat-extension": "3.36.0",
         "mikey179/vfsStream": "^1.6"
     }
 }
index c79a846..e8aaa2f 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "content-hash": "f047772340a956bbf2c02d91536b11c6",
+    "content-hash": "956ce0b653b805efb6a9a483f8c9a847",
     "packages": [],
     "packages-dev": [
         {
         },
         {
             "name": "behat/mink-browserkit-driver",
-            "version": "v1.3.2",
+            "version": "1.3.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/minkphp/MinkBrowserKitDriver.git",
-                "reference": "10e67fb4a295efcd62ea0bf16025a85ea19534fb"
+                "reference": "1b9a7ce903cfdaaec5fb32bfdbb26118343662eb"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/minkphp/MinkBrowserKitDriver/zipball/10e67fb4a295efcd62ea0bf16025a85ea19534fb",
-                "reference": "10e67fb4a295efcd62ea0bf16025a85ea19534fb",
+                "url": "https://api.github.com/repos/minkphp/MinkBrowserKitDriver/zipball/1b9a7ce903cfdaaec5fb32bfdbb26118343662eb",
+                "reference": "1b9a7ce903cfdaaec5fb32bfdbb26118343662eb",
                 "shasum": ""
             },
             "require": {
                 "behat/mink": "^1.7.1@dev",
                 "php": ">=5.3.6",
-                "symfony/browser-kit": "~2.3|~3.0",
-                "symfony/dom-crawler": "~2.3|~3.0"
+                "symfony/browser-kit": "~2.3|~3.0|~4.0",
+                "symfony/dom-crawler": "~2.3|~3.0|~4.0"
             },
             "require-dev": {
-                "silex/silex": "~1.2",
-                "symfony/phpunit-bridge": "~2.7|~3.0"
+                "mink/driver-testsuite": "dev-master",
+                "symfony/http-kernel": "~2.3|~3.0|~4.0"
             },
             "type": "mink-driver",
             "extra": {
                 "browser",
                 "testing"
             ],
-            "time": "2016-03-05T08:59:47+00:00"
+            "time": "2018-05-02T09:25:31+00:00"
         },
         {
             "name": "behat/mink-extension",
         },
         {
             "name": "guzzlehttp/guzzle",
-            "version": "6.3.0",
+            "version": "6.3.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/guzzle.git",
-                "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699"
+                "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/f4db5a78a5ea468d4831de7f0bf9d9415e348699",
-                "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699",
+                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/407b0cb880ace85c9b63c5f9551db498cb2d50ba",
+                "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba",
                 "shasum": ""
             },
             "require": {
             },
             "require-dev": {
                 "ext-curl": "*",
-                "phpunit/phpunit": "^4.0 || ^5.0",
+                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
                 "psr/log": "^1.0"
             },
             "suggest": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "6.2-dev"
+                    "dev-master": "6.3-dev"
                 }
             },
             "autoload": {
                 "rest",
                 "web service"
             ],
-            "time": "2017-06-22T18:50:49+00:00"
+            "time": "2018-04-22T15:46:56+00:00"
         },
         {
             "name": "guzzlehttp/promises",
         },
         {
             "name": "moodlehq/behat-extension",
-            "version": "v3.35.1",
+            "version": "v3.36.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/moodlehq/moodle-behat-extension.git",
-                "reference": "e6e92fd551185f73603bad5694e854f3f6906e0e"
+                "reference": "ba8c4b8b323e05f7af128604f3f3dc60c953135a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/e6e92fd551185f73603bad5694e854f3f6906e0e",
-                "reference": "e6e92fd551185f73603bad5694e854f3f6906e0e",
+                "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/ba8c4b8b323e05f7af128604f3f3dc60c953135a",
+                "reference": "ba8c4b8b323e05f7af128604f3f3dc60c953135a",
                 "shasum": ""
             },
             "require": {
             },
             "notification-url": "https://packagist.org/downloads/",
             "license": [
-                "GPLv3"
+                "GPL-3.0-or-later"
             ],
             "authors": [
                 {
                 "Behat",
                 "moodle"
             ],
-            "time": "2018-01-24T14:09:40+00:00"
+            "time": "2018-02-04T18:04:02+00:00"
         },
         {
             "name": "myclabs/deep-copy",
         },
         {
             "name": "phpspec/prophecy",
-            "version": "1.7.5",
+            "version": "1.7.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpspec/prophecy.git",
-                "reference": "dfd6be44111a7c41c2e884a336cc4f461b3b2401"
+                "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/dfd6be44111a7c41c2e884a336cc4f461b3b2401",
-                "reference": "dfd6be44111a7c41c2e884a336cc4f461b3b2401",
+                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/33a7e3c4fda54e912ff6338c48823bd5c0f0b712",
+                "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712",
                 "shasum": ""
             },
             "require": {
                 "doctrine/instantiator": "^1.0.2",
                 "php": "^5.3|^7.0",
                 "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0",
-                "sebastian/comparator": "^1.1|^2.0",
+                "sebastian/comparator": "^1.1|^2.0|^3.0",
                 "sebastian/recursion-context": "^1.0|^2.0|^3.0"
             },
             "require-dev": {
                 "spy",
                 "stub"
             ],
-            "time": "2018-02-19T10:16:54+00:00"
+            "time": "2018-04-18T13:57:24+00:00"
         },
         {
             "name": "phpunit/dbunit",
         },
         {
             "name": "phpunit/php-code-coverage",
-            "version": "5.3.0",
+            "version": "5.3.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "661f34d0bd3f1a7225ef491a70a020ad23a057a1"
+                "reference": "c89677919c5dd6d3b3852f230a663118762218ac"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/661f34d0bd3f1a7225ef491a70a020ad23a057a1",
-                "reference": "661f34d0bd3f1a7225ef491a70a020ad23a057a1",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c89677919c5dd6d3b3852f230a663118762218ac",
+                "reference": "c89677919c5dd6d3b3852f230a663118762218ac",
                 "shasum": ""
             },
             "require": {
                 "testing",
                 "xunit"
             ],
-            "time": "2017-12-06T09:29:45+00:00"
+            "time": "2018-04-06T15:36:58+00:00"
         },
         {
             "name": "phpunit/php-file-iterator",
         },
         {
             "name": "phpunit/phpunit",
-            "version": "6.5.7",
+            "version": "6.5.8",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "6bd77b57707c236833d2b57b968e403df060c9d9"
+                "reference": "4f21a3c6b97c42952fd5c2837bb354ec0199b97b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6bd77b57707c236833d2b57b968e403df060c9d9",
-                "reference": "6bd77b57707c236833d2b57b968e403df060c9d9",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4f21a3c6b97c42952fd5c2837bb354ec0199b97b",
+                "reference": "4f21a3c6b97c42952fd5c2837bb354ec0199b97b",
                 "shasum": ""
             },
             "require": {
                 "testing",
                 "xunit"
             ],
-            "time": "2018-02-26T07:01:09+00:00"
+            "time": "2018-04-10T11:38:34+00:00"
         },
         {
             "name": "phpunit/phpunit-mock-objects",
-            "version": "5.0.6",
+            "version": "5.0.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
-                "reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf"
+                "reference": "3eaf040f20154d27d6da59ca2c6e28ac8fd56dce"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/33fd41a76e746b8fa96d00b49a23dadfa8334cdf",
-                "reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/3eaf040f20154d27d6da59ca2c6e28ac8fd56dce",
+                "reference": "3eaf040f20154d27d6da59ca2c6e28ac8fd56dce",
                 "shasum": ""
             },
             "require": {
                 "mock",
                 "xunit"
             ],
-            "time": "2018-01-06T05:45:45+00:00"
+            "time": "2018-05-29T13:50:43+00:00"
         },
         {
             "name": "psr/container",
         },
         {
             "name": "symfony/browser-kit",
-            "version": "v3.4.6",
+            "version": "v3.4.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/browser-kit.git",
-                "reference": "490f27762705c8489bd042fe3e9377a191dba9b4"
+                "reference": "840bb6f0d5b3701fd768b68adf7193c2d0f98f79"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/490f27762705c8489bd042fe3e9377a191dba9b4",
-                "reference": "490f27762705c8489bd042fe3e9377a191dba9b4",
+                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/840bb6f0d5b3701fd768b68adf7193c2d0f98f79",
+                "reference": "840bb6f0d5b3701fd768b68adf7193c2d0f98f79",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony BrowserKit Component",
             "homepage": "https://symfony.com",
-            "time": "2018-01-03T07:37:34+00:00"
+            "time": "2018-03-19T22:32:39+00:00"
         },
         {
             "name": "symfony/class-loader",
-            "version": "v3.4.6",
+            "version": "v3.4.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/class-loader.git",
         },
         {
             "name": "symfony/config",
-            "version": "v3.4.6",
+            "version": "v3.4.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/config.git",
-                "reference": "05e10567b529476a006b00746c5f538f1636810e"
+                "reference": "73e055cf2e6467715f187724a0347ea32079967c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/config/zipball/05e10567b529476a006b00746c5f538f1636810e",
-                "reference": "05e10567b529476a006b00746c5f538f1636810e",
+                "url": "https://api.github.com/repos/symfony/config/zipball/73e055cf2e6467715f187724a0347ea32079967c",
+                "reference": "73e055cf2e6467715f187724a0347ea32079967c",
                 "shasum": ""
             },
             "require": {
                 "php": "^5.5.9|>=7.0.8",
-                "symfony/filesystem": "~2.8|~3.0|~4.0"
+                "symfony/filesystem": "~2.8|~3.0|~4.0",
+                "symfony/polyfill-ctype": "~1.8"
             },
             "conflict": {
                 "symfony/dependency-injection": "<3.3",
             ],
             "description": "Symfony Config Component",
             "homepage": "https://symfony.com",
-            "time": "2018-02-14T10:03:57+00:00"
+            "time": "2018-05-14T16:49:53+00:00"
         },
         {
             "name": "symfony/console",
-            "version": "v3.3.16",
+            "version": "v3.3.17",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
         },
         {
             "name": "symfony/css-selector",
-            "version": "v3.4.6",
+            "version": "v3.4.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/css-selector.git",
-                "reference": "544655f1fc078a9cd839fdda2b7b1e64627c826a"
+                "reference": "d2ce52290b648ae33b5301d09bc14ee378612914"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/css-selector/zipball/544655f1fc078a9cd839fdda2b7b1e64627c826a",
-                "reference": "544655f1fc078a9cd839fdda2b7b1e64627c826a",
+                "url": "https://api.github.com/repos/symfony/css-selector/zipball/d2ce52290b648ae33b5301d09bc14ee378612914",
+                "reference": "d2ce52290b648ae33b5301d09bc14ee378612914",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony CssSelector Component",
             "homepage": "https://symfony.com",
-            "time": "2018-02-03T14:55:07+00:00"
+            "time": "2018-05-16T12:49:49+00:00"
         },
         {
             "name": "symfony/debug",
-            "version": "v3.4.6",
+            "version": "v3.4.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/debug.git",
-                "reference": "9b1071f86e79e1999b3d3675d2e0e7684268b9bc"
+                "reference": "b28fd73fefbac341f673f5efd707d539d6a19f68"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/debug/zipball/9b1071f86e79e1999b3d3675d2e0e7684268b9bc",
-                "reference": "9b1071f86e79e1999b3d3675d2e0e7684268b9bc",
+                "url": "https://api.github.com/repos/symfony/debug/zipball/b28fd73fefbac341f673f5efd707d539d6a19f68",
+                "reference": "b28fd73fefbac341f673f5efd707d539d6a19f68",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Debug Component",
             "homepage": "https://symfony.com",
-            "time": "2018-02-28T21:49:22+00:00"
+            "time": "2018-05-16T14:03:39+00:00"
         },
         {
             "name": "symfony/dependency-injection",
-            "version": "v3.3.16",
+            "version": "v3.3.17",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dependency-injection.git",
         },
         {
             "name": "symfony/dom-crawler",
-            "version": "v3.4.6",
+            "version": "v3.4.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dom-crawler.git",
-                "reference": "2bb5d3101cc01f4fe580e536daf4f1959bc2d24d"
+                "reference": "201b210fafcdd193c1e45b2994bf7133fb6263e8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/2bb5d3101cc01f4fe580e536daf4f1959bc2d24d",
-                "reference": "2bb5d3101cc01f4fe580e536daf4f1959bc2d24d",
+                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/201b210fafcdd193c1e45b2994bf7133fb6263e8",
+                "reference": "201b210fafcdd193c1e45b2994bf7133fb6263e8",
                 "shasum": ""
             },
             "require": {
                 "php": "^5.5.9|>=7.0.8",
+                "symfony/polyfill-ctype": "~1.8",
                 "symfony/polyfill-mbstring": "~1.0"
             },
             "require-dev": {
             ],
             "description": "Symfony DomCrawler Component",
             "homepage": "https://symfony.com",
-            "time": "2018-02-22T10:48:49+00:00"
+            "time": "2018-05-01T22:53:27+00:00"
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v3.4.6",
+            "version": "v3.4.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "58990682ac3fdc1f563b7e705452921372aad11d"
+                "reference": "fdd5abcebd1061ec647089c6c41a07ed60af09f8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/58990682ac3fdc1f563b7e705452921372aad11d",
-                "reference": "58990682ac3fdc1f563b7e705452921372aad11d",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/fdd5abcebd1061ec647089c6c41a07ed60af09f8",
+                "reference": "fdd5abcebd1061ec647089c6c41a07ed60af09f8",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony EventDispatcher Component",
             "homepage": "https://symfony.com",
-            "time": "2018-02-14T10:03:57+00:00"
+            "time": "2018-04-06T07:35:25+00:00"
         },
         {
             "name": "symfony/filesystem",
-            "version": "v3.4.6",
+            "version": "v3.4.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/filesystem.git",
-                "reference": "253a4490b528597aa14d2bf5aeded6f5e5e4a541"
+                "reference": "8e03ca3fa52a0f56b87506f38cf7bd3f9442b3a0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/filesystem/zipball/253a4490b528597aa14d2bf5aeded6f5e5e4a541",
-                "reference": "253a4490b528597aa14d2bf5aeded6f5e5e4a541",
+                "url": "https://api.github.com/repos/symfony/filesystem/zipball/8e03ca3fa52a0f56b87506f38cf7bd3f9442b3a0",
+                "reference": "8e03ca3fa52a0f56b87506f38cf7bd3f9442b3a0",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.5.9|>=7.0.8"
+                "php": "^5.5.9|>=7.0.8",
+                "symfony/polyfill-ctype": "~1.8"
             },
             "type": "library",
             "extra": {
             ],
             "description": "Symfony Filesystem Component",
             "homepage": "https://symfony.com",
-            "time": "2018-02-22T10:48:49+00:00"
+            "time": "2018-05-16T08:49:21+00:00"
+        },
+        {
+            "name": "symfony/polyfill-ctype",
+            "version": "v1.8.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-ctype.git",
+                "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/7cc359f1b7b80fc25ed7796be7d96adc9b354bae",
+                "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.8-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Ctype\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                },
+                {
+                    "name": "Gert de Pagter",
+                    "email": "BackEndTea@gmail.com"
+                }
+            ],
+            "description": "Symfony polyfill for ctype functions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "ctype",
+                "polyfill",
+                "portable"
+            ],
+            "time": "2018-04-30T19:57:29+00:00"
         },
         {
             "name": "symfony/polyfill-mbstring",
-            "version": "v1.7.0",
+            "version": "v1.8.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-mbstring.git",
-                "reference": "78be803ce01e55d3491c1397cf1c64beb9c1b63b"
+                "reference": "3296adf6a6454a050679cde90f95350ad604b171"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/78be803ce01e55d3491c1397cf1c64beb9c1b63b",
-                "reference": "78be803ce01e55d3491c1397cf1c64beb9c1b63b",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/3296adf6a6454a050679cde90f95350ad604b171",
+                "reference": "3296adf6a6454a050679cde90f95350ad604b171",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.7-dev"
+                    "dev-master": "1.8-dev"
                 }
             },
             "autoload": {
                 "portable",
                 "shim"
             ],
-            "time": "2018-01-30T19:27:44+00:00"
+            "time": "2018-04-26T10:06:28+00:00"
         },
         {
             "name": "symfony/process",
-            "version": "v2.8.36",
+            "version": "v2.8.41",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/process.git",
-                "reference": "756f614c5061729ea245ac6717231f7e3bfb74f9"
+                "reference": "713952f2ccbcc8342ecdbe1cb313d3e2da8aad28"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/process/zipball/756f614c5061729ea245ac6717231f7e3bfb74f9",
-                "reference": "756f614c5061729ea245ac6717231f7e3bfb74f9",
+                "url": "https://api.github.com/repos/symfony/process/zipball/713952f2ccbcc8342ecdbe1cb313d3e2da8aad28",
+                "reference": "713952f2ccbcc8342ecdbe1cb313d3e2da8aad28",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Process Component",
             "homepage": "https://symfony.com",
-            "time": "2018-02-12T17:44:58+00:00"
+            "time": "2018-05-15T21:17:45+00:00"
         },
         {
             "name": "symfony/translation",
-            "version": "v3.3.16",
+            "version": "v3.3.17",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/translation.git",
         },
         {
             "name": "symfony/yaml",
-            "version": "v3.3.16",
+            "version": "v3.3.17",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/yaml.git",
index f66cd8c..4390d5a 100644 (file)
@@ -139,7 +139,7 @@ class core_course_management_renderer extends plugin_renderer_base {
         $listing = coursecat::get(0)->get_children();
 
         $attributes = array(
-            'class' => 'ml-1',
+            'class' => 'ml',
             'role' => 'tree',
             'aria-labelledby' => 'category-listing-title'
         );
@@ -530,7 +530,7 @@ class core_course_management_renderer extends plugin_renderer_base {
             array('id' => 'course-listing-title', 'tabindex' => '0'));
         $html .= $this->course_listing_actions($category, $course, $perpage);
         $html .= $this->listing_pagination($category, $page, $perpage, false, $viewmode);
-        $html .= html_writer::start_tag('ul', array('class' => 'ml-1 course-list', 'role' => 'group'));
+        $html .= html_writer::start_tag('ul', array('class' => 'ml course-list', 'role' => 'group'));
         foreach ($category->get_courses($options) as $listitem) {
             $html .= $this->course_listitem($category, $listitem, $courseid);
         }
@@ -1117,7 +1117,7 @@ class core_course_management_renderer extends plugin_renderer_base {
         ));
         $html .= html_writer::tag('h3', get_string('courses'));
         $html .= $this->search_pagination($totalcourses, $page, $perpage);
-        $html .= html_writer::start_tag('ul', array('class' => 'ml-1'));
+        $html .= html_writer::start_tag('ul', array('class' => 'ml'));
         foreach ($courses as $listitem) {
             $i++;
             if ($i == $totalcourses) {
index 68767ff..b3cd970 100644 (file)
@@ -2311,6 +2311,208 @@ class core_course_renderer extends plugin_renderer_base {
 
         return $hubdescription;
     }
+
+    /**
+     * Output frontpage summary text and frontpage modules (stored as section 1 in site course)
+     *
+     * This may be disabled in settings
+     *
+     * @return string
+     */
+    public function frontpage_section1() {
+        global $SITE, $USER;
+
+        $output = '';
+        $editing = $this->page->user_is_editing();
+
+        if ($editing) {
+            // Make sure section with number 1 exists.
+            course_create_sections_if_missing($SITE, 1);
+        }
+
+        $modinfo = get_fast_modinfo($SITE);
+        $section = $modinfo->get_section_info(1);
+        if (($section && (!empty($modinfo->sections[1]) or !empty($section->summary))) or $editing) {
+            $output .= $this->box_start('generalbox sitetopic');
+
+            // If currently moving a file then show the current clipboard.
+            if (ismoving($SITE->id)) {
+                $stractivityclipboard = strip_tags(get_string('activityclipboard', '', $USER->activitycopyname));
+                $output .= '<p><font size="2">';
+                $cancelcopyurl = new moodle_url('/course/mod.php', ['cancelcopy' => 'true', 'sesskey' => sesskey()]);
+                $output .= "$stractivityclipboard&nbsp;&nbsp;(" . html_writer::link($cancelcopyurl, get_string('cancel')) .')';
+                $output .= '</font></p>';
+            }
+
+            $context = context_course::instance(SITEID);
+
+            // If the section name is set we show it.
+            if (trim($section->name) !== '') {
+                $output .= $this->heading(
+                    format_string($section->name, true, array('context' => $context)),
+                    2,
+                    'sectionname'
+                );
+            }
+
+            $summarytext = file_rewrite_pluginfile_urls($section->summary,
+                'pluginfile.php',
+                $context->id,
+                'course',
+                'section',
+                $section->id);
+            $summaryformatoptions = new stdClass();
+            $summaryformatoptions->noclean = true;
+            $summaryformatoptions->overflowdiv = true;
+
+            $output .= format_text($summarytext, $section->summaryformat, $summaryformatoptions);
+
+            if ($editing && has_capability('moodle/course:update', $context)) {
+                $streditsummary = get_string('editsummary');
+                $editsectionurl = new moodle_url('/course/editsection.php', ['id' => $section->id]);
+                $output .= html_writer::link($editsectionurl, $this->pix_icon('t/edit', $streditsummary)) .
+                    "<br /><br />";
+            }
+
+            $output .= $this->course_section_cm_list($SITE, $section);
+
+            $output .= $this->course_section_add_cm_control($SITE, $section->section);
+            $output .= $this->box_end();
+        }
+
+        return $output;
+    }
+
+    /**
+     * Output news for the frontpage (extract from site-wide news forum)
+     *
+     * @param stdClass $newsforum record from db table 'forum' that represents the site news forum
+     * @return string
+     */
+    protected function frontpage_news($newsforum) {
+        global $CFG, $SITE, $SESSION, $USER;
+        require_once($CFG->dirroot .'/mod/forum/lib.php');
+
+        $output = '';
+
+        if (isloggedin()) {
+            $SESSION->fromdiscussion = $CFG->wwwroot;
+            $subtext = '';
+            if (\mod_forum\subscriptions::is_subscribed($USER->id, $newsforum)) {
+                if (!\mod_forum\subscriptions::is_forcesubscribed($newsforum)) {
+                    $subtext = get_string('unsubscribe', 'forum');
+                }
+            } else {
+                $subtext = get_string('subscribe', 'forum');
+            }
+            $suburl = new moodle_url('/mod/forum/subscribe.php', array('id' => $newsforum->id, 'sesskey' => sesskey()));
+            $output .= html_writer::tag('div', html_writer::link($suburl, $subtext), array('class' => 'subscribelink'));
+        }
+
+        ob_start();
+        forum_print_latest_discussions($SITE, $newsforum, $SITE->newsitems, 'plain', 'p.modified DESC');
+        $output .= ob_get_contents();
+        ob_end_clean();
+
+        return $output;
+    }
+
+    /**
+     * Renders part of frontpage with a skip link (i.e. "My courses", "Site news", etc.)
+     *
+     * @param string $skipdivid
+     * @param string $contentsdivid
+     * @param string $header Header of the part
+     * @param string $contents Contents of the part
+     * @return string
+     */
+    protected function frontpage_part($skipdivid, $contentsdivid, $header, $contents) {
+        $output = html_writer::link('#' . $skipdivid,
+            get_string('skipa', 'access', core_text::strtolower(strip_tags($header))),
+            array('class' => 'skip-block skip'));
+
+        // Wrap frontpage part in div container.
+        $output .= html_writer::start_tag('div', array('id' => $contentsdivid));
+        $output .= $this->heading($header);
+
+        $output .= $contents;
+
+        // End frontpage part div container.
+        $output .= html_writer::end_tag('div');
+
+        $output .= html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => $skipdivid));
+        return $output;
+    }
+
+    /**
+     * Outputs contents for frontpage as configured in $CFG->frontpage or $CFG->frontpageloggedin
+     *
+     * @return string
+     */
+    public function frontpage() {
+        global $CFG, $SITE;
+
+        $output = '';
+
+        if (isloggedin() and !isguestuser() and isset($CFG->frontpageloggedin)) {
+            $frontpagelayout = $CFG->frontpageloggedin;
+        } else {
+            $frontpagelayout = $CFG->frontpage;
+        }
+
+        foreach (explode(',', $frontpagelayout) as $v) {
+            switch ($v) {
+                // Display the main part of the front page.
+                case FRONTPAGENEWS:
+                    if ($SITE->newsitems) {
+                        // Print forums only when needed.
+                        require_once($CFG->dirroot .'/mod/forum/lib.php');
+                        if (($newsforum = forum_get_course_forum($SITE->id, 'news')) &&
+                                ($forumcontents = $this->frontpage_news($newsforum))) {
+                            $newsforumcm = get_fast_modinfo($SITE)->instances['forum'][$newsforum->id];
+                            $output .= $this->frontpage_part('skipsitenews', 'site-news-forum',
+                                $newsforumcm->get_formatted_name(), $forumcontents);
+                        }
+                    }
+                    break;
+
+                case FRONTPAGEENROLLEDCOURSELIST:
+                    $mycourseshtml = $this->frontpage_my_courses();
+                    if (!empty($mycourseshtml)) {
+                        $output .= $this->frontpage_part('skipmycourses', 'frontpage-course-list',
+                            get_string('mycourses'), $mycourseshtml);
+                        break;
+                    }
+                    // No "break" here. If there are no enrolled courses - continue to 'Available courses'.
+
+                case FRONTPAGEALLCOURSELIST:
+                    $availablecourseshtml = $this->frontpage_available_courses();
+                    if (!empty($availablecourseshtml)) {
+                        $output .= $this->frontpage_part('skipavailablecourses', 'frontpage-available-course-list',
+                            get_string('availablecourses'), $availablecourseshtml);
+                    }
+                    break;
+
+                case FRONTPAGECATEGORYNAMES:
+                    $output .= $this->frontpage_part('skipcategories', 'frontpage-category-names',
+                        get_string('categories'), $this->frontpage_categories_list());
+                    break;
+
+                case FRONTPAGECATEGORYCOMBO:
+                    $output .= $this->frontpage_part('skipcourses', 'frontpage-category-combo',
+                        get_string('courses'), $this->frontpage_combo_list());
+                    break;
+
+                case FRONTPAGECOURSESEARCH:
+                    $output .= $this->box($this->course_search_form('', 'short'), 'mdl-align');
+                    break;
+
+            }
+            $output .= '<br />';
+        }
+
+        return $output;
+    }
 }
 
 /**
index 502093c..01b2b5f 100644 (file)
--- a/index.php
+++ b/index.php
@@ -111,215 +111,27 @@ $PAGE->set_heading($SITE->fullname);
 $courserenderer = $PAGE->get_renderer('core', 'course');
 echo $OUTPUT->header();
 
-// Print Section or custom info.
 $siteformatoptions = course_get_format($SITE)->get_format_options();
 $modinfo = get_fast_modinfo($SITE);
-$modnames = get_module_types_names();
-$modnamesplural = get_module_types_names(true);
 $modnamesused = $modinfo->get_used_module_names();
-$mods = $modinfo->get_cms();
 
+// Print Section or custom info.
 if (!empty($CFG->customfrontpageinclude)) {
+    // Pre-fill some variables that custom front page might use.
+    $modnames = get_module_types_names();
+    $modnamesplural = get_module_types_names(true);
+    $mods = $modinfo->get_cms();
+
     include($CFG->customfrontpageinclude);
 
 } else if ($siteformatoptions['numsections'] > 0) {
-    if ($editing) {
-        // Make sure section with number 1 exists.
-        course_create_sections_if_missing($SITE, 1);
-        // Re-request modinfo in case section was created.
-        $modinfo = get_fast_modinfo($SITE);
-    }
-    $section = $modinfo->get_section_info(1);
-    if (($section && (!empty($modinfo->sections[1]) or !empty($section->summary))) or $editing) {
-        echo $OUTPUT->box_start('generalbox sitetopic');
-
-        // If currently moving a file then show the current clipboard.
-        if (ismoving($SITE->id)) {
-            $stractivityclipboard = strip_tags(get_string('activityclipboard', '', $USER->activitycopyname));
-            echo '<p><font size="2">';
-            echo "$stractivityclipboard&nbsp;&nbsp;(<a href=\"course/mod.php?cancelcopy=true&amp;sesskey=".sesskey()."\">";
-            echo get_string('cancel') . '</a>)';
-            echo '</font></p>';
-        }
-
-        $context = context_course::instance(SITEID);
-
-        // If the section name is set we show it.
-        if (trim($section->name) !== '') {
-            echo $OUTPUT->heading(
-                format_string($section->name, true, array('context' => $context)),
-                2,
-                'sectionname'
-            );
-        }
-
-        $summarytext = file_rewrite_pluginfile_urls($section->summary,
-            'pluginfile.php',
-            $context->id,
-            'course',
-            'section',
-            $section->id);
-        $summaryformatoptions = new stdClass();
-        $summaryformatoptions->noclean = true;
-        $summaryformatoptions->overflowdiv = true;
-
-        echo format_text($summarytext, $section->summaryformat, $summaryformatoptions);
-
-        if ($editing && has_capability('moodle/course:update', $context)) {
-            $streditsummary = get_string('editsummary');
-            echo "<a title=\"$streditsummary\" " .
-                 " href=\"course/editsection.php?id=$section->id\">" . $OUTPUT->pix_icon('t/edit', $streditsummary) .
-                 "</a><br /><br />";
-        }
-
-        $courserenderer = $PAGE->get_renderer('core', 'course');
-        echo $courserenderer->course_section_cm_list($SITE, $section);
-
-        echo $courserenderer->course_section_add_cm_control($SITE, $section->section);
-        echo $OUTPUT->box_end();
-    }
+    echo $courserenderer->frontpage_section1();
 }
 // Include course AJAX.
 include_course_ajax($SITE, $modnamesused);
 
-if (isloggedin() and !isguestuser() and isset($CFG->frontpageloggedin)) {
-    $frontpagelayout = $CFG->frontpageloggedin;
-} else {
-    $frontpagelayout = $CFG->frontpage;
-}
-
-foreach (explode(',', $frontpagelayout) as $v) {
-    switch ($v) {
-        // Display the main part of the front page.
-        case FRONTPAGENEWS:
-            if ($SITE->newsitems) {
-                // Print forums only when needed.
-                require_once($CFG->dirroot .'/mod/forum/lib.php');
-
-                if (! $newsforum = forum_get_course_forum($SITE->id, 'news')) {
-                    print_error('cannotfindorcreateforum', 'forum');
-                }
-
-                // Fetch news forum context for proper filtering to happen.
-                $newsforumcm = get_coursemodule_from_instance('forum', $newsforum->id, $SITE->id, false, MUST_EXIST);
-                $newsforumcontext = context_module::instance($newsforumcm->id, MUST_EXIST);
-
-                $forumname = format_string($newsforum->name, true, array('context' => $newsforumcontext));
-                echo html_writer::link('#skipsitenews',
-                    get_string('skipa', 'access', core_text::strtolower(strip_tags($forumname))),
-                    array('class' => 'skip-block skip'));
-
-                // Wraps site news forum in div container.
-                echo html_writer::start_tag('div', array('id' => 'site-news-forum'));
-
-                if (isloggedin()) {
-                    $SESSION->fromdiscussion = $CFG->wwwroot;
-                    $subtext = '';
-                    if (\mod_forum\subscriptions::is_subscribed($USER->id, $newsforum)) {
-                        if (!\mod_forum\subscriptions::is_forcesubscribed($newsforum)) {
-                            $subtext = get_string('unsubscribe', 'forum');
-                        }
-                    } else {
-                        $subtext = get_string('subscribe', 'forum');
-                    }
-                    echo $OUTPUT->heading($forumname);
-                    $suburl = new moodle_url('/mod/forum/subscribe.php', array('id' => $newsforum->id, 'sesskey' => sesskey()));
-                    echo html_writer::tag('div', html_writer::link($suburl, $subtext), array('class' => 'subscribelink'));
-                } else {
-                    echo $OUTPUT->heading($forumname);
-                }
-
-                forum_print_latest_discussions($SITE, $newsforum, $SITE->newsitems, 'plain', 'p.modified DESC');
-
-                // End site news forum div container.
-                echo html_writer::end_tag('div');
+echo $courserenderer->frontpage();
 
-                echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipsitenews'));
-            }
-        break;
-
-        case FRONTPAGEENROLLEDCOURSELIST:
-            $mycourseshtml = $courserenderer->frontpage_my_courses();
-            if (!empty($mycourseshtml)) {
-                echo html_writer::link('#skipmycourses',
-                    get_string('skipa', 'access', core_text::strtolower(get_string('mycourses'))),
-                    array('class' => 'skip skip-block'));
-
-                // Wrap frontpage course list in div container.
-                echo html_writer::start_tag('div', array('id' => 'frontpage-course-list'));
-
-                echo $OUTPUT->heading(get_string('mycourses'));
-                echo $mycourseshtml;
-
-                // End frontpage course list div container.
-                echo html_writer::end_tag('div');
-
-                echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipmycourses'));
-                break;
-            }
-            // No "break" here. If there are no enrolled courses - continue to 'Available courses'.
-
-        case FRONTPAGEALLCOURSELIST:
-            $availablecourseshtml = $courserenderer->frontpage_available_courses();
-            if (!empty($availablecourseshtml)) {
-                echo html_writer::link('#skipavailablecourses',
-                    get_string('skipa', 'access', core_text::strtolower(get_string('availablecourses'))),
-                    array('class' => 'skip skip-block'));
-
-                // Wrap frontpage course list in div container.
-                echo html_writer::start_tag('div', array('id' => 'frontpage-course-list'));
-
-                echo $OUTPUT->heading(get_string('availablecourses'));
-                echo $availablecourseshtml;
-
-                // End frontpage course list div container.
-                echo html_writer::end_tag('div');
-
-                echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipavailablecourses'));
-            }
-        break;
-
-        case FRONTPAGECATEGORYNAMES:
-            echo html_writer::link('#skipcategories',
-                get_string('skipa', 'access', core_text::strtolower(get_string('categories'))),
-                array('class' => 'skip skip-block'));
-
-            // Wrap frontpage category names in div container.
-            echo html_writer::start_tag('div', array('id' => 'frontpage-category-names'));
-
-            echo $OUTPUT->heading(get_string('categories'));
-            echo $courserenderer->frontpage_categories_list();
-
-            // End frontpage category names div container.
-            echo html_writer::end_tag('div');
-
-            echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipcategories'));
-        break;
-
-        case FRONTPAGECATEGORYCOMBO:
-            echo html_writer::link('#skipcourses',
-                get_string('skipa', 'access', core_text::strtolower(get_string('courses'))),
-                array('class' => 'skip skip-block'));
-
-            // Wrap frontpage category combo in div container.
-            echo html_writer::start_tag('div', array('id' => 'frontpage-category-combo'));
-
-            echo $OUTPUT->heading(get_string('courses'));
-            echo $courserenderer->frontpage_combo_list();
-
-            // End frontpage category combo div container.
-            echo html_writer::end_tag('div');
-
-            echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipcourses'));
-        break;
-
-        case FRONTPAGECOURSESEARCH:
-            echo $OUTPUT->box($courserenderer->course_search_form('', 'short'), 'mdl-align');
-        break;
-
-    }
-    echo '<br />';
-}
 if ($editing && has_capability('moodle/course:create', context_system::instance())) {
     echo $courserenderer->add_new_course_button();
 }
index 17be075..aa79e8e 100644 (file)
@@ -825,6 +825,7 @@ $string['order3'] = 'Third';
 $string['order4'] = 'Fourth';
 $string['outgoingmailconfig'] = 'Outgoing mail configuration';
 $string['overridetossl'] = 'HTTPS for logins has now been deprecated. This instance is now forced to SSL. To remedy this warning change your wwwroot in config.php to https://';
+$string['pageinfodebugsummary'] = 'This page is: {$a}';
 $string['passwordchangelogout'] = 'Log out after password change';
 $string['passwordchangelogout_desc'] = 'If enabled, when a password is changed, all browser sessions are terminated, apart from the one in which the new password is specified. (This setting does not affect password changes via bulk user upload.)';
 $string['passwordchangetokendeletion'] = 'Remove web service access tokens after password change';
index e9f514b..dc711e8 100644 (file)
@@ -120,6 +120,8 @@ $string['configrestoreroleassignments'] = 'If enabled by default roles assignmen
 $string['configrestoreuserscompletion'] = 'If enabled user completion information will be restored by default if it was included in the backup.';
 $string['configrestoreusers'] = 'Sets the default for whether to restore users if they were included in the backup.';
 $string['confirmcancel'] = 'Cancel backup';
+$string['confirmcancelrestore'] = 'Cancel restore';
+$string['confirmcancelimport'] = 'Cancel import';
 $string['confirmcancelquestion'] = 'Are you sure you wish to cancel?
 Any information you have entered will be lost.';
 $string['confirmcancelyes'] = 'Cancel';
@@ -198,6 +200,7 @@ $string['importcurrentstage2'] = 'Schema settings';
 $string['importcurrentstage4'] = 'Confirmation and review';
 $string['importcurrentstage8'] = 'Perform import';
 $string['importcurrentstage16'] = 'Complete';
+$string['importrootsettings'] = 'Import settings';
 $string['importsettings'] = 'General import settings';
 $string['importsuccess'] = 'Import complete. Click continue to return to the course.';
 $string['includeactivities'] = 'Include:';
index e83cbf3..efd628b 100644 (file)
@@ -266,7 +266,7 @@ $string['configuration'] = 'Configuration';
 $string['confirm'] = 'Confirm';
 $string['confirmdeletesection'] = 'Are you absolutely sure you want to completely delete "{$a}" and all the activities it contains?';
 $string['confirmed'] = 'Your registration has been confirmed';
-$string['confirmednot'] = 'Your registration has not yet been confirmed!';
+$string['confirmednot'] = 'Your registration has not yet been confirmed. Please check your mailbox for a confirmation email.';
 $string['confirmcheckfull'] = 'Are you absolutely sure you want to confirm {$a} ?';
 $string['confirmcoursemove'] = 'Are you sure you want to move this course ({$a->course}) into this category ({$a->category})?';
 $string['considereddigitalminor'] = 'You are considered to be a digital minor.';
@@ -602,10 +602,13 @@ line at the top of your web browser window.
 
 If you need help, please contact the site administrator,
 {$a->admin}';
+$string['emailconfirmationresend'] = 'Resend confirmation email';
 $string['emailconfirmationsubject'] = '{$a}: account confirmation';
 $string['emailconfirmsent'] = '<p>An email should have been sent to your address at <b>{$a}</b></p>
    <p>It contains easy instructions to complete your registration.</p>
    <p>If you continue to have difficulty, contact the site administrator.</p>';
+$string['emailconfirmsentfailure'] = 'Confirmation email failed to send';
+$string['emailconfirmsentsuccess'] = 'Confirmation email sent successfully';
 $string['emaildigest'] = 'Email digest type';
 $string['emaildigest_help'] = 'This is the daily digest setting that forums will use by default.
 
@@ -1286,7 +1289,7 @@ $string['moveselectedcoursesto'] = 'Move selected courses to...';
 $string['movetoanotherfolder'] = 'Move to another folder';
 $string['moveup'] = 'Move up';
 $string['msnid'] = 'MSN ID';
-$string['mustconfirm'] = 'You need to confirm your login';
+$string['mustconfirm'] = 'You need to confirm your account';
 $string['mustchangepassword'] = 'The new password must be different than the current one';
 $string['mycourses'] = 'My courses';
 $string['myfiles'] = 'My private files';
@@ -1648,6 +1651,7 @@ $string['requestedcourses'] = 'Requested courses';
 $string['requestreason'] = 'Reason for course request';
 $string['required'] = 'Required';
 $string['requirespayment'] = 'This course requires payment for access';
+$string['resendemail'] = 'Resend email';
 $string['reset'] = 'Reset';
 $string['resetcomponent'] = 'Component';
 $string['resetcourse'] = 'Reset course';
index 72d4e97..6c39562 100644 (file)
@@ -86,10 +86,24 @@ class api {
         $issuer = new issuer(0, $record);
         $issuer->create();
 
+        // The Facebook API version.
+        $apiversion = '2.12';
+        // The Graph API URL.
+        $graphurl = 'https://graph.facebook.com/v' . $apiversion;
+        // User information fields that we want to fetch.
+        $infofields = [
+            'id',
+            'first_name',
+            'last_name',
+            'link',
+            'picture.type(large)',
+            'name',
+            'email',
+        ];
         $endpoints = [
-            'authorization_endpoint' => 'https://www.facebook.com/v2.12/dialog/oauth',
-            'token_endpoint' => 'https://graph.facebook.com/v2.12/oauth/access_token',
-            'userinfo_endpoint' => 'https://graph.facebook.com/v2.12/me?fields=id,first_name,last_name,link,picture,name,email'
+            'authorization_endpoint' => sprintf('https://www.facebook.com/v%s/dialog/oauth', $apiversion),
+            'token_endpoint' => $graphurl . '/oauth/access_token',
+            'userinfo_endpoint' => $graphurl . '/me?fields=' . implode(',', $infofields)
         ];
 
         foreach ($endpoints as $name => $url) {
index 0650971..71d15a6 100644 (file)
@@ -360,6 +360,7 @@ class icon_system_fontawesome extends icon_system_font {
             'core:t/message' => 'fa-comment',
             'core:t/more' => 'fa-caret-down',
             'core:t/move' => 'fa-arrows-v',
+            'core:t/online' => 'fa-circle',
             'core:t/passwordunmask-edit' => 'fa-pencil',
             'core:t/passwordunmask-reveal' => 'fa-eye',
             'core:t/portfolioadd' => 'fa-plus',
index 89ba7b5..cc1a1df 100644 (file)
@@ -82,6 +82,13 @@ class core_user {
         'alternatename'
     ];
 
+    /** @var int Indicates that user profile view should be prevented */
+    const VIEWPROFILE_PREVENT = -1;
+    /** @var int Indicates that user profile view should not be prevented */
+    const VIEWPROFILE_DO_NOT_PREVENT = 0;
+    /** @var int Indicates that user profile view should be allowed even if Moodle would prevent it */
+    const VIEWPROFILE_FORCE_ALLOW = 1;
+
     /** @var stdClass keep record of noreply user */
     public static $noreplyuser = false;
 
index 1e6cd3a..2e54708 100644 (file)
@@ -910,7 +910,9 @@ class core_useragent {
      */
     public static function get_browser_version_classes() {
         $classes = array();
-        if (self::is_ie()) {
+        if (self::is_edge()) {
+            $classes[] = 'edge';
+        } else if (self::is_ie()) {
             $classes[] = 'ie';
             for ($i = 12; $i >= 6; $i--) {
                 if (self::check_ie_version($i)) {
@@ -923,12 +925,19 @@ class core_useragent {
             if (preg_match('/rv\:([1-2])\.([0-9])/', self::get_user_agent_string(), $matches)) {
                 $classes[] = "gecko{$matches[1]}{$matches[2]}";
             }
+        } else if (self::is_chrome()) {
+            $classes[] = 'chrome';
+            if (self::is_webkit_android()) {
+                $classes[] = 'android';
+            }
         } else if (self::is_webkit()) {
-            $classes[] = 'safari';
+            if (self::is_safari()) {
+                $classes[] = 'safari';
+            }
             if (self::is_safari_ios()) {
                 $classes[] = 'ios';
             } else if (self::is_webkit_android()) {
-                $classes[] = 'android';
+                $classes[] = 'android'; // Old pre-Chrome android browsers.
             }
         } else if (self::is_opera()) {
             $classes[] = 'opera';
index c3e2e66..b0a6a70 100644 (file)
@@ -293,6 +293,29 @@ class mysqli_native_moodle_database extends moodle_database {
         return $collation;
     }
 
+    /**
+     * Tests if the Antelope file format is still supported or it has been removed.
+     * When removed, only Barracuda file format is supported, given the XtraDB/InnoDB engine.
+     *
+     * @return bool True if the Antelope file format has been removed; otherwise, false.
+     */
+    protected function is_antelope_file_format_no_more_supported() {
+        // Breaking change: Antelope file format support has been removed from both MySQL and MariaDB.
+        // The following InnoDB file format configuration parameters were deprecated and then removed:
+        // - innodb_file_format
+        // - innodb_file_format_check
+        // - innodb_file_format_max
+        // - innodb_large_prefix
+        // 1. MySQL: deprecated in 5.7.7 and removed 8.0.0+.
+        $ismysqlge8d0d0 = ($this->get_dbtype() == 'mysqli') &&
+                version_compare($this->get_server_info()['version'], '8.0.0', '>=');
+        // 2. MariaDB: deprecated in 10.2.0 and removed 10.3.1+.
+        $ismariadbge10d3d1 = ($this->get_dbtype() == 'mariadb') &&
+                version_compare($this->get_server_info()['version'], '10.3.1', '>=');
+
+        return $ismysqlge8d0d0 || $ismariadbge10d3d1;
+    }
+
     /**
      * Get the row format from the database schema.
      *
@@ -307,9 +330,8 @@ class mysqli_native_moodle_database extends moodle_database {
                       FROM INFORMATION_SCHEMA.TABLES
                      WHERE table_schema = DATABASE() AND table_name = '{$this->prefix}$table'";
         } else {
-            if (($this->get_dbtype() == 'mysqli') &&
-                // Breaking change in MySQL 8.0.0+: antelope file format support has been removed.
-                version_compare($this->get_server_info()['version'], '8.0.0', '>=')) {
+            if ($this->is_antelope_file_format_no_more_supported()) {
+                // Breaking change: Antelope file format support has been removed, only Barracuda.
                 $dbengine = $this->get_dbengine();
                 $supporteddbengines = array('InnoDB', 'XtraDB');
                 if (in_array($dbengine, $supporteddbengines)) {
@@ -396,9 +418,8 @@ class mysqli_native_moodle_database extends moodle_database {
      * @return bool True if on otherwise false.
      */
     public function is_large_prefix_enabled() {
-        if (($this->get_dbtype() == 'mysqli') &&
-            // Breaking change since 8.0.0: there is only one file format and 'innodb_large_prefix' has been removed.
-            version_compare($this->get_server_info()['version'], '8.0.0', '>=')) {
+        if ($this->is_antelope_file_format_no_more_supported()) {
+            // Breaking change: Antelope file format support has been removed, only Barracuda.
             return true;
         }
 
index 0f67664..f1c3f75 100644 (file)
@@ -1063,6 +1063,9 @@ class stored_file {
      * @return  string|bool false if a problem occurs, the thumbnail image data otherwise
      */
     public function generate_image_thumbnail($width, $height) {
+        global $CFG;
+        require_once($CFG->libdir . '/gdlib.php');
+
         if (empty($width) or empty($height)) {
             return false;
         }
index cb3ab99..446807e 100644 (file)
@@ -4749,32 +4749,38 @@ function get_complete_user_data($field, $value, $mnethostid = null) {
 function check_password_policy($password, &$errmsg) {
     global $CFG;
 
-    if (empty($CFG->passwordpolicy)) {
-        return true;
-    }
-
-    $errmsg = '';
-    if (core_text::strlen($password) < $CFG->minpasswordlength) {
-        $errmsg .= '<div>'. get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength) .'</div>';
-
-    }
-    if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits) {
-        $errmsg .= '<div>'. get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits) .'</div>';
-
-    }
-    if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower) {
-        $errmsg .= '<div>'. get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower) .'</div>';
-
+    if (!empty($CFG->passwordpolicy)) {
+        $errmsg = '';
+        if (core_text::strlen($password) < $CFG->minpasswordlength) {
+            $errmsg .= '<div>'. get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength) .'</div>';
+        }
+        if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits) {
+            $errmsg .= '<div>'. get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits) .'</div>';
+        }
+        if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower) {
+            $errmsg .= '<div>'. get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower) .'</div>';
+        }
+        if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper) {
+            $errmsg .= '<div>'. get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper) .'</div>';
+        }
+        if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum) {
+            $errmsg .= '<div>'. get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum) .'</div>';
+        }
+        if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars)) {
+            $errmsg .= '<div>'. get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars) .'</div>';
+        }
     }
-    if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper) {
-        $errmsg .= '<div>'. get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper) .'</div>';
 
-    }
-    if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum) {
-        $errmsg .= '<div>'. get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum) .'</div>';
-    }
-    if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars)) {
-        $errmsg .= '<div>'. get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars) .'</div>';
+    // Fire any additional password policy functions from plugins.
+    // Plugin functions should output an error message string or empty string for success.
+    $pluginsfunction = get_plugins_with_function('check_password_policy');
+    foreach ($pluginsfunction as $plugintype => $plugins) {
+        foreach ($plugins as $pluginfunction) {
+            $pluginerr = $pluginfunction($password);
+            if ($pluginerr) {
+                $errmsg .= '<div>'. $pluginerr .'</div>';
+            }
+        }
     }
 
     if ($errmsg == '') {
@@ -9145,6 +9151,9 @@ function get_performance_info() {
     $info['html'] .= '<li class="timeused col-sm-4">'.$info['realtime'].' secs</li> ';
     $info['txt'] .= 'time: '.$info['realtime'].'s ';
 
+    // GET/POST (or NULL if $_SERVER['REQUEST_METHOD'] is undefined) is useful for txt logged information.
+    $info['txt'] .= 'method: ' . ($_SERVER['REQUEST_METHOD'] ?? "NULL") . ' ';
+
     if (function_exists('memory_get_usage')) {
         $info['memory_total'] = memory_get_usage();
         $info['memory_growth'] = memory_get_usage() - $PERF->startmemory;
index 4be95a1..5b50374 100644 (file)
@@ -781,7 +781,8 @@ class core_renderer extends renderer_base {
         $output .= $this->theme_switch_links();
 
         if (!empty($CFG->debugpageinfo)) {
-            $output .= '<div class="performanceinfo pageinfo">This page is: ' . $this->page->debug_summary() . '</div>';
+            $output .= '<div class="performanceinfo pageinfo">' . get_string('pageinfodebugsummary', 'core_admin',
+                $this->page->debug_summary()) . '</div>';
         }
         if (debugging(null, DEBUG_DEVELOPER) and has_capability('moodle/site:config', context_system::instance())) {  // Only in developer mode
             // Add link to profiling report if necessary
index a21ca51..fb10e5a 100644 (file)
@@ -116,7 +116,7 @@ class Hint_ResultPrinter extends PHPUnit\TextUI\ResultPrinter {
             }
         }
 
-        $this->write("\nTo re-run:\n $executable $testName $file\n");
+        $this->write("\nTo re-run:\n $executable \"$testName\" $file\n");
     }
 }
 
index 3fcf992..8c0ad12 100644 (file)
@@ -64,7 +64,7 @@ class core_useragent_testcase extends advanced_testcase {
                     ),
 
                     'versionclasses'                => array(
-                        'safari',
+                        'edge',
                     ),
                 ),
             ),
@@ -97,8 +97,7 @@ class core_useragent_testcase extends advanced_testcase {
                     ),
 
                     'versionclasses'                => array(
-                        'safari',
-                        'android',
+                        'edge'
                     ),
 
                     'devicetype'                    => 'mobile',
@@ -898,8 +897,7 @@ class core_useragent_testcase extends advanced_testcase {
                     'is_webkit'                     => true,
 
                     'versionclasses'                => array(
-                        'safari',
-                        'ios',
+                        'ios'
                     ),
 
                     'devicetype'                    => 'mobile',
@@ -922,7 +920,6 @@ class core_useragent_testcase extends advanced_testcase {
                     'is_webkit'                     => true,
 
                     'versionclasses'                => array(
-                        'safari',
                         'ios',
                     ),
 
@@ -944,7 +941,6 @@ class core_useragent_testcase extends advanced_testcase {
                     'is_webkit'                     => true,
 
                     'versionclasses'                => array(
-                        'safari',
                         'ios',
                     ),
 
@@ -953,7 +949,7 @@ class core_useragent_testcase extends advanced_testcase {
             ),
 
             // Android WebKit 525; G1 Phone.
-            array(
+            'Android WebKit 525; G1 Phone' => array(
                 'Mozilla/5.0 (Linux; U; Android 1.1; en-gb; dream) AppleWebKit/525.10+ (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2 – G1 Phone',
                 array(
                     'is_webkit_android'             => true,
@@ -965,7 +961,6 @@ class core_useragent_testcase extends advanced_testcase {
 
                     'versionclasses'                => array(
                         'android',
-                        'safari',
                     ),
 
                     'devicetype'                    => 'mobile',
@@ -975,7 +970,7 @@ class core_useragent_testcase extends advanced_testcase {
             ),
 
             // Android WebKit 530; Nexus.
-            array(
+            'Android WebKit 530; Nexus' => array(
                 'Mozilla/5.0 (Linux; U; Android 2.1; en-us; Nexus One Build/ERD62) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17 –Nexus',
                 array(
                     'is_webkit_android'             => true,
@@ -988,7 +983,6 @@ class core_useragent_testcase extends advanced_testcase {
 
                     'versionclasses'                => array(
                         'android',
-                        'safari',
                     ),
 
                     'devicetype'                    => 'mobile',
@@ -1017,7 +1011,7 @@ class core_useragent_testcase extends advanced_testcase {
                     ),
 
                     'versionclasses'                => array(
-                        'safari',
+                        'chrome',
                         'android',
                     ),
 
@@ -1045,7 +1039,7 @@ class core_useragent_testcase extends advanced_testcase {
                     ),
 
                     'versionclasses'                => array(
-                        'safari',
+                        'chrome',
                         'android',
                     ),
 
@@ -1066,7 +1060,7 @@ class core_useragent_testcase extends advanced_testcase {
                     'is_webkit'                     => true,
 
                     'versionclasses'                => array(
-                        'safari',
+                        'chrome',
                     ),
                 ),
             ),
@@ -1086,7 +1080,7 @@ class core_useragent_testcase extends advanced_testcase {
                     'is_webkit'                     => true,
 
                     'versionclasses'                => array(
-                        'safari',
+                        'chrome',
                     ),
                 ),
             ),
@@ -1231,7 +1225,6 @@ class core_useragent_testcase extends advanced_testcase {
                     ),
 
                     'versionclasses'                => array(
-                        'safari',
                         'ios',
                     ),
 
index 9ab9abb..923cb43 100644 (file)
@@ -31,6 +31,7 @@ redirect_if_major_upgrade_required();
 
 $testsession = optional_param('testsession', 0, PARAM_INT); // test session works properly
 $anchor      = optional_param('anchor', '', PARAM_RAW);      // Used to restore hash anchor to wantsurl.
+$resendconfirmemail = optional_param('resendconfirmemail', false, PARAM_BOOL);
 
 $context = context_system::instance();
 $PAGE->set_url("$CFG->wwwroot/login/index.php");
@@ -172,7 +173,22 @@ if ($frm and isset($frm->username)) {                             // Login WITH
             $PAGE->set_heading($site->fullname);
             echo $OUTPUT->header();
             echo $OUTPUT->heading(get_string("mustconfirm"));
+            if ($resendconfirmemail) {
+                if (!send_confirmation_email($user)) {
+                    echo $OUTPUT->notification(get_string('emailconfirmsentfailure'), \core\output\notification::NOTIFY_ERROR);
+                } else {
+                    echo $OUTPUT->notification(get_string('emailconfirmsentsuccess'), \core\output\notification::NOTIFY_SUCCESS);
+                }
+            }
             echo $OUTPUT->box(get_string("emailconfirmsent", "", $user->email), "generalbox boxaligncenter");
+            $resendconfirmurl = new moodle_url('/login/index.php',
+                [
+                    'username' => $frm->username,
+                    'password' => $frm->password,
+                    'resendconfirmemail' => true
+                ]
+            );
+            echo $OUTPUT->single_button($resendconfirmurl, get_string('emailconfirmationresend'));
             echo $OUTPUT->footer();
             die;
         }
index 8927e03..eb29ae1 100644 (file)
@@ -361,6 +361,7 @@ function core_login_validate_forgot_password_data($data) {
         } else {
             if ($user = get_complete_user_data('email', $data['email'])) {
                 if (empty($user->confirmed)) {
+                    send_confirmation_email($user);
                     $errors['email'] = get_string('confirmednot');
                 }
             }
@@ -372,6 +373,7 @@ function core_login_validate_forgot_password_data($data) {
     } else {
         if ($user = get_complete_user_data('username', $data['username'])) {
             if (empty($user->confirmed)) {
+                send_confirmation_email($user);
                 $errors['email'] = get_string('confirmednot');
             }
         }
index 5315e50..9386106 100644 (file)
@@ -149,9 +149,10 @@ class media_videojs_plugin extends core_media_player_native {
         // which is BEFORE we have a chance to load any additional libraries (youtube).
         // The data-setup-lazy is just a tag name that video.js does not recognise so we can manually initialise
         // it when we are sure the dependencies are loaded.
+        static $playercounter = 1;
         $attributes = [
             'data-setup-lazy' => '{' . join(', ', $datasetup) . '}',
-            'id' => 'id_videojs_' . uniqid(),
+            'id' => 'id_videojs_' . uniqid() . '_' . $playercounter++,
             'class' => get_config('media_videojs', $isaudio ? 'audiocssclass' : 'videocssclass')
         ];
 
index f63e959..2ca50f2 100644 (file)
         <div class="name">
             {{fullname}}
             {{#showonlinestatus}}
-                <span {{^isonline}}class="hidden"{{/isonline}} data-region="contact-icon-online">
-                    {{#pix}} t/go, core, {{#str}} online, message {{/str}} {{/pix}}
+            <div {{#isonline}}class="d-inline status online"{{/isonline}}{{^isonline}}class="hidden"{{/isonline}}>
+                <span class="online-text" data-region="contact-icon-online">
+                    {{#pix}} t/online, core, {{#str}} online, message {{/str}} {{/pix}}
                 </span>
+            </div>
             {{/showonlinestatus}}
             <span {{^isblocked}}class="hidden"{{/isblocked}} data-region="contact-icon-blocked">
                 {{#pix}} t/block, core, {{#str}} contactblocked, message {{/str}} {{/pix}}
index c1a17a9..9ca16b6 100644 (file)
@@ -139,14 +139,16 @@ class assignfeedback_editpdf_renderer extends plugin_renderer_base {
             $nav_next = 'nav_next';
         }
 
-        $iconalt = get_string('navigateprevious', 'assignfeedback_editpdf');
+        $iconshortcut = $this->get_shortcut('navigate-previous-button');
+        $iconalt = get_string('navigateprevious', 'assignfeedback_editpdf', $iconshortcut);
         $iconhtml = $this->image_icon($nav_prev, $iconalt, 'assignfeedback_editpdf');
         $navigation1 .= html_writer::tag('button', $iconhtml, array('disabled'=>'true',
             'class'=>'navigate-previous-button', 'accesskey' => $this->get_shortcut('navigate-previous-button')));
         $navigation1 .= html_writer::tag('select', null, array('disabled'=>'true',
             'aria-label' => get_string('gotopage', 'assignfeedback_editpdf'), 'class'=>'navigate-page-select',
             'accesskey' => $this->get_shortcut('navigate-page-select')));
-        $iconalt = get_string('navigatenext', 'assignfeedback_editpdf');
+        $iconshortcut = $this->get_shortcut('navigate-next-button');
+        $iconalt = get_string('navigatenext', 'assignfeedback_editpdf', $iconshortcut);
         $iconhtml = $this->image_icon($nav_next, $iconalt, 'assignfeedback_editpdf');
         $navigation1 .= html_writer::tag('button', $iconhtml, array('disabled'=>'true',
             'class'=>'navigate-next-button', 'accesskey' => $this->get_shortcut('navigate-next-button')));
index 1f7fac4..67c1ab1 100644 (file)
@@ -65,8 +65,8 @@ $string['jsrequired'] = 'JavaScript is required to annotate a PDF. Please enable
 $string['launcheditor'] = 'Launch PDF editor...';
 $string['line'] = 'Line';
 $string['loadingeditor'] = 'Loading PDF editor';
-$string['navigatenext'] = 'Next page';
-$string['navigateprevious'] = 'Previous page';
+$string['navigatenext'] = 'Next page (Alt/Shift-Alt/Ctrl-Option + {$a})';
+$string['navigateprevious'] = 'Previous page (Alt/Shift-Alt/Ctrl-Option + {$a})';
 $string['oval'] = 'Oval';
 $string['output'] = 'Output:';
 $string['pagenumber'] = 'Page {$a}';
index 52e7536..b23aa66 100644 (file)
@@ -10,7 +10,6 @@
     position: relative;
     min-width: 817px;
     min-height: 400px;
-    cursor: crosshair;
     background-repeat: no-repeat;
     background-color: #ccc;
     margin-left: auto;
     position: inherit;
 }
 
-.assignfeedback_editpdf_widget .drawingregion[data-currenttool=drag] .drawingcanvas {
-    cursor: move;
-}
-
-.assignfeedback_editpdf_widget .drawingregion[data-currenttool=select] .drawingcanvas {
-    cursor: default;
-}
-
 .assignfeedback_editpdf_widget .drawingregion[data-currenttool=select] .commentdrawable textarea,
 .assignfeedback_editpdf_widget .drawingregion[data-currenttool=select] .commentdrawable svg {
     cursor: move;
index 5f704e2..dc22ae1 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js differ
index b3620af..827e3d2 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js differ
index 5f704e2..dc22ae1 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js differ
index 20c5c10..6ba0d9b 100644 (file)
@@ -110,13 +110,13 @@ Y.extend(ANNOTATIONHIGHLIGHT, M.assignfeedback_editpdf.annotation, {
         shape = this.editor.graphic.addShape({
             type: Y.Rect,
             width: bounds.width,
-            height: 16,
+            height: 20,
             stroke: false,
             fill: {
                color: highlightcolour
             },
             x: bounds.x,
-            y: edit.start.y
+            y: edit.start.y - 10
         });
 
         drawable.shapes.push(shape);
@@ -139,9 +139,9 @@ Y.extend(ANNOTATIONHIGHLIGHT, M.assignfeedback_editpdf.annotation, {
         this.gradeid = this.editor.get('gradeid');
         this.pageno = this.editor.currentpage;
         this.x = bounds.x;
-        this.y = edit.start.y;
+        this.y = edit.start.y - 10;
         this.endx = bounds.x + bounds.width;
-        this.endy = edit.start.y + 16;
+        this.endy = edit.start.y + 10;
         this.colour = edit.annotationcolour;
         this.page = '';
 
index 6f94adc..4320b85 100644 (file)
@@ -266,7 +266,7 @@ EDITOR.prototype = {
      * @method refresh_button_state
      */
     refresh_button_state: function() {
-        var button, currenttoolnode, imgurl, drawingregion;
+        var button, currenttoolnode, imgurl, drawingregion, stampimgurl, drawingcanvas;
 
         // Initalise the colour buttons.
         button = this.get_dialogue_element(SELECTOR.COMMENTCOLOURBUTTON);
@@ -291,9 +291,28 @@ EDITOR.prototype = {
         drawingregion.setAttribute('data-currenttool', this.currentedit.tool);
 
         button = this.get_dialogue_element(SELECTOR.STAMPSBUTTON);
-        button.one('img').setAttrs({'src': this.get_stamp_image_url(this.currentedit.stamp),
+        stampimgurl = this.get_stamp_image_url(this.currentedit.stamp);
+        button.one('img').setAttrs({'src': stampimgurl,
                                     'height': '16',
                                     'width': '16'});
+
+        drawingcanvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS);
+        switch (this.currentedit.tool) {
+            case 'drag':
+                drawingcanvas.setStyle('cursor', 'move');
+                break;
+            case 'highlight':
+                drawingcanvas.setStyle('cursor', 'text');
+                break;
+            case 'select':
+                drawingcanvas.setStyle('cursor', 'default');
+                break;
+            case 'stamp':
+                drawingcanvas.setStyle('cursor', 'url(' + stampimgurl + '), crosshair');
+                break;
+            default:
+                drawingcanvas.setStyle('cursor', 'crosshair');
+        }
     },
 
     /**
index 06eb4be..7e550a3 100644 (file)
@@ -922,6 +922,12 @@ class assign_files implements renderable {
         }
         foreach ($dir['files'] as $file) {
             $file->portfoliobutton = '';
+
+            $file->timemodified = userdate(
+                $file->get_timemodified(),
+                get_string('strftimedatetime', 'langconfig')
+            );
+
             if (!empty($CFG->enableportfolios)) {
                 require_once($CFG->libdir . '/portfoliolib.php');
                 $button = new portfolio_add_button();
index 872b728..f461e21 100644 (file)
@@ -1417,11 +1417,15 @@ class mod_assign_renderer extends plugin_renderer_base {
                                              'moodle',
                                              array('class'=>'icon'));
             $result .= '<li yuiConfig=\'' . json_encode($yuiconfig) . '\'>' .
-                       '<div>' . $image . ' ' .
-                                 $file->fileurl . ' ' .
-                                 $plagiarismlinks . ' ' .
-                                 $file->portfoliobutton . '</div>' .
-                       '</li>';
+                '<div>' .
+                    '<div class="fileuploadsubmission">' . $image . ' ' .
+                    $file->fileurl . ' ' .
+                    $plagiarismlinks . ' ' .
+                    $file->portfoliobutton . ' ' .
+                    '</div>' .
+                    '<div class="fileuploadsubmissiontime">' . $file->timemodified . '</div>' .
+                '</div>' .
+            '</li>';
         }
 
         $result .= '</ul>';
index 245e5b4..8579c48 100644 (file)
     background-color: transparent;
 }
 
+.path-mod-assign div.fileuploadsubmission {
+    float: left;
+    min-width: 300px;
+}
+
+.path-mod-assign div.fileuploadsubmissiontime {
+    float: right;
+}
+
 /***** End submission status *****/
 
 .path-mod-assign [data-region="grade-panel"] #id_gradeheader {
index 20d6de0..7e2aaad 100644 (file)
@@ -29,6 +29,9 @@ require_once($CFG->dirroot.'/calendar/lib.php');
 // Event types.
 define('CHAT_EVENT_TYPE_CHATTIME', 'chattime');
 
+// Gap between sessions. 5 minutes or more of idleness between messages in a chat means the messages belong in different sessions.
+define('CHAT_SESSION_GAP', 300);
+
 // The HTML head for the message window to start with (<!-- nix --> is used to get some browsers starting with output.
 global $CHAT_HTMLHEAD;
 $CHAT_HTMLHEAD = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/REC-html40/loose.dtd\"><html><head></head>\n<body>\n\n".padding(200);
@@ -1486,58 +1489,63 @@ function mod_chat_core_calendar_provide_event_action(calendar_event $event,
 /**
  * Given a set of messages for a chat, return the completed chat sessions (including optionally not completed ones).
  *
- * @param  array $messages list of messages from a chat
+ * @param  array $messages list of messages from a chat. It is assumed that these are sorted by timestamp in DESCENDING order.
  * @param  bool $showall   whether to include incomplete sessions or not
  * @return array           the list of sessions
- * @since  Moodle 3.4
+ * @since  Moodle 3.5
  */
 function chat_get_sessions($messages, $showall = false) {
-    $sessions     = array();
-    $sessiongap   = 5 * 60;    // 5 minutes silence means a new session.
-    $sessionend   = 0;
-    $sessionstart = 0;
-    $sessionusers = array();
-    $lasttime     = 0;
-
-    $messagesleft = count($messages);
-
-    foreach ($messages as $message) {  // We are walking BACKWARDS through the messages.
-
-        $messagesleft --;              // Countdown.
+    $sessions     = [];
+    $start        = 0;
+    $end          = 0;
+    $sessiontimes = [];
+
+    // Group messages by session times.
+    foreach ($messages as $message) {
+        // Initialise values start-end times if necessary.
+        if (empty($start)) {
+            $start = $message->timestamp;
+        }
+        if (empty($end)) {
+            $end = $message->timestamp;
+        }
 
-        if (!$lasttime) {
-            $lasttime = $message->timestamp;
+        // If this message's timestamp has been more than the gap, it means it's been idle.
+        if ($start - $message->timestamp > CHAT_SESSION_GAP) {
+            // Mark this as the session end of the next session.
+            $end = $message->timestamp;
         }
-        if (!$sessionend) {
-            $sessionend = $message->timestamp;
+        // Use this time as the session's start (until it gets overwritten on the next iteration, if needed).
+        $start = $message->timestamp;
+
+        // Set this start-end pair in our list of session times.
+        $sessiontimes[$end]['sessionstart'] = $start;
+        if (!isset($sessiontimes[$end]['sessionend'])) {
+            $sessiontimes[$end]['sessionend'] = $end;
         }
-        if ((($lasttime - $message->timestamp) < $sessiongap) and $messagesleft) {  // Same session.
-            if ($message->userid and !$message->issystem) {       // Remember user and count messages.
-                if (empty($sessionusers[$message->userid])) {
-                    $sessionusers[$message->userid] = 1;
-                } else {
-                    $sessionusers[$message->userid] ++;
-                }
-            }
-        } else {
-            $sessionstart = $lasttime;
-
-            $iscomplete = ($sessionend - $sessionstart > 60 and count($sessionusers) > 1);
-            if ($showall or $iscomplete) {
-                $sessions[] = (object) array(
-                    'sessionstart' => $sessionstart,
-                    'sessionend' => $sessionend,
-                    'sessionusers' => $sessionusers,
-                    'iscomplete' => $iscomplete,
-                );
+        if ($message->userid && !$message->issystem) {
+            if (!isset($sessiontimes[$end]['sessionusers'][$message->userid])) {
+                $sessiontimes[$end]['sessionusers'][$message->userid] = 1;
+            } else {
+                $sessiontimes[$end]['sessionusers'][$message->userid]++;
             }
+        }
+    }
+
+    // Go through each session time and prepare the session data to be returned.
+    foreach ($sessiontimes as $sessionend => $sessiondata) {
+        if (!isset($sessiondata['sessionusers'])) {
+            $sessiondata['sessionusers'] = [];
+        }
+        $sessionusers = $sessiondata['sessionusers'];
+        $sessionstart = $sessiondata['sessionstart'];
 
-            $sessionend = $message->timestamp;
-            $sessionusers = array();
-            $sessionusers[$message->userid] = 1;
+        $iscomplete = $sessionend - $sessionstart > 60 && count($sessionusers) > 1;
+        if ($showall || $iscomplete) {
+            $sessions[] = (object) ($sessiondata + ['iscomplete' => $iscomplete]);
         }
-        $lasttime = $message->timestamp;
     }
+
     return $sessions;
 }
 
@@ -1550,7 +1558,7 @@ function chat_get_sessions($messages, $showall = false) {
  * @param  int $end         the session end timestamp (0 to not filter by time)
  * @param  string $sort     an order to sort the results in (optional, a valid SQL ORDER BY parameter)
  * @return array session messages
- * @since  Moodle 3.4
+ * @since  Moodle 3.5
  */
 function chat_get_session_messages($chatid, $group = false, $start = 0, $end = 0, $sort = '') {
     global $DB;
index 98e1ab0..06635fb 100644 (file)
@@ -116,14 +116,13 @@ class chat_portfolio_caller extends portfolio_module_caller_base {
     public function prepare_package() {
         $content = '';
         $lasttime = 0;
-        $sessiongap = 5 * 60;    // 5 minutes silence means a new session
         foreach ($this->messages as $message) {  // We are walking FORWARDS through messages
             $m = clone $message; // grrrrrr - this causes the sha1 to change as chat_format_message changes what it's passed.
             $formatmessage = chat_format_message($m, $this->cm->course, $this->user);
             if (!isset($formatmessage->html)) {
                 continue;
             }
-            if (empty($lasttime) || (($message->timestamp - $lasttime) > $sessiongap)) {
+            if (empty($lasttime) || (($message->timestamp - $lasttime) > CHAT_SESSION_GAP)) {
                 $content .= '<hr />';
                 $content .= userdate($message->timestamp);
             }
index d41a121..939cd75 100644 (file)
@@ -82,6 +82,7 @@ $strdeletesession = get_string('deletesession', 'chat');
 $navlinks = array();
 
 $canexportsess = has_capability('mod/chat:exportsession', $context);
+$canviewfullnames = has_capability('moodle/site:viewfullnames', $context);
 
 // Print a session if one has been specified.
 
@@ -213,7 +214,7 @@ foreach ($sessions as $session) {
     foreach ($session->sessionusers as $sessionuser => $usermessagecount) {
         if ($user = $DB->get_record('user', array('id' => $sessionuser))) {
             $OUTPUT->user_picture($user, array('courseid' => $course->id));
-            echo '&nbsp;'.fullname($user, true); // XXX TODO  use capability instead of true.
+            echo '&nbsp;' . fullname($user, $canviewfullnames);
             echo "&nbsp;($usermessagecount)<br />";
         }
     }
index 2443f71..050753b 100644 (file)
@@ -428,7 +428,8 @@ class mod_chat_external_testcase extends externallib_advanced_testcase {
         $result = external_api::clean_returnvalue(mod_chat_external::get_sessions_returns(), $result);
         $this->assertCount(1, $result['sessions']); // One session.
         $this->assertTrue($result['sessions'][0]['iscomplete']); // Session complete.
-        $this->assertEquals($timenow - HOURSECS + 70, $result['sessions'][0]['sessionstart']);  // First not system message time.
+        // The session started when user1 entered the chat.
+        $this->assertEquals($timenow - HOURSECS, $result['sessions'][0]['sessionstart']);
         $this->assertEmpty($result['warnings']);
     }
 
index 4edef1a..b233af1 100644 (file)
@@ -143,6 +143,200 @@ class mod_chat_lib_testcase extends advanced_testcase {
         $this->assertFalse($actionevent->is_actionable());
     }
 
+    /**
+     * Test for chat_get_sessions().
+     */
+    public function test_chat_get_sessions() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $generator = $this->getDataGenerator();
+
+        // Setup test data.
+        $this->setAdminUser();
+        $course = $generator->create_course();
+        $chat = $generator->create_module('chat', ['course' => $course->id]);
+
+        $user1 = $generator->create_user();
+        $user2 = $generator->create_user();
+        $studentrole = $DB->get_record('role', ['shortname' => 'student']);
+        $generator->enrol_user($user1->id, $course->id, $studentrole->id);
+        $generator->enrol_user($user2->id, $course->id, $studentrole->id);
+
+        // Login as user 1.
+        $this->setUser($user1);
+        $chatsid = chat_login_user($chat->id, 'ajax', 0, $course);
+        $chatuser = $DB->get_record('chat_users', ['sid' => $chatsid]);
+
+        // This is when the session starts (when the user enters the chat).
+        $sessionstart = $chatuser->lastping;
+
+        // Send some messages.
+        chat_send_chatmessage($chatuser, 'hello!');
+        chat_send_chatmessage($chatuser, 'bye bye!');
+
+        // Login as user 2.
+        $this->setUser($user2);
+        $chatsid = chat_login_user($chat->id, 'ajax', 0, $course);
+        $chatuser = $DB->get_record('chat_users', ['sid' => $chatsid]);
+
+        // Send a message and take note of this message ID.
+        $messageid = chat_send_chatmessage($chatuser, 'greetings!');
+
+        // This is when the session ends (timestamp of the last message sent to the chat).
+        $sessionend = $DB->get_field('chat_messages', 'timestamp', ['id' => $messageid]);
+
+        // Get the messages for this chat session.
+        $messages = chat_get_session_messages($chat->id, false, 0, 0, 'timestamp DESC');
+
+        // We should have 3 user and 2 system (enter) messages.
+        $this->assertCount(5, $messages);
+
+        // Fetch the chat sessions from the messages we retrieved.
+        $sessions = chat_get_sessions($messages, true);
+
+        // There should be only one session.
+        $this->assertCount(1, $sessions);
+
+        // Get this session.
+        $session = reset($sessions);
+
+        // Confirm that the start and end times of the session matches.
+        $this->assertEquals($sessionstart, $session->sessionstart);
+        $this->assertEquals($sessionend, $session->sessionend);
+        // Confirm we have 2 participants in the chat.
+        $this->assertCount(2, $session->sessionusers);
+    }
+
+    /**
+     * Test for chat_get_sessions with messages belonging to multiple sessions.
+     */
+    public function test_chat_get_sessions_multiple() {
+        $messages = [];
+        $gap = 5; // 5 secs.
+
+        $now = time();
+        $timestamp = $now;
+
+        // Messages belonging to 3 sessions. Session 1 has 10 messages, 2 has 15, 3 has 25.
+        $sessionusers = [];
+        $sessiontimes = [];
+        $session = 0; // Incomplete session.
+        for ($i = 1; $i <= 50; $i++) {
+            // Take note of expected session times as we go through.
+            switch ($i) {
+                case 1:
+                    // Session 1 start time.
+                    $sessiontimes[0]['start'] = $timestamp;
+                    break;
+                case 10:
+                    // Session 1 end time.
+                    $sessiontimes[0]['end'] = $timestamp;
+                    break;
+                case 11:
+                    // Session 2 start time.
+                    $sessiontimes[1]['start'] = $timestamp;
+                    break;
+                case 25:
+                    // Session 2 end time.
+                    $sessiontimes[1]['end'] = $timestamp;
+                    break;
+                case 26:
+                    // Session 3 start time.
+                    $sessiontimes[2]['start'] = $timestamp;
+                    break;
+                case 50:
+                    // Session 3 end time.
+                    $sessiontimes[2]['end'] = $timestamp;
+                    break;
+            }
+
+            // User 1 to 5.
+            $user = rand(1, 5);
+
+            // Let's also include system messages as well. Give them to pop in 1-in-10 chance.
+            $issystem = rand(1, 10) == 10;
+
+            if ($issystem) {
+                $message = 'enter';
+            } else {
+                $message = 'Message ' . $i;
+                if (!isset($sessionusers[$session][$user])) {
+                    $sessionusers[$session][$user] = 1;
+                } else {
+                    $sessionusers[$session][$user]++;
+                }
+            }
+            $messages[] = (object)[
+                'id' => $i,
+                'chatid' => 1,
+                'userid' => $user,
+                'message' => $message,
+                'issystem' => $issystem,
+                'timestamp' => $timestamp,
+            ];
+
+            // Set the next timestamp.
+            if ($i == 10 || $i == 25) {
+                // New session.
+                $session++;
+                $timestamp += CHAT_SESSION_GAP + 1;
+            } else {
+                $timestamp += $gap;
+            }
+        }
+        // Reverse sort the messages so they're in descending order.
+        rsort($messages);
+
+        // Get chat sessions showing only complete ones.
+        $completesessions = chat_get_sessions($messages);
+        // Session 1 is incomplete, so there should only be 2 sessions when $showall is false.
+        $this->assertCount(2, $completesessions);
+
+        // Reverse sort sessions so they are in ascending order matching our expected session times and users.
+        $completesessions = array_reverse($completesessions);
+        foreach ($completesessions as $index => $session) {
+            // We increment index by 1 because the incomplete expected session (index=0) is not included.
+            $expectedindex = $index + 1;
+
+            // Check the session users.
+            $users = $sessionusers[$expectedindex];
+            $this->assertCount(count($users), $session->sessionusers);
+            // Check the message counts for each user in this session.
+            foreach ($users as $userid => $messagecount) {
+                $this->assertEquals($messagecount, $session->sessionusers[$userid]);
+            }
+
+            $sessionstart = $sessiontimes[$expectedindex]['start'];
+            $sessionend = $sessiontimes[$expectedindex]['end'];
+            $this->assertEquals($sessionstart, $session->sessionstart);
+            $this->assertEquals($sessionend, $session->sessionend);
+        }
+
+        // Get all the chat sessions.
+        $allsessions = chat_get_sessions($messages, true);
+        // When showall is true, we should get 3 sessions.
+        $this->assertCount(3, $allsessions);
+
+        // Reverse sort sessions so they are in ascending order matching our expected session times and users.
+        $allsessions = array_reverse($allsessions);
+        foreach ($allsessions as $index => $session) {
+            // Check the session users.
+            $users = $sessionusers[$index];
+            $this->assertCount(count($users), $session->sessionusers);
+            // Check the message counts for each user in this session.
+            foreach ($users as $userid => $messagecount) {
+                $this->assertEquals($messagecount, $session->sessionusers[$userid]);
+            }
+
+            $sessionstart = $sessiontimes[$index]['start'];
+            $sessionend = $sessiontimes[$index]['end'];
+            $this->assertEquals($sessionstart, $session->sessionstart);
+            $this->assertEquals($sessionend, $session->sessionend);
+        }
+    }
+
     /**
      * Creates an action event.
      *
index ddcf62d..ee251cd 100644 (file)
@@ -59,7 +59,7 @@ class mod_folder_renderer extends plugin_renderer_base {
         $foldertree = new folder_tree($folder, $cm);
         if ($folder->display == FOLDER_DISPLAY_INLINE) {
             // Display module name as the name of the root directory.
-            $foldertree->dir['dirname'] = $cm->get_formatted_name();
+            $foldertree->dir['dirname'] = $cm->get_formatted_name(array('escape' => false));
         }
         $output .= $this->output->box($this->render($foldertree),
                 'generalbox foldertree');
index 5a4744a..c0bf984 100644 (file)
@@ -3850,14 +3850,16 @@ function forum_print_discussion_header(&$post, $forum, $group = -1, $datestring
     $postuser = username_load_fields_from_object($postuser, $post, null, $postuserfields);
     $postuser->id = $post->userid;
     echo '<td class="author">';
-    echo '<span class="picture">';
+    echo '<div class="media">';
+    echo '<span class="pull-left">';
     echo $OUTPUT->user_picture($postuser, array('courseid'=>$forum->course));
     echo '</span>';
-    echo '<span class="name">';
     // User name
+    echo '<div class="media-body">';
     $fullname = fullname($postuser, has_capability('moodle/site:viewfullnames', $modcontext));
     echo '<a href="'.$CFG->wwwroot.'/user/view.php?id='.$post->userid.'&amp;course='.$forum->course.'">'.$fullname.'</a>';
-    echo '</span>';
+    echo '</div>';
+    echo '</div>';
     echo "</td>\n";
 
     // Group picture
index 1472856..9a7d217 100644 (file)
     margin: 0 .3em;
 }
 
-.path-mod-forum .forumheaderlist .picture {
-    width: 35px;
-}
-
 .path-mod-forum .forumheaderlist .discussion .starter {
     vertical-align: middle;
 }
     text-align: right;
 }
 
-.path-mod-forum .forumheaderlist .replies,
-.path-mod-forum .forumheaderlist .discussion .author {
+.path-mod-forum .forumheaderlist .replies {
     white-space: nowrap;
 }
 
diff --git a/pix/t/online.png b/pix/t/online.png
new file mode 100644 (file)
index 0000000..c449d4a
Binary files /dev/null and b/pix/t/online.png differ
diff --git a/pix/t/online.svg b/pix/t/online.svg
new file mode 100644 (file)
index 0000000..64f57e9
--- /dev/null
@@ -0,0 +1,3 @@
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\r
+       <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">\r
+]><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" preserveAspectRatio="xMinYMid meet" overflow="visible"><circle cx="6" cy="6" r="6" fill="#7d7"/></svg>
\ No newline at end of file
index 71e601a..41dc87e 100644 (file)
@@ -79,12 +79,20 @@ abstract class contextlist_base implements
      * Get the complete list of context objects that relate to this
      * request.
      *
-     * @return  \contect[]
+     * @return  \context[]
      */
     public function get_contexts() : array {
         $contexts = [];
         foreach ($this->contextids as $contextid) {
-            $contexts[] = \context::instance_by_id($contextid);
+            // It is possible that this context has been deleted and we now have subsequent calls being made with this
+            // contextlist. Exceptions here will stop the further processing of this component and that is why we are
+            // doing a try catch.
+            try {
+                $contexts[] = \context::instance_by_id($contextid);
+            } catch (\Exception $e) {
+                // Remove this context.
+                unset($this->contextids[$this->iteratorposition]);
+            }
         }
 
         return $contexts;
@@ -114,7 +122,25 @@ abstract class contextlist_base implements
      * @return  \context
      */
     public function current() {
-        return \context::instance_by_id($this->contextids[$this->iteratorposition]);
+        // It is possible that this context has been deleted and we now have subsequent calls being made with this
+        // contextlist. Exceptions here will stop the further processing of this component and that is why we are
+        // doing a try catch.
+        try {
+            $context = \context::instance_by_id($this->contextids[$this->iteratorposition]);
+        } catch (\Exception $e) {
+            // Remove this context.
+            unset($this->contextids[$this->iteratorposition]);
+            // Check to see if there are any more contexts left.
+            if ($this->count()) {
+                // Move the pointer to the next record and try again.
+                $this->next();
+                $context = $this->current();
+            } else {
+                // There are no more context ids left.
+                return;
+            }
+        }
+        return $context;
     }
 
     /**
index e60dcbc..0dc4f79 100644 (file)
@@ -141,6 +141,130 @@ class contextlist_base_test extends advanced_testcase {
             $this->assertNotFalse(array_search($context, $contexts));
         }
     }
+
+    /**
+     * Test that deleting a context results in current returning nothing.
+     */
+    public function test_current_context_one_context() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $data = (object) [
+            'contextlevel' => CONTEXT_BLOCK,
+            'instanceid' => 45,
+            'path' => '1/5/67/107',
+            'depth' => 4
+        ];
+
+        $contextid = $DB->insert_record('context', $data);
+
+        $contextbase = new test_contextlist_base();
+        $contextbase->set_contextids([$contextid]);
+        $this->assertCount(1, $contextbase);
+        $currentcontext = $contextbase->current();
+        $this->assertEquals($contextid, $currentcontext->id);
+        $DB->delete_records('context', ['id' => $contextid]);
+        context_helper::reset_caches();
+        $this->assertEmpty($contextbase->current());
+    }
+
+    /**
+     * Test that deleting a context results in the next record being returned.
+     */
+    public function test_current_context_two_contexts() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $data = (object) [
+            'contextlevel' => CONTEXT_BLOCK,
+            'instanceid' => 45,
+            'path' => '1/5/67/107',
+            'depth' => 4
+        ];
+
+        $contextid1 = $DB->insert_record('context', $data);
+
+        $data = (object) [
+            'contextlevel' => CONTEXT_BLOCK,
+            'instanceid' => 47,
+            'path' => '1/5/54/213',
+            'depth' => 4
+        ];
+
+        $contextid2 = $DB->insert_record('context', $data);
+
+        $contextbase = new test_contextlist_base();
+        $contextbase->set_contextids([$contextid1, $contextid2]);
+        $this->assertCount(2, $contextbase);
+        $DB->delete_records('context', ['id' => $contextid1]);
+        context_helper::reset_caches();
+        // Current should return context 2.
+        $this->assertEquals($contextid2, $contextbase->current()->id);
+    }
+
+    /**
+     * Test that if there are no non-deleted contexts that nothing is returned.
+     */
+    public function test_get_contexts_all_deleted() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $data = (object) [
+            'contextlevel' => CONTEXT_BLOCK,
+            'instanceid' => 45,
+            'path' => '1/5/67/107',
+            'depth' => 4
+        ];
+
+        $contextid = $DB->insert_record('context', $data);
+
+        $contextbase = new test_contextlist_base();
+        $contextbase->set_contextids([$contextid]);
+        $this->assertCount(1, $contextbase);
+        $DB->delete_records('context', ['id' => $contextid]);
+        context_helper::reset_caches();
+        $this->assertEmpty($contextbase->get_contexts());
+    }
+
+    /**
+     * Test that get_contexts() returns only active contexts.
+     */
+    public function test_get_contexts_one_deleted() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $data = (object) [
+            'contextlevel' => CONTEXT_BLOCK,
+            'instanceid' => 45,
+            'path' => '1/5/67/107',
+            'depth' => 4
+        ];
+
+        $contextid1 = $DB->insert_record('context', $data);
+
+        $data = (object) [
+            'contextlevel' => CONTEXT_BLOCK,
+            'instanceid' => 47,
+            'path' => '1/5/54/213',
+            'depth' => 4
+        ];
+
+        $contextid2 = $DB->insert_record('context', $data);
+
+        $contextbase = new test_contextlist_base();
+        $contextbase->set_contextids([$contextid1, $contextid2]);
+        $this->assertCount(2, $contextbase);
+        $DB->delete_records('context', ['id' => $contextid1]);
+        context_helper::reset_caches();
+        $contexts = $contextbase->get_contexts();
+        $this->assertCount(1, $contexts);
+        $context = array_shift($contexts);
+        $this->assertEquals($contextid2, $context->id);
+    }
 }
 
 /**
index 2308f1a..26c92d6 100644 (file)
@@ -222,7 +222,7 @@ class qtype_multianswer_textfield_renderer extends qtype_multianswer_subq_render
             'value' => $response,
             'id' => $qa->get_qt_field_name($fieldname),
             'size' => $size,
-            'class' => 'form-control',
+            'class' => 'form-control mb-1',
         );
         if ($options->readonly) {
             $inputattributes['readonly'] = 'readonly';
@@ -245,7 +245,7 @@ class qtype_multianswer_textfield_renderer extends qtype_multianswer_subq_render
                         $qa, 'question', 'answerfeedback', $matchinganswer->id),
                 s($correctanswer->answer), $options);
 
-        $output = html_writer::start_tag('span', array('class' => 'subquestion form-inline'));
+        $output = html_writer::start_tag('span', array('class' => 'subquestion form-inline d-inline'));
         $output .= html_writer::tag('label', get_string('answer'),
                 array('class' => 'subq accesshide', 'for' => $inputattributes['id']));
         $output .= html_writer::empty_tag('input', $inputattributes);
index bf24aac..af0e685 100644 (file)
@@ -1349,7 +1349,7 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
 /* Fixed for safari browser on iPhone4S with ios7@mixin */
 
 @media (max-height: 639px) {
-    .ios.safari .choosercontainer #chooseform .submitbuttons {
+    .ios .choosercontainer #chooseform .submitbuttons {
         padding: 45px 0;
     }
 }
index dfd9f04..fde60b3 100644 (file)
     padding-bottom: 13rem;
 }
 
+.jumpmenu .form-inline {
+    display: block;
+}
+
 .path-course-view .completionprogress {
     margin-left: 25px;
 }
index 8c7846d..11be4f6 100644 (file)
@@ -33,7 +33,7 @@
 
         &.online {
             .online-text {
-                display: block;
+                display: inherit;
             }
             .offline-text {
                 display: none;
index 0404720..984a6fb 100644 (file)
@@ -77,7 +77,6 @@ select {
         }
 
         .discussion {
-            .author,
             .replies,
             .lastpost {
                 white-space: normal;
@@ -91,8 +90,6 @@ select {
             .topic,
             .discussionsubscription,
             .topic.starter,
-            .picture,
-            .author,
             .replies,
             .lastpost {
                 vertical-align: top;
index 98d7c19..ed6680b 100644 (file)
@@ -1573,7 +1573,7 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
 }
 /* Fixed for safari browser on iPhone4S with ios7.*/
 @media (max-height: 639px) {
-    .ios.safari .choosercontainer #chooseform .submitbuttons {
+    .ios .choosercontainer #chooseform .submitbuttons {
         padding: 45px 0;
     }
 }
index ef42132..70aa519 100644 (file)
@@ -29,7 +29,7 @@
 
         &.online {
             .online-text {
-                display: block;
+                display: inherit;
             }
             .offline-text {
                 display: none;
index e79aa82..e91e542 100644 (file)
@@ -104,7 +104,6 @@ body.path-mod-feedback #region-main .mform.feedback_form .feedback-item-pagebrea
             }
         }
         .discussion {
-            .author,
             .replies,
             .lastpost {
                 white-space: normal;
@@ -116,15 +115,10 @@ body.path-mod-feedback #region-main .mform.feedback_form .feedback-item-pagebrea
             .topic,
             .discussionsubscription,
             .topic.starter,
-            .author,
             .replies,
             .lastpost {
                 vertical-align: top;
             }
-            .picture {
-                margin: 0 20px;
-                float: left;
-            }
             .unread {
                 img {
                     margin: 0;
@@ -414,6 +408,16 @@ div#dock {
 #page-mod-lesson-view .branchbuttoncontainer .singlebutton input[type="submit"] {
     white-space: normal;
 }
+#page-mod-lesson-view .answeroption .fitemtitle {
+    width: 30%;
+    text-align: left;
+}
+#page-mod-lesson-view .answeroption .fitemtitle label {
+    padding-right: 15px;
+}
+#page-mod-lesson-view .fitem .felement {
+    margin-left: 30%;
+}
 .path-mod-lesson .reviewessay {
     width: 40%;
     border: 1px solid #ddd;
index 6b31450..4bb1976 100644 (file)
@@ -3909,7 +3909,7 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
 }
 /* Fixed for safari browser on iPhone4S with ios7.*/
 @media (max-height: 639px) {
-  .ios.safari .choosercontainer #chooseform .submitbuttons {
+  .ios .choosercontainer #chooseform .submitbuttons {
     padding: 45px 0;
   }
 }
@@ -8128,7 +8128,7 @@ a.ygtvspacer:hover {
   color: #ff6961;
 }
 .messaging-area-container .status.online .online-text {
-  display: block;
+  display: inherit;
 }
 .messaging-area-container .status.online .offline-text {
   display: none;
@@ -17342,7 +17342,6 @@ body.path-mod-feedback #region-main .mform.feedback_form .feedback-item-pagebrea
   padding-left: 0.5em;
   padding-right: 0.5em;
 }
-.path-mod-forum .forumheaderlist .discussion .author,
 .path-mod-forum .forumheaderlist .discussion .replies,
 .path-mod-forum .forumheaderlist .discussion .lastpost {
   white-space: normal;
@@ -17354,15 +17353,10 @@ body.path-mod-feedback #region-main .mform.feedback_form .feedback-item-pagebrea
 .path-mod-forum .forumheaderlist .discussion .topic,
 .path-mod-forum .forumheaderlist .discussion .discussionsubscription,
 .path-mod-forum .forumheaderlist .discussion .topic.starter,
-.path-mod-forum .forumheaderlist .discussion .author,
 .path-mod-forum .forumheaderlist .discussion .replies,
 .path-mod-forum .forumheaderlist .discussion .lastpost {
   vertical-align: top;
 }
-.path-mod-forum .forumheaderlist .discussion .picture {
-  margin: 0 20px;
-  float: left;
-}
 .path-mod-forum .forumheaderlist .discussion .unread img {
   margin: 0;
 }
@@ -17613,6 +17607,16 @@ div#dock {
 #page-mod-lesson-view .branchbuttoncontainer .singlebutton input[type="submit"] {
   white-space: normal;
 }
+#page-mod-lesson-view .answeroption .fitemtitle {
+  width: 30%;
+  text-align: left;
+}
+#page-mod-lesson-view .answeroption .fitemtitle label {
+  padding-right: 15px;
+}
+#page-mod-lesson-view .fitem .felement {
+  margin-left: 30%;
+}
 .path-mod-lesson .reviewessay {
   width: 40%;
   border: 1px solid #ddd;
index 64fdd87..7d2692d 100644 (file)
@@ -1,6 +1,19 @@
 This files describes API changes in /theme/* themes,
 information provided here is intended especially for theme designer.
 
+=== 3.6 ===
+
+* The body tag has various browser-specific classes for those cases where some
+  browser-sniffing is unavoidable. This used not to recognised Microsoft Edge,
+  but now it does (class 'edge'). Also, both Safari and Chrome used to be
+  class 'safari', but now there are separate classes 'safari' and 'chrome'.
+  The body's class should change to match the browser:
+  - Google Chrome (Mac & Window): The body class should change from safari to chrome.
+  - Google Chrome (Android) : The body class should change from safari, android to chrome, android.
+  - Microsoft Edge: The body class should change from safari to edge.
+  - Mobile safari: The class ios should exist, safari class should be removed.
+* Remove class .safari styling from activity chooser dialog for theme boost and bootstrapbase.
+
 === 3.4 ===
 
 * There was a mustache template login.mustache in /lib/templates/ rendering the login form
index 4f79b5d..645a0ca 100644 (file)
@@ -367,7 +367,7 @@ class participants_table extends \table_sql {
         $canreviewenrol = has_capability('moodle/course:enrolreview', $this->context);
         if ($canreviewenrol) {
             $fullname = fullname($data);
-            $coursename = $this->course->fullname;
+            $coursename = format_string($this->course->fullname, true, array('context' => $this->context));
             require_once($CFG->dirroot . '/enrol/locallib.php');
             $manager = new \course_enrolment_manager($PAGE, $this->course);
             $userenrolments = $manager->get_user_enrolments($data->id);
index f56955d..5e69105 100644 (file)
@@ -1154,6 +1154,33 @@ function user_can_view_profile($user, $course = null, $usercontext = null) {
         return true;
     }
 
+    // Use callbacks so that (primarily) local plugins can prevent or allow profile access.
+    $forceallow = false;
+    $plugintypes = get_plugins_with_function('control_view_profile');
+    foreach ($plugintypes as $plugins) {
+        foreach ($plugins as $pluginfunction) {
+            $result = $pluginfunction($user, $course, $usercontext);
+            switch ($result) {
+                case core_user::VIEWPROFILE_DO_NOT_PREVENT:
+                    // If the plugin doesn't stop access, just continue to next plugin or use
+                    // default behaviour.
+                    break;
+                case core_user::VIEWPROFILE_FORCE_ALLOW:
+                    // Record that we are definitely going to allow it (unless another plugin
+                    // returns _PREVENT).
+                    $forceallow = true;
+                    break;
+                case core_user::VIEWPROFILE_PREVENT:
+                    // If any plugin returns PREVENT then we return false, regardless of what
+                    // other plugins said.
+                    return false;
+            }
+        }
+    }
+    if ($forceallow) {
+        return true;
+    }
+
     // Course contacts have visible profiles always.
     if (has_coursecontact_role($user->id)) {
         return true;
index f1c459b..c4220aa 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2018053100.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2018060700.00;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 
-$release  = '3.6dev (Build: 20180531)'; // Human-friendly version name
+$release  = '3.6dev (Build: 20180607)'; // Human-friendly version name
 
 $branch   = '36';                       // This version's branch.
 $maturity = MATURITY_ALPHA;             // This version's maturity level.