Skip to content

Commit

Permalink
MDL-34498 session: Add a checker to warn before session expiry
Browse files Browse the repository at this point in the history
Replaces old yui module checknet.
  • Loading branch information
Damyon Wiese committed Jun 6, 2019
1 parent ba2473e commit 7aeeb44
Show file tree
Hide file tree
Showing 25 changed files with 369 additions and 749 deletions.
1 change: 1 addition & 0 deletions lang/en/error.php
Expand Up @@ -496,6 +496,7 @@
$string['serverconnection'] = 'Error connecting to the server';
$string['servicedonotexist'] = 'The service does not exist';
$string['sessionwaiterr'] = 'Timed out while waiting for session lock.<br />Wait for your current requests to finish and try again later.';
$string['sessionexpired'] = 'Session expired';
$string['sessioncookiesdisable'] = 'Incorrect use of require_key_login() - session cookies must be disabled!';
$string['sessiondiskfull'] = 'The session partition is full. It is not possible to log in at this time. Please notify the server administrator.';
$string['sessionhandlerproblem'] = 'Session handler is misconfigured';
Expand Down
4 changes: 3 additions & 1 deletion lang/en/moodle.php
Expand Up @@ -802,6 +802,7 @@
$string['existingcreators'] = 'Existing course creators';
$string['existingstudents'] = 'Enrolled students';
$string['existingteachers'] = 'Existing teachers';
$string['extendsession'] = 'Extend session';
$string['expand'] = 'Expand';
$string['expandall'] = 'Expand all';
$string['expandcategory'] = 'Expand {$a}';
Expand Down Expand Up @@ -1807,7 +1808,8 @@
$string['separateandconnectedinfo'] = 'The scale based on the theory of separate and connected knowing. This theory describes two different ways that we can evaluate and learn about the things we see and hear.<ul><li><strong>Separate knowers</strong> remain as objective as possible without including feelings and emotions. In a discussion with other people, they like to defend their own ideas, using logic to find holes in opponent\'s ideas.</li><li><strong>Connected knowers</strong> are more sensitive to other people. They are skilled at empathy and tend to listen and ask questions until they feel they can connect and "understand things from their point of view". They learn by trying to share the experiences that led to the knowledge they find in other people.</li></ul>';
$string['servererror'] = 'An error occurred whilst communicating with the server';
$string['serverlocaltime'] = 'Server\'s local time';
$string['sessionforceclean'] = 'As a security precaution, user-generated scripts have been disabled within this session.';
$string['sessionforceclean'] = 'As a security precaution, user-generated scripts have been disabled within this session';
$string['sessiontimeoutsoon'] = 'Your session is about to timeout. Do you want to extend your current session?';
$string['setcategorytheme'] = 'Set category theme';
$string['setpassword'] = 'Set password';
$string['setpasswordinstructions'] = 'Please enter your new password below, then save changes.';
Expand Down
5 changes: 4 additions & 1 deletion lib/ajax/service.php
Expand Up @@ -29,6 +29,10 @@

define('AJAX_SCRIPT', true);

if (!empty($_GET['nosessionupdate'])) {
define('NO_SESSION_UPDATE', true);
}

require_once(__DIR__ . '/../../config.php');
require_once($CFG->libdir . '/externallib.php');

Expand Down Expand Up @@ -63,5 +67,4 @@
break;
}
}

