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.
This commit is contained in:
Heena Agheda 2020-03-26 15:02:02 +11:00
parent 4ec279a2f0
commit 9c5dc8fc7d
9 changed files with 151 additions and 55 deletions

View File

@ -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'),

View File

@ -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. <strong>WARNING: it is strongly recommended to leave this setting at the default (empty) - an incorrect value will prevent all logins to the site.</strong>';
$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 <strong>{$a}</strong>.';
$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.';

View File

@ -1180,6 +1180,7 @@ $string['loginstepsnone'] = '<p>Hi!</p>
<p>All you need to do is make up a username and password and use it in the form on this page!</p>
<p>If someone else has already chosen your username then you\'ll have to try again using a different username.</p>';
$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';

View File

@ -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'));
}

View File

@ -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<h){setTimeout(n,h)}return!0}).fail(function(){d.alert("",j)})}},o=function(){k=!1;return b.call([{methodname:"core_session_time_remaining",args:{}}],!0,!0,!0)[0].then(function(a){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(0<h){setTimeout(n,h)}else{setTimeout(o,5*l)}},q=function(){if(f){return}f=!0;p()},r=function(a,b,c){if(f){return}f=!0;h=1e3*a;j=c;i=1e3*b;p()};return{keepalive:r,init:q}});
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=0<c.sessiontimeoutwarning?1e3*c.sessiontimeoutwarning:2*l,n=0<c.sessiontimeoutwarning?Math.min(1e3*(c.sessiontimeout-c.sessiontimeoutwarning),5*l):5*l,o=function(a){k=!0;g=!1;p(a);q()},p=function(a){a.destroy()},q=function(){return b.call([{methodname:"core_session_time_remaining",args:{}}],!0,!0,!0)[0].then(function(a){if(1e3*a.timeremaining>m){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<h){setTimeout(r,h)}return!0}).catch(function(){d.alert("",j)})}},s=function(){k=!1;return b.call([{methodname:"core_session_time_remaining",args:{}}],!0,!0,!0)[0].then(function(a){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<h){setTimeout(r,h)}else{setTimeout(s,n)}},u=function(){if(f){return}f=!0;t()},v=function(a,b,c){if(f){return}f=!0;h=1e3*a;j=c;i=1e3*b;t()};return{keepalive:v,init:u}});
//# sourceMappingURL=network.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -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);
}
};

View File

@ -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;

View File

@ -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