From 2f155e0d79f7a86f7a8b724f9473b3c0a97f432c Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Tue, 13 Jul 2021 20:54:08 +0200 Subject: [PATCH 01/11] fix popups of apps without app.ts/js wont work as eT2 is not available it get's included by the app.ts/js otherwise --- api/src/Etemplate.php | 5 +++++ api/src/Framework/Bundle.php | 2 +- rollup.config.js | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/api/src/Etemplate.php b/api/src/Etemplate.php index 5bf84b6651..0631d9e3b7 100644 --- a/api/src/Etemplate.php +++ b/api/src/Etemplate.php @@ -239,6 +239,11 @@ class Etemplate extends Etemplate\Widget\Template { Framework::includeJS($path); } + // if app has no app.ts/js, we need to load etemplate2.js, otherwise popups wont work! + else + { + Framework::includeJS('/api/js/etemplate/etemplate2.js'); + } // Category styles Categories::css($app); diff --git a/api/src/Framework/Bundle.php b/api/src/Framework/Bundle.php index 2131bfe80d..7b4d6bda89 100644 --- a/api/src/Framework/Bundle.php +++ b/api/src/Framework/Bundle.php @@ -119,7 +119,7 @@ class Bundle $mod = $min_mod; } // use cache-buster only for entry-points / app.js, as the have no hash - if (preg_match('#/js/app(\.min)?\.js$#', $file)) + if (preg_match('#/js/(app(\.min)?|etemplate/etemplate2)\.js$#', $file)) { $to_include[$file] = $path.'?'.$mod.($query ? '&'.$query : ''); } diff --git a/rollup.config.js b/rollup.config.js index 2ba5ced810..c55223a1dd 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -31,6 +31,7 @@ const config = { "pixelegg/js/fw_pixelegg.min": "pixelegg/js/fw_pixelegg.js", "pixelegg/js/fw_mobile.min": "pixelegg/js/fw_mobile.js", "api/js/jsapi/egw.min": "api/js/jsapi/egw_modules.js", + "api/js/etemplate/etemplate2": "api/js/etemplate/etemplate2.ts", // app.ts/js are added automatic by addAppsConfig() below }, From b780c7ebd874f7503d0035537daf9f5469545e05 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Fri, 16 Jul 2021 08:47:22 +0200 Subject: [PATCH 02/11] replace jQuery.ajax with fetch (or XMLHttpRequest for synchronous requests) --- api/js/etemplate/et2_core_xml.ts | 40 ++++++++------ api/js/jsapi/egw_json.js | 92 ++++++++++++++++++++++---------- 2 files changed, 88 insertions(+), 44 deletions(-) diff --git a/api/js/etemplate/et2_core_xml.ts b/api/js/etemplate/et2_core_xml.ts index fd3737e851..04d06f2f7b 100644 --- a/api/js/etemplate/et2_core_xml.ts +++ b/api/js/etemplate/et2_core_xml.ts @@ -47,26 +47,34 @@ export function et2_loadXMLFromURL(_url : string, _callback? : Function, _contex { win = egw.top; } - return win.jQuery.ajax({ - // we add the full url (protocol and domain) as sometimes just the path - // gives a CSP error interpreting it as file:///path - // (if there are a enough 404 errors in html content ...) - url: (_url[0]=='/' ? location.protocol+'//'+location.host : '')+_url, - context: _context, - type: 'GET', - dataType: 'xml', - success: function(_data, _status, _xmlhttp){ - if (typeof _callback === 'function') { - _callback.call(_context, _data.documentElement); + // we add the full url (protocol and domain) as sometimes just the path + // gives a CSP error interpreting it as file:///path + // (if there are a enough 404 errors in html content ...) + return win.fetch((_url[0] === '/' ? location.protocol+'//'+location.host : '')+_url, { + method: 'GET' + }) + .then((response) => { + if (!response.ok) { + throw response; } - }, - error: function(_xmlhttp, _err) { - egw().debug('error', 'Loading eTemplate from '+_url+' failed! '+_xmlhttp.status+' '+_xmlhttp.statusText); + return response.text(); + }) + .then((xml) => { + const parser = new window.DOMParser(); + return parser.parseFromString( xml, "text/xml" ); + }) + .then((xmldoc) => { + if (typeof _callback === 'function') { + _callback.call(_context, xmldoc.children[0]); + } + return xmldoc.children[0]; + }) + .catch((_err) => { + egw().debug('error', 'Loading eTemplate from '+_url+' failed! '+_err.status+' '+_err.statusText); if(typeof _fail_callback === 'function') { _fail_callback.call(_context, _err); } - } - }); + }); } export function et2_directChildrenByTagName(_node : HTMLElement, _tagName : String) : HTMLElement[] diff --git a/api/js/jsapi/egw_json.js b/api/js/jsapi/egw_json.js index de238e99ef..8339689ed7 100644 --- a/api/js/jsapi/egw_json.js +++ b/api/js/jsapi/egw_json.js @@ -203,7 +203,7 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd) if (typeof method === 'undefined') method = 'POST'; // Assemble the complete request - var request_obj = JSON.stringify({ + const request_obj = JSON.stringify({ request: { parameters: this.parameters } @@ -218,53 +218,89 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd) return navigator.sendBeacon(this.url, data); } - // Send the request via AJAX using the jquery ajax function - // we need to use jQuery of window of egw object, as otherwise the one from main window is used! - // (causing eg. apply from server with app.$app.method to run in main window instead of popup) - this.request = (this.egw.window?this.egw.window.jQuery:jQuery).ajax({ - url: this.url, - async: this.async, - context: this, - // only POST can send JSON as direct payload, GET can not - data: method === 'GET' ? { json_data: request_obj } : request_obj, - contentType: method === 'GET' ? false : 'application/json', - dataType: 'json', - type: method, - success: this.handleResponse, - jsonp: false, - error: error || this.handleError - }); - + let url = this.url; + let init = { + method: method + } + if (method === 'GET') + { + url += (url.indexOf('?') === -1 ? '?' : '&') + new URLSearchParams({ json_data: request_obj }); + } + else + { + init.headers = { 'Content-Type': 'application/json'}; + init.body = request_obj; + } + if (this.async) + { + this.request = (this.egw.window?this.egw.window:window).fetch(url, init) + .then((response) => { + if (!response.ok) { + throw response; + } + return response.json(); + }) + .then((data) => this.handleResponse(data)) + .catch((_err) => { + (error || this.handleError).call(this, _err) + }); + } + else + { + console.trace("Synchronous AJAX request detected", this); + this.request = new XMLHttpRequest(); + this.request.open(method, url, false); + if (method !== 'GET') this.request.setRequestHeader('Content-Type', 'application/json'); + this.request.send(init.body); + if (this.request.status !== 200) + { + (error || this.handleError).call(this, this.request, 'error') + } + else + { + this.handleResponse(JSON.parse(this.request.responseText)); + } + } return this.request; }; /** * Default error callback displaying error via egw.message * - * @param {XMLHTTP} _xmlhttp + * @param {XMLHTTP|Response} response * @param {string} _err */ - json_request.prototype.handleError = function(_xmlhttp, _err) { + json_request.prototype.handleError = function(response, _err) { // Don't error about an abort if(_err !== 'abort') { + // for fetch Response get json, as it's used below + if (typeof response.headers === 'object' && response.headers.get('Content-Type') === 'application/json') + { + return response.json().then((json) => { + response.responseJSON = json; + this.handleError(response, 'error'); + }) + } + const date = typeof response.headers === 'object' ? 'Date: '+response.headers.get('Date') : + response.getAllResponseHeaders().match(/^Date:.*$/mi)[0] || + 'Date: '+(new Date).toString(); this.egw.message.call(this.egw, this.egw.lang('A request to the EGroupware server returned with an error')+ - ': '+_xmlhttp.statusText+' ('+_xmlhttp.status+")\n\n"+ + ': '+response.statusText+' ('+response.status+")\n\n"+ this.egw.lang('Please reload the EGroupware desktop (F5 / Cmd+r).')+"\n"+ this.egw.lang('If the error persists, contact your administrator for help and ask to check the error-log of the webserver.')+ - "\n\nURL: "+this.url+"\n"+ - (_xmlhttp.getAllResponseHeaders() ? (_xmlhttp.getAllResponseHeaders().match(/^Date:.*$/mi) ? _xmlhttp.getAllResponseHeaders().match(/^Date:.*$/mi)[0]:''):'')+ + "\n\nURL: "+this.url+"\n"+date+ // if EGroupware send JSON payload with error, errno show it here too - (_err === 'error' && _xmlhttp.status === 400 && typeof _xmlhttp.responseJSON === 'object' && _xmlhttp.responseJSON.error ? - "\nError: "+_xmlhttp.responseJSON.error+' ('+_xmlhttp.responseJSON.errno+')' : '') + (_err === 'error' && response.status === 400 && typeof response.responseJSON === 'object' && response.responseJSON.error ? + "\nError: "+response.responseJSON.error+' ('+response.responseJSON.errno+')' : '') ); - this.egw.debug('error', 'Ajax request to', this.url, ' failed: ', _err, _xmlhttp.status, _xmlhttp.statusText, _xmlhttp.responseJSON); + this.egw.debug('error', 'Ajax request to', this.url, ' failed: ', _err, response.status, response.statusText, response.responseJSON); // check of unparsable JSON on server-side, which might be caused by some network problem --> resend max. twice - if (_err === 'error' && _xmlhttp.status === 400 && typeof _xmlhttp.responseJSON === 'object' && - _xmlhttp.responseJSON.errno && _xmlhttp.responseJSON.error.substr(0, 5) === 'JSON ') + if (_err === 'error' && response.status === 400 && typeof response.responseJSON === 'object' && + response.responseJSON.errno && response.responseJSON.error.substr(0, 5) === 'JSON ') { // ToDo: resend request max. twice } From 50b88396098252a1a65cf477a707287253307adb Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Fri, 16 Jul 2021 11:49:53 +0200 Subject: [PATCH 03/11] fix not working InfoLog index: synchronous request must return fulfilled promise --- api/js/etemplate/et2_core_xml.ts | 3 ++- api/js/jsapi/egw_json.js | 35 ++++++++++++++++---------------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/api/js/etemplate/et2_core_xml.ts b/api/js/etemplate/et2_core_xml.ts index 04d06f2f7b..9b73f74aa2 100644 --- a/api/js/etemplate/et2_core_xml.ts +++ b/api/js/etemplate/et2_core_xml.ts @@ -70,7 +70,8 @@ export function et2_loadXMLFromURL(_url : string, _callback? : Function, _contex return xmldoc.children[0]; }) .catch((_err) => { - egw().debug('error', 'Loading eTemplate from '+_url+' failed! '+_err.status+' '+_err.statusText); + egw().message('Loading eTemplate from '+_url+' failed!'+"\n\n"+ + (typeof _err.stack !== 'undefined' ? _err.stack : _err.status+' '+_err.statusText), 'error'); if(typeof _fail_callback === 'function') { _fail_callback.call(_context, _err); } diff --git a/api/js/jsapi/egw_json.js b/api/js/jsapi/egw_json.js index 8339689ed7..7e402bd921 100644 --- a/api/js/jsapi/egw_json.js +++ b/api/js/jsapi/egw_json.js @@ -75,9 +75,6 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd) this.sender = _sender ? _sender : null; this.egw = _egw; - // We currently don't have a request object - this.request = null; - // Some variables needed for notification about a JS files done loading this.onLoadFinish = null; this.jsFiles = 0; @@ -191,7 +188,7 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd) * @param {string} method ='POST' allow to eg. use a (cachable) 'GET' request instead of POST * @param {function} error option error callback(_xmlhttp, _err) used instead our default this.error * - * @return {jqXHR|boolean} jQuery jqXHR request object or for async==="keepalive" boolean is returned + * @return {Promise|boolean} Promise or for async==="keepalive" boolean is returned */ json_request.prototype.sendRequest = function(async, method, error) { @@ -209,7 +206,7 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd) } }); - // send with keepalive===true or sendBeacon to be used in beforeunload event + // send with keepalive===true for sendBeacon to be used in beforeunload event if (this.async === "keepalive" && typeof navigator.sendBeacon !== "undefined") { const data = new FormData(); @@ -231,16 +228,17 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd) init.headers = { 'Content-Type': 'application/json'}; init.body = request_obj; } + let promise; if (this.async) { - this.request = (this.egw.window?this.egw.window:window).fetch(url, init) + promise = (this.egw.window?this.egw.window:window).fetch(url, init) .then((response) => { - if (!response.ok) { + if (!response.ok || !response.headers.get('Content-Type').startsWith('application/json')) { throw response; } return response.json(); }) - .then((data) => this.handleResponse(data)) + .then((data) => this.handleResponse(data) || data) .catch((_err) => { (error || this.handleError).call(this, _err) }); @@ -248,20 +246,24 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd) else { console.trace("Synchronous AJAX request detected", this); - this.request = new XMLHttpRequest(); - this.request.open(method, url, false); - if (method !== 'GET') this.request.setRequestHeader('Content-Type', 'application/json'); - this.request.send(init.body); - if (this.request.status !== 200) + const request = new XMLHttpRequest(); + request.open(method, url, false); + if (method !== 'GET') request.setRequestHeader('Content-Type', 'application/json'); + request.send(init.body); + if (request.status >= 200 && request.status < 300) { - (error || this.handleError).call(this, this.request, 'error') + const json = JSON.parse(request.responseText); + promise = Promise.resolve(this.handleResponse(json) || json); } else { - this.handleResponse(JSON.parse(this.request.responseText)); + (error || this.handleError).call(this, request, 'error') } } - return this.request; + // compatibility with jQuery.ajax + if (promise && typeof promise.then === 'function') promise.done = promise.then; + + return promise; }; /** @@ -419,7 +421,6 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd) this.callback.call(this.context,res); } } - this.request = null; }; var json = { From 87694e660c188ff08e85cbe9d1f850780491bc82 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Fri, 16 Jul 2021 12:03:29 +0200 Subject: [PATCH 04/11] seems to be no need to send a synchronous request --- api/js/etemplate/et2_widget_selectbox.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/api/js/etemplate/et2_widget_selectbox.ts b/api/js/etemplate/et2_widget_selectbox.ts index 666d329aaf..d64f5df037 100644 --- a/api/js/etemplate/et2_widget_selectbox.ts +++ b/api/js/etemplate/et2_widget_selectbox.ts @@ -1496,33 +1496,33 @@ export class et2_selectbox extends et2_inputWidget // Fetch with json instead of jsonq because there may be more than // one widget listening for the response by the time it gets back, // and we can't do that when it's queued. - var req = egw.json( + const req = egw.json( 'EGroupware\\Api\\Etemplate\\Widget\\Select::ajax_get_options', [widget._type,options_string,attrs.value] - ).sendRequest(!in_nextmatch); + ).sendRequest(); // was !in_nextmatch to send synchronous request if(typeof cache === 'undefined') { cache_owner[cache_id] = req; } cache = req; } - if(typeof cache.done == 'function') + if (typeof cache.then === 'function') { // pending, wait for it - cache.done(jQuery.proxy(function(response) { + cache.then((response) => { cache = cache_owner[cache_id] = response.response[0].data||undefined; // Set select_options in attributes in case we get a resonse before // the widget is finished loading (otherwise it will re-set to {}) attrs.select_options = cache; - egw.window.setTimeout(jQuery.proxy(function() { + egw.window.setTimeout(() => { // Avoid errors if widget is destroyed before the timeout - if(this.widget && typeof this.widget.id !== 'undefined') + if (widget && typeof widget.id !== 'undefined') { - this.widget.set_select_options(et2_selectbox.find_select_options(this.widget,{}, this.widget.options)); + widget.set_select_options(et2_selectbox.find_select_options(widget,{}, widget.options)); } - },this),1); - },{widget:widget,cache_id:cache_id})); + }, 1); + }); return []; } else From ce0a51318753b70f8bb12b1b172cac23fa12d139 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Fri, 16 Jul 2021 14:50:06 +0200 Subject: [PATCH 05/11] egw.preference(name, app, true) returns now a promise to query preference async changed notifications to query preference async together with lang-files fixed error-handling in new egw.json() --- api/js/jsapi/egw_json.js | 4 +-- api/js/jsapi/egw_preferences.js | 44 +++++++++++++++-------- notifications/js/notificationajaxpopup.js | 5 ++- 3 files changed, 36 insertions(+), 17 deletions(-) diff --git a/api/js/jsapi/egw_json.js b/api/js/jsapi/egw_json.js index 7e402bd921..dad1ebc5ef 100644 --- a/api/js/jsapi/egw_json.js +++ b/api/js/jsapi/egw_json.js @@ -285,7 +285,7 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd) }) } const date = typeof response.headers === 'object' ? 'Date: '+response.headers.get('Date') : - response.getAllResponseHeaders().match(/^Date:.*$/mi)[0] || + (typeof response.getAllResponseHeaders === 'function' ? response.getAllResponseHeaders().match(/^Date:.*$/mi)[0] : null) || 'Date: '+(new Date).toString(); this.egw.message.call(this.egw, this.egw.lang('A request to the EGroupware server returned with an error')+ @@ -416,7 +416,7 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd) } } // Call request callback, if provided - if(this.callback != null && !only_data) + if(typeof this.callback === 'function' && !only_data) { this.callback.call(this.context,res); } diff --git a/api/js/jsapi/egw_preferences.js b/api/js/jsapi/egw_preferences.js index a57cdf54f7..bd3d9d3e22 100644 --- a/api/js/jsapi/egw_preferences.js +++ b/api/js/jsapi/egw_preferences.js @@ -53,31 +53,47 @@ egw.extend('preferences', egw.MODULE_GLOBAL, function() /** * Query an EGroupware user preference * - * If a prefernce is not already loaded (only done for "common" by default), it is synchroniosly queryed from the server! + * If a preference is not already loaded (only done for "common" by default), + * it is synchronously queried from the server, if no _callback parameter is given! * * @param {string} _name name of the preference, eg. 'dateformat', or '*' to get all the application's preferences * @param {string} _app default 'common' - * @param {function|false|undefined} _callback optional callback, if preference needs loading first - * if false given and preference is not loaded, undefined is return and no (synchronious) request is send to server + * @param {function|boolean|undefined} _callback optional callback, if preference needs loading first + * - default/undefined: preference is synchronously queried, if not loaded, and returned + * - function: if loaded, preference is returned, if not false and callback is called once it's loaded + * - true: a promise for the preference is returned + * - false: if preference is not loaded, undefined is return and no (synchronous) request is send to server * @param {object} _context context for callback - * @return string|bool preference value or false, if callback given and preference not yet loaded + * @return Promise|object|string|bool (Promise for) preference value or false, if callback given and preference not yet loaded */ preference: function(_name, _app, _callback, _context) { - if (typeof _app == 'undefined') _app = 'common'; + if (typeof _app === 'undefined') _app = 'common'; - if (typeof prefs[_app] == 'undefined') + if (typeof prefs[_app] === 'undefined') { if (_callback === false) return undefined; - var request = this.json('EGroupware\\Api\\Framework::ajax_get_preference', [_app], _callback, _context); - request.sendRequest(typeof _callback == 'function', 'GET'); // use synchronous (cachable) GET request - if (typeof prefs[_app] == 'undefined') prefs[_app] = {}; - if (typeof _callback == 'function') return false; + const request = this.json('EGroupware\\Api\\Framework::ajax_get_preference', [_app], _callback, _context); + const promise = request.sendRequest(typeof _callback !== 'undefined', 'GET'); + if (typeof prefs[_app] === 'undefined') prefs[_app] = {}; + if (_callback === true) return promise.then(() => this.preference(_name, _app)); + if (typeof _callback === 'function') return false; } - if (_name == "*") return typeof prefs[_app] ==='object' ? jQuery.extend({},prefs[_app]) : prefs[_app]; - - return typeof prefs[_app][_name] === 'object' && prefs[_app][_name] !== null ? - jQuery.extend({},prefs[_app][_name]) : prefs[_app][_name]; + let ret; + if (_name === "*") + { + ret = typeof prefs[_app] === 'object' ? jQuery.extend({}, prefs[_app]) : prefs[_app]; + } + else + { + ret = typeof prefs[_app][_name] === 'object' && prefs[_app][_name] !== null ? + jQuery.extend({}, prefs[_app][_name]) : prefs[_app][_name]; + } + if (_callback === true) + { + return Promise.resolve(ret); + } + return ret; }, /** diff --git a/notifications/js/notificationajaxpopup.js b/notifications/js/notificationajaxpopup.js index 48b29712b1..b2dc13bfae 100644 --- a/notifications/js/notificationajaxpopup.js +++ b/notifications/js/notificationajaxpopup.js @@ -908,7 +908,10 @@ var self = notifications; var langRequire = jQuery('#notifications_script_id').attr('data-langRequire'); - egw.langRequire(window, [JSON.parse(langRequire)]).then(()=> + Promise.all([ + egw.langRequire(window, [JSON.parse(langRequire)]), + egw.preference('notification_chain','notifications', true) + ]).then(() => { var $egwpopup_fw = jQuery('#topmenu_info_notifications'); switch (egw.preference('notification_chain','notifications')) From 59ac3f4bda658a7c5e9a5bee1e67e6bfaeb649bf Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Mon, 19 Jul 2021 08:31:06 +0200 Subject: [PATCH 06/11] always name generated file app.min.js so client-side can load it without knowing source is .ts or .js --> fixes client-side initiated load errors --- api/js/egw_action/egw_action_common.js | 15 ++++++--------- api/js/jsapi/egw_json.js | 4 ++-- rollup.config.js | 2 +- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/api/js/egw_action/egw_action_common.js b/api/js/egw_action/egw_action_common.js index e250edaaa7..25c512bf8a 100644 --- a/api/js/egw_action/egw_action_common.js +++ b/api/js/egw_action/egw_action_common.js @@ -396,17 +396,14 @@ egwFnct.prototype.setValue = function(_value) // check if we need a not yet included app.js object --> include it now and re-set when it arrives else if (i === 1 && typeof app.classes[parts[1]] === "undefined") { - var self = this; - return new Promise(function(resolve) - { - egw.includeJS("/"+parts[1]+"/js/app.js", function () - { - if(typeof app.classes[parts[i]] !== "undefined") + return import(egw.webserverUrl+"/"+parts[1]+"/js/app.min.js?"+((new Date).valueOf()/86400|0).toString()) + .then(() => { + if(typeof app.classes[parts[i]] === "undefined") { - resolve(this.setValue(_value)); + throw new Error("app.classes."+parts[i]+" not found!"); } - }.bind(this), self, egw.webserverUrl); - }.bind(this)); + this.setValue(_value); + }); } // check if we need a not yet instantiated app.js object --> instantiate it now else if (i === 1 && typeof app.classes[parts[1]] === "function") diff --git a/api/js/jsapi/egw_json.js b/api/js/jsapi/egw_json.js index dad1ebc5ef..020dab9626 100644 --- a/api/js/jsapi/egw_json.js +++ b/api/js/jsapi/egw_json.js @@ -575,9 +575,9 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd) // check if we need a not yet included app.js object --> include it now and return a Promise else if (i == 1 && parts[0] == 'app' && typeof app.classes[parts[1]] === 'undefined') { - return import(this.webserverUrl+'/'+parts[1]+'/js/app.js?'+((new Date).valueOf()/86400|0).toString()) + return import(this.webserverUrl+'/'+parts[1]+'/js/app.min.js?'+((new Date).valueOf()/86400|0).toString()) .then(() => this.applyFunc(_func, args, _context), - (err) => {console.error("Failure loading /"+parts[1]+'/js/app.js' + " (" + err + ")\nAborting.")}); + (err) => {console.error("Failure loading /"+parts[1]+'/js/app.min.js' + " (" + err + ")\nAborting.")}); } // check if we need a not yet instantiated app.js object --> instantiate it now else if (i == 1 && parts[0] == 'app' && typeof app.classes[parts[1]] === 'function') diff --git a/rollup.config.js b/rollup.config.js index c55223a1dd..d36e92d712 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -154,7 +154,7 @@ export default function addAppsConfig() { try { statSync(file.name + '/js/app.ts'); - config.input[file.name + '/js/app'] = file.name + '/js/app.ts'; + config.input[file.name + '/js/app.min'] = file.name + '/js/app.ts'; } catch (e) { try { From 3a618937a958950b34e3319b2fa7831eafba048b Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Mon, 19 Jul 2021 10:24:58 +0200 Subject: [PATCH 07/11] do NOT check Content-Type header as it's sometimes not set to application/json parsing JSON will error out, if payload is no JSON anyway happens with mark-as-spam and SpamTitan --- api/js/jsapi/egw_json.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/js/jsapi/egw_json.js b/api/js/jsapi/egw_json.js index 020dab9626..f273b5b561 100644 --- a/api/js/jsapi/egw_json.js +++ b/api/js/jsapi/egw_json.js @@ -233,7 +233,7 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd) { promise = (this.egw.window?this.egw.window:window).fetch(url, init) .then((response) => { - if (!response.ok || !response.headers.get('Content-Type').startsWith('application/json')) { + if (!response.ok) { throw response; } return response.json(); From efb1711eef8fcf87a49da937d169850f52a3fe41 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Mon, 19 Jul 2021 11:23:35 +0200 Subject: [PATCH 08/11] always sending notification, status and filemanager preferences with user-date to avoid requesting them synchronous --- api/user.php | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/api/user.php b/api/user.php index 1bf0986580..3a016b6879 100644 --- a/api/user.php +++ b/api/user.php @@ -30,10 +30,16 @@ include '../header.inc.php'; $GLOBALS['egw']->session->commit_session(); // use an etag over config and link-registry -$preferences = json_encode($GLOBALS['egw_info']['user']['preferences']['common']); -$ab_preferences = json_encode($GLOBALS['egw_info']['user']['preferences']['addressbook']); +$preferences['common'] = $GLOBALS['egw_info']['user']['preferences']['common']; +foreach(['addressbook', 'notifications', 'status', 'filemanager'] as $app) +{ + if (!empty($GLOBALS['egw_info']['user']['apps'][$app])) + { + $preferences[$app] = $GLOBALS['egw_info']['user']['preferences'][$app]; + } +} $user = $GLOBALS['egw']->accounts->json($GLOBALS['egw_info']['user']['account_id']); -$etag = '"'.md5($preferences.$ab_preferences.$user).'"'; +$etag = '"'.md5(json_encode($preferences).$user).'"'; // headers to allow caching, egw_framework specifies etag on url to force reload, even with Expires header Api\Session::cache_control(86400); // cache for 1 day @@ -47,9 +53,11 @@ if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] == $ exit; } -$content = 'egw.set_preferences('.$preferences.", 'common', egw && egw.window !== window);\n"; -$content .= 'egw.set_preferences('.$ab_preferences.", 'addressbook', egw && egw.window !== window);\n"; -$content .= 'egw.set_user('.$user.", egw && egw.window !== window);\n"; +$content = 'egw.set_user('.$user.", egw && egw.window !== window);\n"; +foreach($preferences as $app => $data) +{ + $content .= 'egw.set_preferences('.json_encode($data).', '.json_encode($app).", egw && egw.window !== window);\n"; +} // we run our own gzip compression, to set a correct Content-Length of the encoded content if (in_array('gzip', explode(',',$_SERVER['HTTP_ACCEPT_ENCODING'])) && function_exists('gzencode')) From eec16362dc03f384af2b015307da65137d1156c8 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Mon, 19 Jul 2021 11:50:33 +0200 Subject: [PATCH 09/11] amending commit 59ac3f4 "always name generated file app.min.js" --- admin/js/app.ts | 2 +- infolog/js/app.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/admin/js/app.ts b/admin/js/app.ts index f685e39c63..5ce20cd093 100644 --- a/admin/js/app.ts +++ b/admin/js/app.ts @@ -1081,7 +1081,7 @@ class AdminApp extends EgwApp if(egw.app('policy')) { - import(egw.link('/policy/js/app.js?'+((new Date).valueOf()/86400|0).toString())).then(() => { + import(egw.link('/policy/js/app.min.js?'+((new Date).valueOf()/86400|0).toString())).then(() => { if(typeof app.policy === 'undefined' || typeof app.policy.confirm === 'undefined') { app.policy = new app.classes.policy(); diff --git a/infolog/js/app.ts b/infolog/js/app.ts index 6694fea546..71a0ac59ae 100644 --- a/infolog/js/app.ts +++ b/infolog/js/app.ts @@ -778,7 +778,7 @@ class InfologApp extends EgwApp if (!app.stylite) { - import(egw.webserverUrl+'/stylite/js/app.js?'+((new Date).valueOf()/86400|0).toString()).then(() => + import(egw.webserverUrl+'/stylite/js/app.min.js?'+((new Date).valueOf()/86400|0).toString()).then(() => { app.stylite = new app.classes.stylite; app.stylite.et2 = this.et2; From cc27253b7b3f880a9b2c6195e07fc0890eddfd06 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Mon, 19 Jul 2021 14:15:16 +0200 Subject: [PATCH 10/11] keep original REQUEST_URI in ORIG_REQUEST_URI when modifying it --- api/src/Vfs/WebDAV.php | 1 + 1 file changed, 1 insertion(+) diff --git a/api/src/Vfs/WebDAV.php b/api/src/Vfs/WebDAV.php index ca7a9e3aeb..d002a5e963 100644 --- a/api/src/Vfs/WebDAV.php +++ b/api/src/Vfs/WebDAV.php @@ -636,6 +636,7 @@ class WebDAV extends HTTP_WebDAV_Server_Filesystem { if ($_SERVER['REQUEST_METHOD'] == 'GET' && (($this->force_download = strpos($_SERVER['REQUEST_URI'],'?download')) !== false)) { + $_SERVER['ORIG_REQUEST_URI'] = $_SERVER['REQUEST_URI']; $_SERVER['REQUEST_URI'] = substr($_SERVER['REQUEST_URI'],0,$this->force_download); } parent::__construct(); From 0a1e784f2e4478557cee5fce8142a420f5e4a455 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Mon, 19 Jul 2021 16:57:25 +0200 Subject: [PATCH 11/11] egw.getCache(name) function to get a cache-object shared between all EGroupware windows and popups used to share server-side generated options for et2_selectbox also removed nextmatch specific code from et2_selectbox which seems no longer necessary --- api/js/etemplate/et2_widget_selectbox.ts | 42 +++++++----------------- api/js/jsapi/egw_utils.js | 18 ++++++++++ 2 files changed, 29 insertions(+), 31 deletions(-) diff --git a/api/js/etemplate/et2_widget_selectbox.ts b/api/js/etemplate/et2_widget_selectbox.ts index d64f5df037..d3e2b923c3 100644 --- a/api/js/etemplate/et2_widget_selectbox.ts +++ b/api/js/etemplate/et2_widget_selectbox.ts @@ -1469,29 +1469,11 @@ export class et2_selectbox extends et2_inputWidget // normalize options by removing trailing commas options_string = options_string.replace(/,+$/, ''); - var cache_id = widget._type+'_'+options_string; - var cache_owner = ( - // Todo: @new-js-loader et2_selectbox is no longer instanciated globaly --> caching needs to be fixed - et2_selectbox - /*egw.window.et2_selectbox ? - egw.window.et2_selectbox : - egw(window).window.et2_selectbox*/ - ).type_cache; - var cache = cache_owner[cache_id]; + const cache_id = widget._type+'_'+options_string; + const cache_owner = egw.getCache('et2_selectbox'); + let cache = cache_owner[cache_id]; - // Options for a selectbox in a nextmatch must be returned now, as the - // widget we have is not enough to set the options later. - var in_nextmatch = false; - if(typeof cache === 'undefined' || typeof cache.length === 'undefined') - { - var parent = widget._parent; - while(parent && !in_nextmatch) - { - in_nextmatch = parent && parent._type && parent._type === 'nextmatch'; - parent = parent._parent; - } - } - if (typeof cache == 'undefined' || in_nextmatch) + if (typeof cache === 'undefined') { // Fetch with json instead of jsonq because there may be more than // one widget listening for the response by the time it gets back, @@ -1499,7 +1481,7 @@ export class et2_selectbox extends et2_inputWidget const req = egw.json( 'EGroupware\\Api\\Etemplate\\Widget\\Select::ajax_get_options', [widget._type,options_string,attrs.value] - ).sendRequest(); // was !in_nextmatch to send synchronous request + ).sendRequest(); if(typeof cache === 'undefined') { cache_owner[cache_id] = req; @@ -1511,17 +1493,15 @@ export class et2_selectbox extends et2_inputWidget // pending, wait for it cache.then((response) => { cache = cache_owner[cache_id] = response.response[0].data||undefined; - // Set select_options in attributes in case we get a resonse before + // Set select_options in attributes in case we get a response before // the widget is finished loading (otherwise it will re-set to {}) attrs.select_options = cache; - egw.window.setTimeout(() => { - // Avoid errors if widget is destroyed before the timeout - if (widget && typeof widget.id !== 'undefined') - { - widget.set_select_options(et2_selectbox.find_select_options(widget,{}, widget.options)); - } - }, 1); + // Avoid errors if widget is destroyed before the timeout + if (widget && typeof widget.id !== 'undefined') + { + widget.set_select_options(et2_selectbox.find_select_options(widget,{}, widget.options)); + } }); return []; } diff --git a/api/js/jsapi/egw_utils.js b/api/js/jsapi/egw_utils.js index d83b3ec0d3..b00a0cf51d 100644 --- a/api/js/jsapi/egw_utils.js +++ b/api/js/jsapi/egw_utils.js @@ -145,9 +145,27 @@ egw.extend('utils', egw.MODULE_GLOBAL, function() var uid_counter = 0; + /** + * Global cache shared between all EGroupware windows + * @type {{}} + */ + const cache = {}; + // Create the utils object which contains references to all functions // covered by it. var utils = { + /** + * Get a cache object shared between all EGroupware windows + * + * @param {string} _name unique name for the cache-object + * @return {*} + */ + getCache: function(_name) + { + if (typeof cache[_name] === 'undefined') cache[_name] = {}; + + return cache[_name]; + }, ajaxUrl: function(_menuaction) { if(_menuaction.indexOf('menuaction=') >= 0)