/* This file is part of Tryton. The COPYRIGHT file at the top level of this repository contains the full copyright notices and license terms. */ (function() { 'use strict'; // https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/btoa#Unicode_strings function utoa(str) { return window.btoa(unescape(encodeURIComponent(str))); } Sao.Session = Sao.class_(Object, { init: function(database, login) { this.login_service = null; this.user_id = null; this.session = null; this.cache = new Cache(); this.prm = jQuery.when(); // renew promise this.database = database; this.login = login; this.restore(); this.context = {}; this.restore_context(); if (!Sao.Session.current_session) { Sao.Session.current_session = this; } }, get_auth: function() { return utoa(this.login + ':' + this.user_id + ':' + this.session); }, do_login: function(parameters) { var dfd = jQuery.Deferred(); var login = this.login; var device_cookies = JSON.parse( localStorage.getItem('sao_device_cookies')); var device_cookie = null; if (device_cookies && device_cookies[this.database]) { device_cookie = device_cookies[this.database][this.login]; } var func = function(parameters) { parameters.device_cookie = device_cookie; return { 'method': 'common.db.login', 'params': [login, parameters, Sao.i18n.getlang()] }; }; new Sao.Login(func, this).run().then(result => { this.login = login; this.user_id = result[0]; this.session = result[1]; this.store(); this.renew_device_cookie(); dfd.resolve(); }, () => { this.user_id = null; this.session = null; this.store(); dfd.reject(); }); return dfd.promise(); }, do_logout: function() { if (!(this.user_id && this.session)) { return jQuery.when(); } var args = { 'id': 0, 'method': 'common.db.logout', 'params': [] }; var prm = jQuery.ajax({ 'headers': { 'Authorization': 'Session ' + this.get_auth() }, 'contentType': 'application/json', 'data': JSON.stringify(args), 'dataType': 'json', 'url': '/' + this.database + '/', 'type': 'post', }); this.unstore(); this.database = null; this.login = null; this.user_id = null; this.session = null; if (Sao.Session.current_session === this) { Sao.Session.current_session = null; } return prm; }, reload_context: function() { var args = { 'method': 'model.res.user.get_preferences', 'params': [true, this.context] }; this.reset_context(); var prm = Sao.rpc(args, this); return prm.then(context => { jQuery.extend(this.context, context); this.store_context(); }); }, reset_context: function() { this.context = { client: Sao.Bus.id, }; }, restore_context: function() { this.reset_context(); var context = sessionStorage.getItem('sao_context_' + this.database); if (context !== null) { jQuery.extend( this.context, Sao.rpc.convertJSONObject(JSON.parse(context))); } }, store_context: function() { var context = jQuery.extend({}, this.context); delete context.client; context = JSON.stringify(Sao.rpc.prepareObject(context)); sessionStorage.setItem('sao_context_' + this.database, context); }, restore: function() { if (this.database && !this.session) { var session_data = localStorage.getItem( 'sao_session_' + this.database); if (session_data !== null) { session_data = JSON.parse(session_data); if (!this.login || this.login == session_data.login) { this.login_service = session_data.login_service; this.login = session_data.login; this.user_id = session_data.user_id; this.session = session_data.session; } } } }, store: function() { var session = { 'login': this.login, 'user_id': this.user_id, 'session': this.session, }; session = JSON.stringify(session); localStorage.setItem('sao_session_' + this.database, session); }, unstore: function() { localStorage.removeItem('sao_session_' + this.database); }, renew_device_cookie: function() { var device_cookie; var device_cookies = JSON.parse( localStorage.getItem('sao_device_cookies')); if (!device_cookies || !(this.database in device_cookies)) { device_cookie = null; } else { device_cookie = device_cookies[this.database][this.login]; } var renew_prm = Sao.rpc({ method: 'model.res.user.device.renew', params: [device_cookie, {}], }, this); renew_prm.done(result => { device_cookies = JSON.parse( localStorage.getItem('sao_device_cookies')); if (!device_cookies) { device_cookies = {}; } if (!(this.database in device_cookies)) { device_cookies[this.database] = {}; } device_cookies[this.database][this.login] = result; localStorage.setItem( 'sao_device_cookies', JSON.stringify(device_cookies)); }); renew_prm.fail(() => { Sao.error("Cannot renew device cookie"); }); } }); Sao.Session.server_version = function() { var timeoutID = Sao.common.processing.show(); return jQuery.ajax({ 'contentType': 'application/json', 'data': JSON.stringify({ 'id': 0, 'method': 'common.server.version', 'params': [] }), 'dataType': 'json', 'url': '/', 'type': 'post', 'complete': [function() { Sao.common.processing.hide(timeoutID); }] }).then(function(data) { return data.result; }); }; Sao.Session.login_dialog = function() { var dialog = new Sao.Dialog( Sao.i18n.gettext("Login"), 'login-dialog', 'md', true, Sao.__version__); dialog.database_select = jQuery('', { 'class': 'form-control', 'id': 'database', 'name': 'database', }).hide(); dialog.database_input = jQuery('', { 'class': 'form-control', 'id': 'database', 'name': 'database', }).hide(); dialog.login_input = jQuery('', { 'class': 'form-control', 'id': 'login', 'name': 'login', }); dialog.button = jQuery('', { 'class': 'btn btn-primary btn-block', 'type': 'submit', 'title': Sao.i18n.gettext("Login"), }).text(' ' + Sao.i18n.gettext("Login")); dialog.body.append(jQuery('
', { 'class': 'form-group' }).append(jQuery('', { 'class': 'control-label', 'for': 'database' }).text(Sao.i18n.gettext('Database'))) .append(dialog.database_select) .append(dialog.database_input)) .append(jQuery('', { 'class': 'panel panel-default', }) .append(jQuery('', { 'class': 'panel-body', }) .append(jQuery('', { 'class': 'form-group' }).append(jQuery('', { 'class': 'control-label', 'for': 'login' }).text(Sao.i18n.gettext('User name'))) .append(dialog.login_input) ) .append(dialog.button))); return dialog; }; Sao.Session.get_credentials = function() { var database_url = function() { return window.location.hash.replace( /^(#(!|))/, '').split('/', 1)[0] || null; }; var dfd = jQuery.Deferred(); var database = database_url(); var session = new Sao.Session(database, null); if (session.session) { dfd.resolve(session); return dfd; } var dialog = Sao.Session.login_dialog(); var empty_field = function() { return dialog.modal.find('input,select').filter(':visible:not([readonly])') .filter(function() { return !jQuery(this).val(); }); }; var disable_form = function(disabled=true) { dialog.body.find('input,select,button').prop('disabled', disabled); }; var login = function() { var login = dialog.login_input.val(); var database = database || dialog.database_select.val() || dialog.database_input.val(); dialog.modal.find('.has-error').removeClass('has-error'); if (!(login && database)) { empty_field().closest('.form-group').addClass('has-error'); return; } dialog.button.focus(); disable_form(); session.database = database; session.login = login; session.restore(); (session.session ? jQuery.when() : session.do_login()) .then(function() { dialog.modal.modal('hide'); dfd.resolve(session); dialog.modal.remove(); if (database_url() != database) { window.location = '#' + database; } }, function() { disable_form(false); empty_field().closest('.form-group').addClass('has-error'); empty_field().first().focus(); }); }; var login_service = function(evt) { var database = database || dialog.database_select.val() || dialog.database_input.val(); dialog.modal.find('.has-error').removeClass('has-error'); if (!database) { dialog.database_select .closest('.form-group').addClass('has-error'); return; } disable_form(); var host = window.location.protocol + '//' + window.location.host; var next = new URL(host + '/'); next.searchParams.append('login_service', evt.data); var url = new URL(host + '/' + database + evt.data); url.searchParams.append('next', next.href); var service_window = window.open(url.href, '_blank', 'popup=1'); const timer = window.setInterval(() => { if (service_window.closed) { window.clearInterval(timer); session.database = database; session.restore(); if (session.session) { dfd.resolve(session); dialog.modal.remove(); if (database_url() != database) { window.location = '#' + database; } } else { disable_form(false); empty_field().first().focus(); } } }, 500); }; dialog.modal.modal({ backdrop: false, keyboard: false }); dialog.modal.find('form').unbind().submit(function(e) { e.preventDefault(); login(); }); dialog.modal.on('shown.bs.modal', function() { empty_field().first().focus(); }); jQuery.when(Sao.DB.list()).then(function(databases) { var el; databases = databases || []; if (databases.length == 1 ) { database = databases[0]; el = dialog.database_input; } else { el = dialog.database_select; for (const database of databases) { el.append(jQuery('', { 'value': database, 'text': database })); } } el.prop('readonly', databases.length == 1); el.show(); el.val(database || ''); }, function() { dialog.database_input.show(); }); jQuery.when(Sao.Authentication.services()).then(function(services) { if (services.length) { var panel_body = jQuery('', { 'class': 'panel-body', }).append(jQuery('') .text(Sao.i18n.gettext("Login with"))); dialog.body.append(jQuery('', { 'class': 'panel panel-default', }).append(panel_body)); for (const [name, url] of services) { panel_body.append(jQuery('', { 'class': 'btn btn-block btn-default', 'type': 'button', }).text(name).click(url, login_service)); } } }); return dfd.promise(); }; Sao.Session.renew = function(session) { if (session.prm.state() == 'pending') { return session.prm; } var dfd = jQuery.Deferred(); session.session = null; session.prm = dfd.promise(); if (!session.login_service) { session.do_login().then(dfd.resolve, function() { Sao.logout(); dfd.reject(); }); } else { session.unstore(); var host = window.location.protocol + '//' + window.location.host; var next = new URL(host + '/'); next.searchParams.append('login_service', session.login_service); next.searchParams.append('renew', session.user_id); var url = new URL(host + '/' + session.database + session.login_service); url.searchParams.append('next', next.href); var service_window = window.open(url.href, '_blank', 'popup=1'); const timer = window.setInterval(() => { if (service_window.closed) { window.clearInterval(timer); session.restore(); if (session.session) { dfd.resolve(); } else { Sao.logout(); dfd.reject(); } } }, 500); } dfd.done(function() { Sao.Bus.listen(); }); return session.prm; }; Sao.Session.current_session = null; Sao.Login = Sao.class_(Object, { init: function(func, session) { this.func = func; this.session = session || Sao.Session.current_session; }, run: function(parameters={}) { var dfd = jQuery.Deferred(); var timeoutID = Sao.common.processing.show(); var data = this.func(parameters); data.id = 0; var args = { 'contentType': 'application/json', 'data': JSON.stringify(data), 'dataType': 'json', 'url': '/' + this.session.database + '/', 'type': 'post', 'complete': [function() { Sao.common.processing.hide(timeoutID); }] }; if (this.session.user_id && this.session.session) { args.headers = { 'Authorization': 'Session ' + this.session.get_auth() }; } var ajax_prm = jQuery.ajax(args); var ajax_success = function(data) { if (data === null) { Sao.common.warning.run('', Sao.i18n.gettext('Unable to reach the server.')); dfd.reject(); } else if (data.error) { if (data.error[0].startsWith('401')) { return this.run({}).then(dfd.resolve, dfd.reject); } else if (data.error[0].startsWith('429')) { Sao.common.message.run( Sao.i18n.gettext('Too many requests. Try again later.'), 'tryton-error').always(dfd.resolve); } else if (data.error[0].startsWith('404')) { Sao.common.message.run( Sao.i18n.gettext("Not found."), 'tryton-error').always(dfd.reject); } else if (data.error[0] != 'LoginException') { Sao.common.error.run(data.error[0], data.error[1]) .always(dfd.reject); } else { var args = data.error[1]; var name = args[0]; var message = args[1]; var type = args[2]; this['get_' + type](message, name).then(value => { parameters[name] = value; return this.run(parameters).then( dfd.resolve, dfd.reject); }, dfd.reject); } } else { dfd.resolve(data.result); } }; var ajax_error = function(query, status_, error) { if (query.status == 401) { // Retry this.run({}).then(dfd.resolve, dfd.reject); } else if (query.status == 429) { Sao.common.message.run( Sao.i18n.gettext('Too many requests. Try again later.'), 'tryton-error').always(dfd.resolve); } else if (query.status == 404) { Sao.common.message.run( Sao.i18n.gettext("Not found."), 'tryton-error').always(dfd.reject); } else { Sao.common.error.run(status_, error).always(dfd.reject); } }; ajax_prm.done(ajax_success.bind(this)); ajax_prm.fail(ajax_error.bind(this)); return dfd.promise(); }, get_char: function(message, name) { return Sao.common.ask.run(message, name); }, get_password: function(message, name) { return Sao.common.ask.run(message, name, false); }, }); var Cache = Sao.class_(Object, { init: function() { this.store = {}; }, cached: function(prefix) { return prefix in this.store; }, set: function(prefix, key, expire, value) { expire = new Date(new Date().getTime() + expire * 1000); Sao.setdefault(this.store, prefix, {})[key] = { 'expire': expire, 'value': JSON.stringify(Sao.rpc.prepareObject(value)), }; }, get: function(prefix, key) { var now = new Date(); var data = Sao.setdefault(this.store, prefix, {})[key]; if (!data) { return undefined; } if (data.expire < now) { delete this.store[prefix][key]; return undefined; } Sao.Logger.info("(cached)", prefix, key); return Sao.rpc.convertJSONObject(jQuery.parseJSON(data.value)); }, clear: function(prefix) { if (prefix) { this.store[prefix] = {}; } else { this.store = {}; } }, }); Sao.DB = {}; Sao.DB.list = function() { var timeoutID = Sao.common.processing.show(); return jQuery.ajax({ 'contentType': 'application/json', 'data': JSON.stringify({ 'id': 0, 'method': 'common.db.list', 'params': [] }), 'dataType': 'json', 'url': '/', 'type': 'post', 'complete': [function() { Sao.common.processing.hide(timeoutID); }] }).then(function(data) { return data.result; }); }; Sao.Authentication = {}; Sao.Authentication.services = function() { var timeoutID = Sao.common.processing.show(); return jQuery.ajax({ 'contentType': 'application/json', 'data': JSON.stringify({ 'id': 0, 'method': 'common.authentication.services', 'params': [] }), 'dataType': 'json', 'url': '/', 'type': 'post', 'complete': [function() { Sao.common.processing.hide(timeoutID); }] }).then(function(data) { Sao.Authentication.services = function() { return data.result; }; return data.result; }); }; }());