echo json_encode($responses);
2 changes: 1 addition & 1 deletion lib/amd/build/ajax.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/amd/build/network.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/amd/build/page_global.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 26 additions & 8 deletions lib/amd/src/ajax.js
Expand Up @@ -40,11 +40,12 @@ define(['jquery', 'core/config', 'core/log', 'core/url'], function($, config, Lo
*/
var requestSuccess = function(responses) {
// Call each of the success handlers.
var requests = this;
var exception = null;
var i = 0;
var request;
var response;
var requests = this,
exception = null,
i = 0,
request,
response,
nosessionupdate;

if (responses.error) {
// There was an error with the request as a whole.
Expand All @@ -69,6 +70,7 @@ define(['jquery', 'core/config', 'core/log', 'core/url'], function($, config, Lo
request.deferred.resolve(response.data);
} else {
exception = response.exception;
nosessionupdate = requests[i].nosessionupdate;
break;
}
} else {
Expand All @@ -80,7 +82,7 @@ define(['jquery', 'core/config', 'core/log', 'core/url'], function($, config, Lo
// Something failed, reject the remaining promises.
if (exception !== null) {
// Redirect to the login page.
if (exception.errorcode === "servicerequireslogin") {
if (exception.errorcode === "servicerequireslogin" && !nosessionupdate) {
window.location = URL.relativeUrl("/login/index.php");
} else {
requests.forEach(function(request) {
Expand Down Expand Up @@ -132,9 +134,12 @@ define(['jquery', 'core/config', 'core/log', 'core/url'], function($, config, Lo
* If false - this function will call the faster nologin ajax script - but
* will fail unless all functions have been marked as 'loginrequired' => false
* in services.php
* @param {Boolean} nosessionupdate Optional, defaults to false.
* If true, the timemodified for the session will not be updated.
* @param {Integer} timeout number of milliseconds to wait for a response. Defaults to no limit.
* @return {Promise[]} Array of promises that will be resolved when the ajax call returns.
*/
call: function(requests, async, loginrequired) {
call: function(requests, async, loginrequired, nosessionupdate, timeout) {
$(window).bind('beforeunload', function() {
unloading = true;
});
Expand All @@ -150,13 +155,21 @@ define(['jquery', 'core/config', 'core/log', 'core/url'], function($, config, Lo
if (typeof async === "undefined") {
async = true;
}
if (typeof timeout === 'undefined') {
timeout = 0;
}

if (typeof nosessionupdate === "undefined") {
nosessionupdate = false;
}
for (i = 0; i < requests.length; i++) {
var request = requests[i];
ajaxRequestData.push({
index: i,
methodname: request.methodname,
args: request.args
});
request.nosessionupdate = nosessionupdate;
request.deferred = $.Deferred();
promises.push(request.deferred.promise());
// Allow setting done and fail handlers as arguments.
Expand Down Expand Up @@ -185,7 +198,8 @@ define(['jquery', 'core/config', 'core/log', 'core/url'], function($, config, Lo
dataType: 'json',
processData: false,
async: async,
contentType: "application/json"
contentType: "application/json",
timeout: timeout
};

var script = 'service.php';
Expand All @@ -195,6 +209,10 @@ define(['jquery', 'core/config', 'core/log', 'core/url'], function($, config, Lo
var url = config.wwwroot + '/lib/ajax/' + script +
'?sesskey=' + config.sesskey + '&info=' + requestInfo;

if (nosessionupdate) {
url += '&nosessionupdate=true';
}

// Jquery deprecated done and fail with async=false so we need to do this 2 ways.
if (async) {
$.ajax(url, settings)
Expand Down
169 changes: 169 additions & 0 deletions lib/amd/src/network.js
@@ -0,0 +1,169 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

/**
* Poll the server to keep the session alive.
*
* @module core/network
* @package core
* @copyright 2019 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['jquery', 'core/ajax', 'core/config', 'core/notification', 'core/str'],
function($, Ajax, Config, Notification, Str) {

var started = false;
var warningDisplayed = false;
var keepAliveFrequency = 0;
var requestTimeout = 0;
var keepAliveMessage = false;
var checkFrequency = (Config.sessiontimeout / 10) * 1000;
var warningLimit = checkFrequency * 2; // 1/5 of sessiontimeout.

/**
* Ping the server to keep the session alive.
*
* @return {Promise}
*/
var touchSession = function() {
var request = {
methodname: 'core_session_touch',
args: { }
};

return Ajax.call([request], true, true, false, requestTimeout)[0].then(function() {
if (keepAliveFrequency > 0) {
setTimeout(touchSession, keepAliveFrequency);
}
return true;
}).fail(function() {
Notification.alert('', keepAliveMessage);
});
};

/**
* Ask the server how much time is remaining in this session and
* show confirm/cancel notifications if the session is about to run out.
*
* @return {Promise}
*/
var checkSession = function() {
var request = {
methodname: 'core_session_time_remaining',
args: { }
};

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) {
warningDisplayed = true;
Str.get_strings([
{key: 'norecentactivity', component: 'moodle'},
{key: 'sessiontimeoutsoon', component: 'moodle'},
{key: 'extendsession', component: 'moodle'},
{key: 'cancel', component: 'moodle'}
]).then(function(strings) {
Notification.confirm(
strings[0], // Title.
strings[1], // Message.
strings[2], // Extend session.
strings[3], // Cancel.
function() {
touchSession();
warningDisplayed = false;
// First wait is half the session timeout.
setTimeout(checkSession, checkFrequency * 5);
return true;
},
function() {
warningDisplayed = false;
setTimeout(checkSession, checkFrequency);
}
);
return true;
}).fail(Notification.exception);
} else {
setTimeout(checkSession, checkFrequency);
}
return true;
});
// We do not catch the fails from the above ajax call because they will fail when
// we are not logged in - we don't need to take any action then.
};

/**
* Start calling a function to check if the session is still alive.
*/
var start = function() {
if (keepAliveFrequency > 0) {
setTimeout(touchSession, keepAliveFrequency);
} else {
// First wait is half the session timeout.
setTimeout(checkSession, checkFrequency * 5);
}
};

/**
* Don't allow more than one of these polling loops in a single page.
*/
var init = function() {
// We only allow one concurrent instance of this checker.
if (started) {
return;
}
started = true;

start();
};

/**
* Start polling with more specific values for the frequency, timeout and message.
*
* @param {number} freq How ofter to poll the server.
* @param {number} timeout The time to wait for each request to the server.
* @param {string} message The message to display if the session is going to time out.
*/
var keepalive = function(freq, timeout, message) {
// We only allow one concurrent instance of this checker.
if (started) {
return;
}
started = true;

keepAliveFrequency = freq * 1000;
keepAliveMessage = message;
requestTimeout = timeout * 1000;
start();
};

return {
keepalive: keepalive,
init: init
};
});
5 changes: 4 additions & 1 deletion lib/amd/src/page_global.js
Expand Up @@ -26,11 +26,13 @@ define(
'jquery',
'core/custom_interaction_events',
'core/str',
'core/network'
],
function(
$,
CustomEvents,
Str
Str,
Network
) {

/**
Expand Down Expand Up @@ -127,6 +129,7 @@ function(
*/
var init = function() {
initActionOptionDropdownHandler();
Network.init();
};

return {
Expand Down

0 comments on commit 7aeeb44

Please sign in to comment.