From 690d44219cdf8601b1ff264aa8ba61d475431a26 Mon Sep 17 00:00:00 2001 From: Heena Agheda Date: Thu, 26 Mar 2020 15:02:02 +1100 Subject: [PATCH] MDL-65856 session: UX review of session timeout Add new setting 'sessiontimeoutwarning', gives logged in user ability to extend session when there is no activity. --- admin/settings/server.php | 16 +++++ lang/en/admin.php | 3 + lang/en/moodle.php | 1 + lib/adminlib.php | 38 +++++++++++ lib/amd/build/network.min.js | 2 +- lib/amd/build/network.min.js.map | 2 +- lib/amd/src/network.js | 111 ++++++++++++++++++++----------- lib/outputrequirementslib.php | 29 ++++---- lib/setup.php | 4 ++ 9 files changed, 151 insertions(+), 55 deletions(-) diff --git a/admin/settings/server.php b/admin/settings/server.php index 11541bb6d65..beb70f3f974 100644 --- a/admin/settings/server.php +++ b/admin/settings/server.php @@ -73,6 +73,22 @@ if ($hassiteconfig) { $temp->add(new admin_setting_configduration('sessiontimeout', new lang_string('sessiontimeout', 'admin'), new lang_string('configsessiontimeout', 'admin'), 8 * 60 * 60)); + $sessiontimeoutwarning = new admin_setting_configduration('sessiontimeoutwarning', + new lang_string('sessiontimeoutwarning', 'admin'), + new lang_string('configsessiontimeoutwarning', 'admin'), 20 * 60); + + $sessiontimeoutwarning->set_validate_function(function(int $value): string { + global $CFG; + // Check sessiontimeoutwarning is less than sessiontimeout. + if ($CFG->sessiontimeout <= $value) { + return get_string('configsessiontimeoutwarningcheck', 'admin'); + } else { + return ''; + } + }); + + $temp->add($sessiontimeoutwarning); + $temp->add(new admin_setting_configtext('sessioncookie', new lang_string('sessioncookie', 'admin'), new lang_string('configsessioncookie', 'admin'), '', PARAM_ALPHANUM)); $temp->add(new admin_setting_configtext('sessioncookiepath', new lang_string('sessioncookiepath', 'admin'), diff --git a/lang/en/admin.php b/lang/en/admin.php index 4e1f6c3ea99..92781b58f1e 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -348,6 +348,8 @@ $string['configsessioncookie'] = 'This setting customises the name of the cookie $string['configsessioncookiedomain'] = 'This allows you to change the domain that the Moodle cookies are available from. This is useful for Moodle customisations (e.g. authentication or enrolment plugins) that need to share Moodle session information with a web application on another subdomain. WARNING: it is strongly recommended to leave this setting at the default (empty) - an incorrect value will prevent all logins to the site.'; $string['configsessioncookiepath'] = 'If you need to change where browsers send the Moodle cookies, you can change this setting to specify a subdirectory of your web site. Otherwise the default \'/\' should be fine.'; $string['configsessiontimeout'] = 'If people logged in to this site are idle for a long time (without loading pages) then they are automatically logged out (their session is ended). This variable specifies how long this time should be.'; +$string['configsessiontimeoutwarning'] = 'If people logged in to this site are idle for a long time (without loading pages) then they are warned about their session is about to end. This variable specifies how long this time should be.'; +$string['configsessiontimeoutwarningcheck'] = 'Session timeout warning must be less than session timeout'; $string['configshowicalsource'] = 'Show source information for iCal events'; $string['configshowcommentscount'] = 'Show comments count, it will cost one more query when display comments link'; $string['configshowsiteparticipantslist'] = 'All of these site students and site teachers will be listed on the site participants list. Who shall be allowed to see this site participants list?'; @@ -1191,6 +1193,7 @@ $string['sessioncookiedomain'] = 'Cookie domain'; $string['sessioncookiepath'] = 'Cookie path'; $string['sessionhandling'] = 'Session handling'; $string['sessiontimeout'] = 'Timeout'; +$string['sessiontimeoutwarning'] = 'Timeout Warning'; $string['settingdependenton'] = 'This setting may be hidden, based on the value of {$a}.'; $string['settingfileuploads'] = 'File uploading is required for normal operation, please enable it in PHP configuration.'; $string['settingmemorylimit'] = 'Insufficient memory detected, please set higher memory limit in PHP settings.'; diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 8fbcca4d5bb..95c040d83b8 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -1180,6 +1180,7 @@ $string['loginstepsnone'] = '

Hi!

All you need to do is make up a username and password and use it in the form on this page!

If someone else has already chosen your username then you\'ll have to try again using a different username.

'; $string['loginto'] = 'Log in to {$a}'; +$string['loginagain'] = 'Log in again'; $string['logout'] = 'Log out'; $string['logoutconfirm'] = 'Do you really want to log out?'; $string['logs'] = 'Logs'; diff --git a/lib/adminlib.php b/lib/adminlib.php index ff74c9a1666..1177b59775a 100644 --- a/lib/adminlib.php +++ b/lib/adminlib.php @@ -3814,6 +3814,8 @@ class admin_setting_configduration extends admin_setting { /** @var int default duration unit */ protected $defaultunit; + /** @var callable|null Validation function */ + protected $validatefunction = null; /** * Constructor @@ -3837,6 +3839,36 @@ class admin_setting_configduration extends admin_setting { parent::__construct($name, $visiblename, $description, $defaultsetting); } + /** + * Sets a validate function. + * + * The callback will be passed one parameter, the new setting value, and should return either + * an empty string '' if the value is OK, or an error message if not. + * + * @param callable|null $validatefunction Validate function or null to clear + * @since Moodle 3.10 + */ + public function set_validate_function(?callable $validatefunction = null) { + $this->validatefunction = $validatefunction; + } + + /** + * Validate the setting. This uses the callback function if provided; subclasses could override + * to carry out validation directly in the class. + * + * @param int $data New value being set + * @return string Empty string if valid, or error message text + * @since Moodle 3.10 + */ + protected function validate_setting(int $data): string { + // If validation function is specified, call it now. + if ($this->validatefunction) { + return call_user_func($this->validatefunction, $data); + } else { + return ''; + } + } + /** * Returns selectable units. * @static @@ -3922,6 +3954,12 @@ class admin_setting_configduration extends admin_setting { return get_string('errorsetting', 'admin'); } + // Validate the new setting. + $error = $this->validate_setting($seconds); + if ($error) { + return $error; + } + $result = $this->config_write($this->name, $seconds); return ($result ? '' : get_string('errorsetting', 'admin')); } diff --git a/lib/amd/build/network.min.js b/lib/amd/build/network.min.js index 81e4444b4ca..680602f954a 100644 --- a/lib/amd/build/network.min.js +++ b/lib/amd/build/network.min.js @@ -1,2 +1,2 @@ -define ("core/network",["jquery","core/ajax","core/config","core/notification","core/str"],function(a,b,c,d,e){var f=!1,g=!1,h=0,i=0,j=!1,k=!1,l=1e3*Math.min(c.sessiontimeout/10,600),m=function(){k=!0},n=function(){if(k){return e.get_strings([{key:"sessionexpired",component:"error"},{key:"sessionerroruser",component:"error"}]).then(function(a){d.alert(a[0],a[1]);return!0}).fail(d.exception)}else{return b.call([{methodname:"core_session_touch",args:{}}],!0,!0,!1,i)[0].then(function(){if(0=a.userid){return!1}if(0>a.timeremaining){e.get_strings([{key:"sessionexpired",component:"error"},{key:"sessionerroruser",component:"error"}]).then(function(a){d.alert(a[0],a[1]);return!0}).fail(d.exception)}else if(1e3*a.timeremaining<2*l&&!g){setTimeout(m,1e3*a.timeremaining);g=!0;e.get_strings([{key:"norecentactivity",component:"moodle"},{key:"sessiontimeoutsoon",component:"moodle"},{key:"extendsession",component:"moodle"},{key:"cancel",component:"moodle"}]).then(function(a){d.confirm(a[0],a[1],a[2],a[3],function(){n();g=!1;setTimeout(o,5*l);return!0},function(){g=!1;setTimeout(o,l)});return!0}).fail(d.exception)}else{setTimeout(o,l)}return!0})},p=function(){if(0m){return!1}else{return e.get_strings([{key:"sessionexpired",component:"error"},{key:"sessionerroruser",component:"error"},{key:"loginagain",component:"moodle"},{key:"cancel",component:"moodle"}]).then(function(a){d.confirm(a[0],a[1],a[2],a[3],function(){location.reload();return!0});return!0}).catch(d.exception)}})},r=function(){if(k){return q()}else{return b.call([{methodname:"core_session_touch",args:{}}],!0,!0,!1,i)[0].then(function(){if(0=a.userid){return!1}if(0>=a.timeremaining){return q()}else if(1e3*a.timeremaining<=m&&!g){g=!0;e.get_strings([{key:"norecentactivity",component:"moodle"},{key:"sessiontimeoutsoon",component:"moodle"},{key:"extendsession",component:"moodle"},{key:"cancel",component:"moodle"}]).then(function(a){return d.confirm(a[0],a[1],a[2],a[3],function(){r();g=!1;setTimeout(s,n);return!0},function(){setTimeout(s,l)})}).then(function(b){setTimeout(o,1e3*a.timeremaining,b)}).catch(d.exception)}else{setTimeout(s,l)}return!0})},t=function(){if(0.\n\n/**\n * Poll the server to keep the session alive.\n *\n * @module core/network\n * @package core\n * @copyright 2019 Damyon Wiese\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/ajax', 'core/config', 'core/notification', 'core/str'],\n function($, Ajax, Config, Notification, Str) {\n\n var started = false;\n var warningDisplayed = false;\n var keepAliveFrequency = 0;\n var requestTimeout = 0;\n var keepAliveMessage = false;\n var sessionTimeout = false;\n // 1/10 of session timeout, max of 10 minutes.\n var checkFrequency = Math.min((Config.sessiontimeout / 10), 600) * 1000;\n // 1/5 of sessiontimeout.\n var warningLimit = checkFrequency * 2;\n\n /**\n * The session time has expired - we can't extend it now.\n */\n var timeoutSessionExpired = function() {\n sessionTimeout = true;\n };\n\n /**\n * Ping the server to keep the session alive.\n *\n * @return {Promise}\n */\n var touchSession = function() {\n var request = {\n methodname: 'core_session_touch',\n args: { }\n };\n\n if (sessionTimeout) {\n // We timed out before we extended the session.\n return Str.get_strings([\n {key: 'sessionexpired', component: 'error'},\n {key: 'sessionerroruser', component: 'error'}\n ]).then(function(strings) {\n Notification.alert(\n strings[0], // Title.\n strings[1] // Message.\n );\n return true;\n }).fail(Notification.exception);\n } else {\n return Ajax.call([request], true, true, false, requestTimeout)[0].then(function() {\n if (keepAliveFrequency > 0) {\n setTimeout(touchSession, keepAliveFrequency);\n }\n return true;\n }).fail(function() {\n Notification.alert('', keepAliveMessage);\n });\n }\n };\n\n /**\n * Ask the server how much time is remaining in this session and\n * show confirm/cancel notifications if the session is about to run out.\n *\n * @return {Promise}\n */\n var checkSession = function() {\n var request = {\n methodname: 'core_session_time_remaining',\n args: { }\n };\n\n sessionTimeout = false;\n return Ajax.call([request], true, true, true)[0].then(function(args) {\n if (args.userid <= 0) {\n return false;\n }\n if (args.timeremaining < 0) {\n Str.get_strings([\n {key: 'sessionexpired', component: 'error'},\n {key: 'sessionerroruser', component: 'error'}\n ]).then(function(strings) {\n Notification.alert(\n strings[0], // Title.\n strings[1] // Message.\n );\n return true;\n }).fail(Notification.exception);\n\n } else if (args.timeremaining * 1000 < warningLimit && !warningDisplayed) {\n // If we don't extend the session before the timeout - warn.\n setTimeout(timeoutSessionExpired, args.timeremaining * 1000);\n warningDisplayed = true;\n Str.get_strings([\n {key: 'norecentactivity', component: 'moodle'},\n {key: 'sessiontimeoutsoon', component: 'moodle'},\n {key: 'extendsession', component: 'moodle'},\n {key: 'cancel', component: 'moodle'}\n ]).then(function(strings) {\n Notification.confirm(\n strings[0], // Title.\n strings[1], // Message.\n strings[2], // Extend session.\n strings[3], // Cancel.\n function() {\n touchSession();\n warningDisplayed = false;\n // First wait is half the session timeout.\n setTimeout(checkSession, checkFrequency * 5);\n return true;\n },\n function() {\n warningDisplayed = false;\n setTimeout(checkSession, checkFrequency);\n }\n );\n return true;\n }).fail(Notification.exception);\n } else {\n setTimeout(checkSession, checkFrequency);\n }\n return true;\n });\n // We do not catch the fails from the above ajax call because they will fail when\n // we are not logged in - we don't need to take any action then.\n };\n\n /**\n * Start calling a function to check if the session is still alive.\n */\n var start = function() {\n if (keepAliveFrequency > 0) {\n setTimeout(touchSession, keepAliveFrequency);\n } else {\n // First wait is half the session timeout.\n setTimeout(checkSession, checkFrequency * 5);\n }\n };\n\n /**\n * Don't allow more than one of these polling loops in a single page.\n */\n var init = function() {\n // We only allow one concurrent instance of this checker.\n if (started) {\n return;\n }\n started = true;\n\n start();\n };\n\n /**\n * Start polling with more specific values for the frequency, timeout and message.\n *\n * @param {number} freq How ofter to poll the server.\n * @param {number} timeout The time to wait for each request to the server.\n * @param {string} message The message to display if the session is going to time out.\n */\n var keepalive = function(freq, timeout, message) {\n // We only allow one concurrent instance of this checker.\n if (started) {\n return;\n }\n started = true;\n\n keepAliveFrequency = freq * 1000;\n keepAliveMessage = message;\n requestTimeout = timeout * 1000;\n start();\n };\n\n return {\n keepalive: keepalive,\n init: init\n };\n});\n"],"file":"network.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/network.js"],"names":["define","$","Ajax","Config","Notification","Str","started","warningDisplayed","keepAliveFrequency","requestTimeout","keepAliveMessage","sessionTimeout","checkFrequency","Math","min","sessiontimeout","warningLimit","sessiontimeoutwarning","firstWait","timeoutSessionExpired","modal","closeModal","displaySessionExpired","destroy","call","methodname","args","then","timeremaining","get_strings","key","component","strings","confirm","location","reload","catch","exception","touchSession","setTimeout","alert","checkSession","userid","start","init","keepalive","freq","timeout","message"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Poll the server to keep the session alive.\n *\n * @module core/network\n * @package core\n * @copyright 2019 Damyon Wiese\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/ajax', 'core/config', 'core/notification', 'core/str'],\n function($, Ajax, Config, Notification, Str) {\n\n var started = false;\n var warningDisplayed = false;\n var keepAliveFrequency = 0;\n var requestTimeout = 0;\n var keepAliveMessage = false;\n var sessionTimeout = false;\n // 1/10 of session timeout, max of 10 minutes.\n var checkFrequency = Math.min((Config.sessiontimeout / 10), 600) * 1000;\n // Check if sessiontimeoutwarning is set or double the checkFrequency.\n var warningLimit = (Config.sessiontimeoutwarning > 0) ? (Config.sessiontimeoutwarning * 1000) : (checkFrequency * 2);\n // First wait is minimum of remaining time or half of the session timeout.\n var firstWait = (Config.sessiontimeoutwarning > 0) ?\n Math.min((Config.sessiontimeout - Config.sessiontimeoutwarning) * 1000, checkFrequency * 5) : checkFrequency * 5;\n /**\n * The session time has expired - we can't extend it now.\n * @param {Modal} modal\n */\n var timeoutSessionExpired = function(modal) {\n sessionTimeout = true;\n warningDisplayed = false;\n closeModal(modal);\n displaySessionExpired();\n };\n\n /**\n * Close modal - this relies on modal object passed from Notification.confirm.\n *\n * @param {Modal} modal\n */\n var closeModal = function(modal) {\n modal.destroy();\n };\n\n /**\n * The session time has expired - we can't extend it now.\n * @return {Promise}\n */\n var displaySessionExpired = function() {\n // Check again if its already extended before displaying session expired popup in case multiple tabs are open.\n var request = {\n methodname: 'core_session_time_remaining',\n args: { }\n };\n\n return Ajax.call([request], true, true, true)[0].then(function(args) {\n if (args.timeremaining * 1000 > warningLimit) {\n return false;\n } else {\n return Str.get_strings([\n {key: 'sessionexpired', component: 'error'},\n {key: 'sessionerroruser', component: 'error'},\n {key: 'loginagain', component: 'moodle'},\n {key: 'cancel', component: 'moodle'}\n ]).then(function(strings) {\n Notification.confirm(\n strings[0], // Title.\n strings[1], // Message.\n strings[2], // Login Again.\n strings[3], // Cancel.\n function() {\n location.reload();\n return true;\n }\n );\n return true;\n }).catch(Notification.exception);\n }\n });\n };\n\n /**\n * Ping the server to keep the session alive.\n *\n * @return {Promise}\n */\n var touchSession = function() {\n var request = {\n methodname: 'core_session_touch',\n args: { }\n };\n\n if (sessionTimeout) {\n // We timed out before we extended the session.\n return displaySessionExpired();\n } else {\n return Ajax.call([request], true, true, false, requestTimeout)[0].then(function() {\n if (keepAliveFrequency > 0) {\n setTimeout(touchSession, keepAliveFrequency);\n }\n return true;\n }).catch(function() {\n Notification.alert('', keepAliveMessage);\n });\n }\n };\n\n /**\n * Ask the server how much time is remaining in this session and\n * show confirm/cancel notifications if the session is about to run out.\n *\n * @return {Promise}\n */\n var checkSession = function() {\n var request = {\n methodname: 'core_session_time_remaining',\n args: { }\n };\n sessionTimeout = false;\n return Ajax.call([request], true, true, true)[0].then(function(args) {\n if (args.userid <= 0) {\n return false;\n }\n if (args.timeremaining <= 0) {\n return displaySessionExpired();\n } else if (args.timeremaining * 1000 <= warningLimit && !warningDisplayed) {\n warningDisplayed = true;\n Str.get_strings([\n {key: 'norecentactivity', component: 'moodle'},\n {key: 'sessiontimeoutsoon', component: 'moodle'},\n {key: 'extendsession', component: 'moodle'},\n {key: 'cancel', component: 'moodle'}\n ]).then(function(strings) {\n return Notification.confirm(\n strings[0], // Title.\n strings[1], // Message.\n strings[2], // Extend session.\n strings[3], // Cancel.\n function() {\n touchSession();\n warningDisplayed = false;\n // First wait is minimum of remaining time or half of the session timeout.\n setTimeout(checkSession, firstWait);\n return true;\n },\n function() {\n // User has cancelled notification.\n setTimeout(checkSession, checkFrequency);\n }\n );\n }).then(modal => {\n // If we don't extend the session before the timeout - warn.\n setTimeout(timeoutSessionExpired, args.timeremaining * 1000, modal);\n return;\n }).catch(Notification.exception);\n } else {\n setTimeout(checkSession, checkFrequency);\n }\n return true;\n });\n // We do not catch the fails from the above ajax call because they will fail when\n // we are not logged in - we don't need to take any action then.\n };\n\n /**\n * Start calling a function to check if the session is still alive.\n */\n var start = function() {\n if (keepAliveFrequency > 0) {\n setTimeout(touchSession, keepAliveFrequency);\n } else {\n // First wait is minimum of remaining time or half of the session timeout.\n setTimeout(checkSession, firstWait);\n }\n };\n\n /**\n * Don't allow more than one of these polling loops in a single page.\n */\n var init = function() {\n // We only allow one concurrent instance of this checker.\n if (started) {\n return;\n }\n started = true;\n\n start();\n };\n\n /**\n * Start polling with more specific values for the frequency, timeout and message.\n *\n * @param {number} freq How ofter to poll the server.\n * @param {number} timeout The time to wait for each request to the server.\n * @param {string} message The message to display if the session is going to time out.\n */\n var keepalive = function(freq, timeout, message) {\n // We only allow one concurrent instance of this checker.\n if (started) {\n return;\n }\n started = true;\n\n keepAliveFrequency = freq * 1000;\n keepAliveMessage = message;\n requestTimeout = timeout * 1000;\n start();\n };\n\n return {\n keepalive: keepalive,\n init: init\n };\n});\n"],"file":"network.min.js"} \ No newline at end of file diff --git a/lib/amd/src/network.js b/lib/amd/src/network.js index 5e21a6a6fba..fcf1a0bfefe 100644 --- a/lib/amd/src/network.js +++ b/lib/amd/src/network.js @@ -32,14 +32,66 @@ define(['jquery', 'core/ajax', 'core/config', 'core/notification', 'core/str'], var sessionTimeout = false; // 1/10 of session timeout, max of 10 minutes. var checkFrequency = Math.min((Config.sessiontimeout / 10), 600) * 1000; - // 1/5 of sessiontimeout. - var warningLimit = checkFrequency * 2; + // Check if sessiontimeoutwarning is set or double the checkFrequency. + var warningLimit = (Config.sessiontimeoutwarning > 0) ? (Config.sessiontimeoutwarning * 1000) : (checkFrequency * 2); + // First wait is minimum of remaining time or half of the session timeout. + var firstWait = (Config.sessiontimeoutwarning > 0) ? + Math.min((Config.sessiontimeout - Config.sessiontimeoutwarning) * 1000, checkFrequency * 5) : checkFrequency * 5; + /** + * The session time has expired - we can't extend it now. + * @param {Modal} modal + */ + var timeoutSessionExpired = function(modal) { + sessionTimeout = true; + warningDisplayed = false; + closeModal(modal); + displaySessionExpired(); + }; + + /** + * Close modal - this relies on modal object passed from Notification.confirm. + * + * @param {Modal} modal + */ + var closeModal = function(modal) { + modal.destroy(); + }; /** * The session time has expired - we can't extend it now. + * @return {Promise} */ - var timeoutSessionExpired = function() { - sessionTimeout = true; + var displaySessionExpired = function() { + // Check again if its already extended before displaying session expired popup in case multiple tabs are open. + var request = { + methodname: 'core_session_time_remaining', + args: { } + }; + + return Ajax.call([request], true, true, true)[0].then(function(args) { + if (args.timeremaining * 1000 > warningLimit) { + return false; + } else { + return Str.get_strings([ + {key: 'sessionexpired', component: 'error'}, + {key: 'sessionerroruser', component: 'error'}, + {key: 'loginagain', component: 'moodle'}, + {key: 'cancel', component: 'moodle'} + ]).then(function(strings) { + Notification.confirm( + strings[0], // Title. + strings[1], // Message. + strings[2], // Login Again. + strings[3], // Cancel. + function() { + location.reload(); + return true; + } + ); + return true; + }).catch(Notification.exception); + } + }); }; /** @@ -55,23 +107,14 @@ define(['jquery', 'core/ajax', 'core/config', 'core/notification', 'core/str'], if (sessionTimeout) { // We timed out before we extended the session. - return Str.get_strings([ - {key: 'sessionexpired', component: 'error'}, - {key: 'sessionerroruser', component: 'error'} - ]).then(function(strings) { - Notification.alert( - strings[0], // Title. - strings[1] // Message. - ); - return true; - }).fail(Notification.exception); + return displaySessionExpired(); } else { return Ajax.call([request], true, true, false, requestTimeout)[0].then(function() { if (keepAliveFrequency > 0) { setTimeout(touchSession, keepAliveFrequency); } return true; - }).fail(function() { + }).catch(function() { Notification.alert('', keepAliveMessage); }); } @@ -88,27 +131,14 @@ define(['jquery', 'core/ajax', 'core/config', 'core/notification', 'core/str'], methodname: 'core_session_time_remaining', args: { } }; - sessionTimeout = false; return Ajax.call([request], true, true, true)[0].then(function(args) { if (args.userid <= 0) { return false; } - if (args.timeremaining < 0) { - Str.get_strings([ - {key: 'sessionexpired', component: 'error'}, - {key: 'sessionerroruser', component: 'error'} - ]).then(function(strings) { - Notification.alert( - strings[0], // Title. - strings[1] // Message. - ); - return true; - }).fail(Notification.exception); - - } else if (args.timeremaining * 1000 < warningLimit && !warningDisplayed) { - // If we don't extend the session before the timeout - warn. - setTimeout(timeoutSessionExpired, args.timeremaining * 1000); + if (args.timeremaining <= 0) { + return displaySessionExpired(); + } else if (args.timeremaining * 1000 <= warningLimit && !warningDisplayed) { warningDisplayed = true; Str.get_strings([ {key: 'norecentactivity', component: 'moodle'}, @@ -116,7 +146,7 @@ define(['jquery', 'core/ajax', 'core/config', 'core/notification', 'core/str'], {key: 'extendsession', component: 'moodle'}, {key: 'cancel', component: 'moodle'} ]).then(function(strings) { - Notification.confirm( + return Notification.confirm( strings[0], // Title. strings[1], // Message. strings[2], // Extend session. @@ -124,17 +154,20 @@ define(['jquery', 'core/ajax', 'core/config', 'core/notification', 'core/str'], function() { touchSession(); warningDisplayed = false; - // First wait is half the session timeout. - setTimeout(checkSession, checkFrequency * 5); + // First wait is minimum of remaining time or half of the session timeout. + setTimeout(checkSession, firstWait); return true; }, function() { - warningDisplayed = false; + // User has cancelled notification. setTimeout(checkSession, checkFrequency); } ); - return true; - }).fail(Notification.exception); + }).then(modal => { + // If we don't extend the session before the timeout - warn. + setTimeout(timeoutSessionExpired, args.timeremaining * 1000, modal); + return; + }).catch(Notification.exception); } else { setTimeout(checkSession, checkFrequency); } @@ -151,8 +184,8 @@ define(['jquery', 'core/ajax', 'core/config', 'core/notification', 'core/str'], if (keepAliveFrequency > 0) { setTimeout(touchSession, keepAliveFrequency); } else { - // First wait is half the session timeout. - setTimeout(checkSession, checkFrequency * 5); + // First wait is minimum of remaining time or half of the session timeout. + setTimeout(checkSession, firstWait); } }; diff --git a/lib/outputrequirementslib.php b/lib/outputrequirementslib.php index 7bca2a770a9..d18b9f503f7 100644 --- a/lib/outputrequirementslib.php +++ b/lib/outputrequirementslib.php @@ -319,20 +319,21 @@ class page_requirements_manager { } $this->M_cfg = array( - 'wwwroot' => $CFG->wwwroot, - 'sesskey' => sesskey(), - 'sessiontimeout' => $CFG->sessiontimeout, - 'themerev' => theme_get_revision(), - 'slasharguments' => (int)(!empty($CFG->slasharguments)), - 'theme' => $page->theme->name, - 'iconsystemmodule' => $iconsystem->get_amd_name(), - 'jsrev' => $this->get_jsrev(), - 'admin' => $CFG->admin, - 'svgicons' => $page->theme->use_svg_icons(), - 'usertimezone' => usertimezone(), - 'contextid' => $contextid, - 'langrev' => get_string_manager()->get_revision(), - 'templaterev' => $this->get_templaterev() + 'wwwroot' => $CFG->wwwroot, + 'sesskey' => sesskey(), + 'sessiontimeout' => $CFG->sessiontimeout, + 'sessiontimeoutwarning' => $CFG->sessiontimeoutwarning, + 'themerev' => theme_get_revision(), + 'slasharguments' => (int)(!empty($CFG->slasharguments)), + 'theme' => $page->theme->name, + 'iconsystemmodule' => $iconsystem->get_amd_name(), + 'jsrev' => $this->get_jsrev(), + 'admin' => $CFG->admin, + 'svgicons' => $page->theme->use_svg_icons(), + 'usertimezone' => usertimezone(), + 'contextid' => $contextid, + 'langrev' => get_string_manager()->get_revision(), + 'templaterev' => $this->get_templaterev() ); if ($CFG->debugdeveloper) { $this->M_cfg['developerdebug'] = true; diff --git a/lib/setup.php b/lib/setup.php index 1326f1327c2..210bdeef5d2 100644 --- a/lib/setup.php +++ b/lib/setup.php @@ -801,6 +801,10 @@ if (CLI_SCRIPT) { if (empty($CFG->sessiontimeout)) { $CFG->sessiontimeout = 8 * 60 * 60; } +// Set sessiontimeoutwarning 20 minutes. +if (empty($CFG->sessiontimeoutwarning)) { + $CFG->sessiontimeoutwarning = 20 * 60; +} \core\session\manager::start(); // Set default content type and encoding, developers are still required to use