/** * 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 * @version $Id$ */ "use strict"; /*egw:uses egw_core; */ /** * Log debug messages to browser console and persistent html5 localStorage * * localStorage is limited by a clientside quota, so we need to deal with * situation that storing something in localStorage will throw an exception! * * @param {string} _app * @param {object} _wnd */ egw.extend('debug', egw.MODULE_GLOBAL, function(_app, _wnd) { /** * DEBUGLEVEL specifies which messages are printed to the console. * Decrease the value of EGW_DEBUGLEVEL to get less messages. * * @type Number * 0 = off, no logging * 1 = only "error" * 2 = -- " -- plus "warning" * 3 = -- " -- plus "info" * 4 = -- " -- plus "log" * 5 = -- " -- plus a stacktrace */ var DEBUGLEVEL = 3; /** * Log-level for local storage and error-display in GUI * * @type Number * 0 = off, no logging AND no global error-handler bound * 1 = ... see above */ var LOCAL_LOG_LEVEL = 0; /** * Number of log-entries stored on client, new errors overwrite old ones * * @type Number */ var MAX_LOGS = 200; /** * Number of last old log entry = next one to overwrite * * @type String */ var LASTLOG = 'lastLog'; /** * Prefix for key of log-message, message number gets appended to it * * @type String */ var LOG_PREFIX = 'log_'; /** * Log to clientside html5 localStorage * * @param {String} _level "navigation", "log", "info", "warn", "error" * @param {Array} _args arguments to egw.debug * @param {string} _stack * @returns {Boolean} false if localStorage is NOT supported, null if level requires no logging, true if logged */ function log_on_client(_level, _args, _stack) { if (!window.localStorage) return false; switch(_level) { case 'warn': if (LOCAL_LOG_LEVEL < 2) return null; case 'info': if (LOCAL_LOG_LEVEL < 3) return null; case 'log': if (LOCAL_LOG_LEVEL < 4) return null; default: if (!LOCAL_LOG_LEVEL) return null; } var data = { time: (new Date()).getTime(), level: _level, args: _args }; // Add in a trace, if no navigation _level if (_level != 'navigation') { if (_stack) { data.stack = _stack; } else { // IE needs to throw the error to get a stack trace! try { throw new Error; } catch(error) { data.stack = error.stack; } } } if (typeof window.localStorage[LASTLOG] == 'undefined') { window.localStorage[LASTLOG] = 0; } // check if MAX_LOGS changed in code --> clear whole log if (window.localStorage[LASTLOG] > MAX_LOGS) { clear_client_log(); } try { window.localStorage[LOG_PREFIX+window.localStorage[LASTLOG]] = JSON.stringify(data); window.localStorage[LASTLOG] = (1 + parseInt(window.localStorage[LASTLOG])) % MAX_LOGS; } catch(e) { switch (e.name) { case 'QuotaExceededError': // storage quota is exceeded --> delete whole log case 'NS_ERROR_DOM_QUOTA_REACHED': // FF-name clear_client_log(); break; default: // one of the args is not JSON.stringify, because it contains circular references eg. an et2 widget for(var i=0; i < data.args.length; ++i) { try { JSON.stringify(data.args[i]); } catch(e) { // for Class we try removing _parent and _children attributes and try again to stringify if (data.args[i] instanceof Class) { data.args[i] = jQuery.extend({}, data.args[i]); delete data.args[i]._parent; delete data.args[i]._children; try { JSON.stringify(data.args[i]); continue; // stringify worked --> check other arguments } catch(e) { // ignore error and remove whole argument } } // if above doesnt work, we remove the attribute data.args[i] = '** removed, circular reference **'; } } } try { window.localStorage[LOG_PREFIX+window.localStorage[LASTLOG]] = JSON.stringify(data); window.localStorage[LASTLOG] = (1 + parseInt(window.localStorage[LASTLOG])) % MAX_LOGS; } catch(e) { // ignore error, if eg. localStorage exceeds quota on client } } } /** * Get log from localStorage with oldest message first * * @returns {Array} of Object with values for attributes level, message, trace */ function get_client_log() { var logs = []; if (window.localStorage && typeof window.localStorage[LASTLOG] != 'undefined') { var lastlog = parseInt(window.localStorage[LASTLOG]); for(var i=lastlog; i < lastlog+MAX_LOGS; ++i) { var log = window.localStorage[LOG_PREFIX+(i%MAX_LOGS)]; if (typeof log != 'undefined') { try { logs.push(JSON.parse(log)); } catch(e) { // ignore not existing log entries } } } } return logs; } /** * Clears whole client log */ function clear_client_log() { // Remove indicator icon jQuery('#topmenu_info_error').remove(); if (!window.localStorage) return false; var max = MAX_LOGS; // check if we have more log entries then allowed, happens if MAX_LOGS get changed in code if (window.localStorage[LASTLOG] > MAX_LOGS) { max = 1000; // this should NOT be changed, if MAX_LOGS get's smaller! } for(var i=0; i < max; ++i) { if (typeof window.localStorage[LOG_PREFIX+i] != 'undefined') { delete window.localStorage[LOG_PREFIX+i]; } } delete window.localStorage[LASTLOG]; return true; } /** * Format one log message for display * * @param {Object} log {{level: string, time: number, stack: string, args: array[]}} Log information * Actual message is in args[0] * @returns {DOMNode} */ function format_message(log) { var row = document.createElement('tr'); row.setAttribute('class', log.level); var timestamp = row.insertCell(-1); timestamp.appendChild(document.createTextNode(new Date(log.time))); timestamp.setAttribute('class', 'timestamp'); var level = row.insertCell(-1); level.appendChild(document.createTextNode(log.level)); level.setAttribute('class', 'level'); var message = row.insertCell(-1); for(var i = 0; i < log.args.length; i++) { var arg = document.createElement('p'); arg.appendChild( document.createTextNode(typeof log.args[i] == 'string' ? log.args[i] : JSON.stringify( log.args[i])) ); message.appendChild(arg); } var stack = row.insertCell(-1); stack.appendChild(document.createTextNode(log.stack||'')); stack.setAttribute('class','stack'); return row; } /** * Show user an error happend by displaying a clickable icon with tooltip of current error */ function raise_error() { var icon = jQuery('#topmenu_info_error'); if (!icon.length) { var icon = jQuery(egw(_wnd).image_element(egw.image('dialog_error'))); icon.addClass('topmenu_info_item').attr('id', 'topmenu_info_error'); // ToDo: tooltip icon.on('click', egw(_wnd).show_log); jQuery('#egw_fw_topmenu_info_items,#topmenu_info').append(icon); } } // bind to global error handler, only if LOCAL_LOG_LEVEL > 0 if (LOCAL_LOG_LEVEL) { jQuery(_wnd).on('error', function(e) { // originalEvent does NOT always exist in IE var event = typeof e.originalEvent == 'object' ? e.originalEvent : e; // IE(11) gives a syntaxerror on each pageload pointing to first line of html page (doctype). // As I cant figure out what's wrong there, we are ignoring it for now. if (navigator.userAgent.match(/Trident/i) && typeof event.name == 'undefined' && Object.prototype.toString.call(event) == '[object ErrorEvent]' && event.lineno == 1 && event.filename.indexOf('/index.php') != -1) { return false; } log_on_client('error', [event.message], typeof event.stack != 'undefined' ? event.stack : null); raise_error(); // rethrow error to let browser log and show it in usual way too if (typeof event.error == 'object') { throw event.error; } throw event.message; }); } /** * The debug function can be used to send a debug message to the * java script console. The first parameter specifies the debug * level, all other parameters are passed to the corresponding * console function. */ return { debug: function(_level) { if (typeof _wnd.console != "undefined") { // Get the passed parameters and remove the first entry var args = []; for (var i = 1; i < arguments.length; i++) { args.push(arguments[i]); } // Add in a trace if (DEBUGLEVEL >= 5 && typeof (new Error).stack != "undefined") { var stack = (new Error).stack; args.push(stack); } if (_level == "log" && DEBUGLEVEL >= 4 && typeof _wnd.console.log == "function") { _wnd.console.log.apply(_wnd.console, args); } if (_level == "info" && DEBUGLEVEL >= 3 && typeof _wnd.console.info == "function") { _wnd.console.info.apply(_wnd.console, args); } if (_level == "warn" && DEBUGLEVEL >= 2 && typeof _wnd.console.warn == "function") { _wnd.console.warn.apply(_wnd.console, args); } if (_level == "error" && DEBUGLEVEL >= 1 && typeof _wnd.console.error == "function") { _wnd.console.error.apply(_wnd.console, args); } } // raise errors to user, if LOCAL_LOG_LEVEL > 0 if (LOCAL_LOG_LEVEL && _level == "error") raise_error(args); // log to html5 localStorage if (typeof stack != 'undefined') args.pop(); // remove stacktrace again log_on_client(_level, args); }, /** * Display log to user because he clicked on icon showed by raise_error * * @returns {undefined} */ show_log: function() { var table = document.createElement('table'); var body = document.createElement('tbody'); var client_log = get_client_log(); for(var i = 0; i < client_log.length; i++) { body.appendChild(format_message(client_log[i])); } table.appendChild(body); // Use a wrapper div for ease of styling var wrapper = document.createElement('div'); wrapper.setAttribute('class', 'client_error_log'); wrapper.appendChild(table); if(window.jQuery && window.jQuery.ui.dialog) { var $wrapper = $j(wrapper); // Start hidden $j('tr',$wrapper).addClass('hidden') .on('click', function() { $j(this).toggleClass('hidden',{}); $j(this).find('.stack').children().toggleClass('ui-icon ui-icon-circle-plus'); }); // Wrap in div so we can control height $j('td',$wrapper).wrapInner('
') .filter('.stack').children().addClass('ui-icon ui-icon-circle-plus'); $wrapper.dialog({ title: egw.lang('Error log'), buttons: [ {text: egw.lang('OK'), click: function() {$j(this).dialog( "close" ); }}, {text: egw.lang('clear'), click: function() {clear_client_log(); $j(this).empty();}} ], width: 800, height: 400 }); $wrapper[0].scrollTop = $wrapper[0].scrollHeight; } if (_wnd.console) _wnd.console.log(get_client_log()); } }; });