egroupware/api/js/jsapi/egw_json.js
Ralf Becker 2f1333a116 return and show in browser JSON parsing errors maybe caused by network problems
server sends HTTP status "400 Bad Request" with JSON payload with "error" and "errno" attributes.
error is json_last_error_msg() prefixed with "JSON ".
Not yet implemented is resending the request (max. twice) for JSON parsing errors to try to work around network problems
2018-11-01 12:00:08 +01:00

588 lines
18 KiB
JavaScript

/**
* EGroupware clientside API object
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package etemplate
* @subpackage api
* @link http://www.egroupware.org
* @author Andreas Stöckel (as AT stylite.de)
* @author Ralf Becker <RalfBecker@outdoor-training.de>
* @version $Id$
*/
/*egw:uses
vendor.bower-asset.jquery.dist.jquery;
egw_core;
egw_utils;
egw_files;
egw_debug;
*/
/**
* Module sending json requests
*
* @param {string} _app application name object is instanciated for
* @param {object} _wnd window object is instanciated for
*/
egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd)
{
"use strict";
/**
* Object which contains all registered handlers for JS responses.
* The handlers are organized per response type in the top level of the
* object, where each response type can have an array of handlers attached
* to it.
*/
var plugins = {};
/**
* Global json handlers are from global modules, not window level
*/
if(typeof egw._global_json_handlers == 'undefined')
{
egw._global_json_handlers = {};
}
var global_plugins = egw._global_json_handlers;
/**
* Internal implementation of the JSON request object.
*
* @param {string} _menuaction
* @param {array} _parameters
* @param {function} _callback
* @param {object} _context
* @param {boolean} _async
* @param {object} _sender
* @param {egw} _egw
*/
function json_request(_menuaction, _parameters, _callback, _context,
_async, _sender, _egw)
{
// Copy the parameters
this.url = _egw.ajaxUrl(_menuaction);
// IE JSON-serializes arrays passed in from different window contextx (eg. popups)
// as objects (it looses object-type of array), causing them to be JSON serialized
// as objects and loosing parameters which are undefined
// JSON.strigify([123,undefined]) --> '{"0":123}' instead of '[123,null]'
this.parameters = _parameters ? [].concat(_parameters) : [];
this.async = typeof _async != 'undefined' ? _async : true;
this.callback = _callback ? _callback : null;
this.context = _context ? _context : null;
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;
this.jsCount = 0;
// Function which is currently used to display alerts -- may be replaced by
// some API function.
this.alertHandler = function(_message, _details) {
alert(_message);
if (_details)
{
_egw.debug('info', _message, _details);
}
};
}
/**
* Sends the assembled request to the server
* @param {boolean} [async=false] Overrides async provided in constructor to give an easy way to make simple async requests
* @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} jQuery jqXHR request object
*/
json_request.prototype.sendRequest = function(async, method, error)
{
if(typeof async != "undefined")
{
this.async = async;
}
if (typeof method === 'undefined') method = 'POST';
// Assemble the complete request
var request_obj = JSON.stringify({
request: {
parameters: this.parameters
}
});
// 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,
error: error || this.handleError
});
return this.request;
};
/**
* Default error callback displaying error via egw.message
*
* @param {XMLHTTP} _xmlhttp
* @param {string} _err
*/
json_request.prototype.handleError = function(_xmlhttp, _err) {
// Don't error about an abort
if(_err !== 'abort')
{
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"+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)[0]:'')+
// 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+')' : ''));
this.egw.debug('error', 'Ajax request to', this.url, ' failed: ', _err, _xmlhttp.status, _xmlhttp.statusText, _xmlhttp.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 ')
{
// ToDo: resend request max. twice
}
}
};
json_request.prototype.handleResponse = function(data) {
if (data && typeof data.response != 'undefined')
{
if (egw.preference('show_generation_time', 'common', false) == "1")
{
var gen_time_div = jQuery('#divGenTime').length > 0 ? jQuery('#divGenTime')
:jQuery('<div id="divGenTime" class="pageGenTime"><span class="pageTime"></span></div>').appendTo('#egw_fw_footer');
}
// Load files first
var js_files = [];
for (var i = data.response.length - 1; i > 0; --i)
{
var res = data.response[i];
if(res.type == 'js' && typeof res.data == 'string')
{
js_files.unshift(res.data);
data.response.splice(i,1);
}
}
if(js_files.length > 0)
{
var start_time = (new Date).getTime();
this.egw.includeJS(js_files, function() {
var end_time = (new Date).getTime();
this.handleResponse(data);
if (egw.preference('show_generation_time', 'common', false) == "1")
{
var gen_time_div = jQuery('#divGenTime');
if (!gen_time_div.length) gen_time_div = jQuery('.pageGenTime');
var gen_time_async = jQuery('.asyncIncludeTime').length > 0 ? jQuery('.asyncIncludeTime'):
gen_time_div.append('<span class="asyncIncludeTime"></span>').find('.asyncIncludeTime');
gen_time_async.text(egw.lang('async includes took %1s', (end_time-start_time)/1000));
}
}, this);
return;
}
// Flag for only data response - don't call callback if only data
var only_data = (data.response.length > 0);
for (var i = 0; i < data.response.length; i++)
{
// Get the response object
var res = data.response[i];
if(typeof res.type == 'string' && res.type != 'data') only_data = false;
// Check whether a plugin for the given type exists
var handlers = [plugins, global_plugins];
for(var handler_idx = 0; handler_idx < handlers.length; handler_idx++)
{
var handler_level = handlers[handler_idx];
if (typeof handler_level[res.type] !== 'undefined')
{
for (var j = 0; j < handler_level[res.type].length; j++) {
try {
// Get a reference to the plugin
var plugin = handler_level[res.type][j];
if (res.type.match(/et2_load/))
{
if (egw.preference('show_generation_time', 'common', false) == "1")
{
if (gen_time_div.length > 0)
{
gen_time_div.find('span.pageTime').text(egw.lang("Page was generated in %1 seconds ", data.page_generation_time));
if (data.session_restore_time)
{
var gen_time_session_span = gen_time_div.find('span.session').length > 0 ? gen_time_div.find('span.session'):
gen_time_div.append('<span class="session"></span>').find('.session');
gen_time_session_span.text(egw.lang("session restore time in %1 seconds ", data.page_generation_time));
}
}
}
}
// Call the plugin callback
plugin.callback.call(
plugin.context ? plugin.context : this.context,
res.type, res, this
);
} catch(e) {
var msg = e.message ? e.message : e + '';
var stack = e.stack ? "\n-- Stack trace --\n" + e.stack : "";
this.egw.debug('error', 'Exception "' + msg + '" while handling JSON response from ' +
this.url + ' [' + JSON.stringify(this.parameters) + '] type "' + res.type +
'", plugin', plugin, 'response', res, stack);
}
}
}
}
}
// Call request callback, if provided
if(this.callback != null && !only_data)
{
this.callback.call(this.context,res);
}
}
this.request = null;
};
var json = {
/** The constructor of the egw_json_request class.
*
* @param _menuaction the menuaction function which should be called and
* which handles the actual request. If the menuaction is a full featured
* url, this one will be used instead.
* @param _parameters which should be passed to the menuaction function.
* @param _async specifies whether the request should be asynchronous or
* not.
* @param _callback specifies the callback function which should be
* called, once the request has been sucessfully executed.
* @param _context is the context which will be used for the callback function
* @param _sender is a parameter being passed to the _callback function
*/
json: function(_menuaction, _parameters, _callback, _context, _async,
_sender)
{
return new json_request(_menuaction, _parameters, _callback,
_context, _async, _sender, this);
},
/**
* Registers a new handler plugin.
*
* @param _callback is the callback function which should be called
* whenever a response is comming from the server.
* @param _context is the context in which the callback function should
* be called. If null is given, the plugin is executed in the context
* of the request object context.
* @param _type is an optional parameter defaulting to 'global'.
* it describes the response type which this plugin should be
* handling.
* @param {boolean} [_global=false] Register the handler globally or
* locally. Global handlers must stay around, so should be used
* for global modules.
*/
registerJSONPlugin: function(_callback, _context, _type, _global)
{
// _type defaults to 'global'
if (typeof _type === 'undefined')
{
_type = 'global';
}
// _global defaults to false
if (typeof _global === 'undefined')
{
_global = false;
}
var scoped = _global ? global_plugins : plugins;
// Create an array for the given category inside the plugins object
if (typeof scoped[_type] === 'undefined')
{
scoped[_type] = [];
}
// Add the entry
scoped[_type].push({
'callback': _callback,
'context': _context
});
},
/**
* Removes a previously registered plugin.
*
* @param _callback is the callback function which should be called
* whenever a response is comming from the server.
* @param _context is the context in which the callback function should
* be called.
* @param _type is an optional parameter defaulting to 'global'.
* it describes the response type which this plugin should be
* handling.
* @param {boolean} [_global=false] Remove a global or local handler.
*/
unregisterJSONPlugin: function(_callback, _context, _type, _global)
{
// _type defaults to 'global'
if (typeof _type === 'undefined')
{
_type = 'global';
}
// _global defaults to false
if (typeof _global === 'undefined')
{
_global = false;
}
var scoped = _global ? global_plugins : plugins;
if (typeof scoped[_type] !== 'undefined') {
for (var i = 0; i < scoped[_type].length; i++)
{
if (scoped[_type][i].callback == _callback &&
scoped[_type][i].context == _context)
{
scoped[_type].slice(i, 1);
break;
}
}
}
}
};
// Regisert the "alert" plugin
json.registerJSONPlugin(function(type, res, req) {
//Check whether all needed parameters have been passed and call the alertHandler function
if ((typeof res.data.message != 'undefined') &&
(typeof res.data.details != 'undefined'))
{
req.alertHandler(
res.data.message,
res.data.details);
return true;
}
throw 'Invalid parameters';
}, null, 'alert');
// Regisert the "message" plugin
json.registerJSONPlugin(function(type, res, req) {
//Check whether all needed parameters have been passed and call the alertHandler function
if ((typeof res.data.message != 'undefined'))
{
req.egw.message(res.data.message, res.data.type);
return true;
}
throw 'Invalid parameters';
}, null, 'message');
// Register the "assign" plugin
json.registerJSONPlugin(function(type, res, req) {
//Check whether all needed parameters have been passed and call the alertHandler function
if ((typeof res.data.id != 'undefined') &&
(typeof res.data.key != 'undefined') &&
(typeof res.data.value != 'undefined'))
{
var obj = _wnd.document.getElementById(res.data.id);
if (obj)
{
obj[res.data.key] = res.data.value;
if (res.data.key == "innerHTML")
{
egw_insertJS(res.data.value);
}
return true;
}
return false;
}
throw 'Invalid parameters';
}, null, 'assign');
// Register the "data" plugin
json.registerJSONPlugin(function(type, res, req) {
//Callback the caller in order to allow him to handle the data
if (req.callback)
{
req.callback.call(req.sender, res.data);
return true;
}
}, null, 'data');
// Register the "script" plugin
json.registerJSONPlugin(function(type, res, req) {
if (typeof res.data == 'string')
{
try
{
var func = new Function(res.data);
func.call(req.egw ? req.egw.window : window);
}
catch (e)
{
req.egw.debug('error', 'Error while executing script: ',
res.data,e);
}
return true;
}
throw 'Invalid parameters';
}, null, 'script');
// Register the "apply" plugin
json.registerJSONPlugin(function(type, res, req) {
if (typeof res.data.func == 'string')
{
var parts = res.data.func.split('.');
var func = parts.pop();
var parent = req.egw.window;
for(var i=0; i < parts.length; ++i)
{
if (typeof parent[parts[i]] != 'undefined')
{
parent = parent[parts[i]];
}
// check if we need a not yet instanciated app.js object --> instanciate it now
else if (i == 1 && parts[0] == 'app' && typeof req.egw.window.app.classes[parts[1]] == 'function')
{
parent = parent[parts[1]] = new req.egw.window.app.classes[parts[1]]();
}
}
if (typeof parent[func] == 'function')
{
try
{
parent[func].apply(parent, res.data.parms);
}
catch (e)
{
req.egw.debug('error', e.message, ' in function', res.data.func,
'Parameters', res.data.parms);
}
return true;
}
else
{
throw '"' + res.data.func + '" is not a callable function (type is ' + typeof parent[func] + ')';
}
}
throw 'Invalid parameters';
}, null, 'apply');
// Register the "jquery" plugin
json.registerJSONPlugin(function(type, res, req) {
if (typeof res.data.select == 'string' &&
typeof res.data.func == 'string')
{
try
{
var jQueryObject = jQuery(res.data.select, req.context);
jQueryObject[res.data.func].apply(jQueryObject, res.data.parms);
}
catch (e)
{
req.egw.debug('error', 'Function', res.data.func,
'Parameters', res.data.parms);
}
return true;
}
throw 'Invalid parameters';
}, _wnd, 'jquery');
// Register the "redirect" plugin
json.registerJSONPlugin(function(type, res, req) {
//console.log(res.data.url);
if (typeof res.data.url == 'string' &&
typeof res.data.global == 'boolean')
{
//Special handling for framework reload
res.data.global |= (res.data.url.indexOf("?cd=10") > 0);
if (res.data.global)
{
egw_topWindow().location.href = res.data.url;
}
// json request was originating from a different popup --> redirect that one
else if(this && this.DOMContainer && this.DOMContainer.ownerDocument.defaultView != window &&
egw(this.DOMContainer.ownerDocument.defaultView).is_popup())
{
this.DOMContainer.ownerDocument.location.href = res.data.url;
}
// main window, open url in respective tab
else
{
egw_appWindowOpen(res.data.app, res.data.url);
}
return true;
}
throw 'Invalid parameters';
}, null, 'redirect');
// Register the 'css' plugin
json.registerJSONPlugin(function(type, res, req) {
if (typeof res.data == 'string')
{
req.egw.includeCSS(res.data);
return true;
}
throw 'Invalid parameters';
}, null, 'css');
// Register the 'js' plugin
json.registerJSONPlugin(function(type, res, req) {
if (typeof res.data == 'string')
{
req.jsCount++;
req.egw.includeJS(res.data, function() {
req.jsFiles++;
if (req.jsFiles == req.jsCount && req.onLoadFinish)
{
req.onLoadFinish.call(req.sender);
}
});
return true;
}
throw 'Invalid parameters';
}, null, 'js');
// Register the 'html' plugin, replacing document content with send html
json.registerJSONPlugin(function(type, res, req) {
if (typeof res.data == 'string')
{
// Empty the document tree
while (_wnd.document.childNodes.length > 0)
{
_wnd.document.removeChild(_wnd.document.childNodes[0]);
}
// Write the given content
_wnd.document.write(res.data);
// Close the document
_wnd.document.close();
return true;
}
throw 'Invalid parameters';
}, null, 'html');
// Return the extension
return json;
});