/* CodeQ: an online programming tutor. Copyright (C) 2015,2016 UL FRI This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program 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 Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ (function () { "use strict"; window.siteDefinition = {logLevel: 'debug'/*, 'logService': '/ws/clientlog'*/}; // for debug purposes // ================================================================================ // JSON-ization (stringification) function // ================================================================================ var jsonize = JSON && JSON.stringify || function (obj) { var t, buffer, i, isFirst; if (null === obj) return 'null'; t = typeof obj; if (t === 'string') { return '"' + obj.replace(regexpBackslash, '\\\\').replace(regexpQuote, '\\"') + '"'; } if (t === 'number') { if (isFinite(obj)) return obj.toString(); throw new Error('Cannot jsonize a non-finite number: ' + obj.toString()); } if (t === 'boolean') { if (obj) return 'true'; return 'false'; } if (t === 'object') { if (obj instanceof String) return jsonize(obj.valueOf()); if (obj instanceof Number) return jsonize(obj.valueOf()); if (obj instanceof Boolean) return jsonize(obj.valueOf()); if (obj instanceof Array) { buffer = [ '[' ]; isFirst = true; for (i = 0; i < obj.length; i++) { if (isFirst) isFirst = false; else buffer.push(','); buffer.push(jsonize(obj[i])); } buffer.push(']'); return buffer.join(''); } buffer = [ '{' ]; isFirst = true; for (i in obj) { if (!obj.hasOwnProperty(i)) continue; if (isFirst) isFirst = false; else buffer.push(','); buffer.push(jsonize(i), ':', jsonize(obj[i])); } buffer.push('}'); return buffer.join(''); } throw new Error('Cannot jsonize ' + t); }; // ================================================================================ // The log module: contains methods for logging, sending logs to the server // ================================================================================ var log = (function () { var logServiceUrl = window.siteDefinition && (typeof siteDefinition.logService === 'string') && siteDefinition.logService, assembleOutput = function (stuff, e) { var lines = [ stuff ]; if (e && e.stack) lines.push(e.stack); return lines.join('\n'); }, console = window.console || {}, levelPrefix = {'debug': 'DEBUG: ', 'info': 'INFO: ', 'warn': 'WARN: ', 'error': 'ERROR: '}, levelSeverity = {'debug': 1, 'info': 2, 'warn': 3, 'error': 4, 'off': 5}, severityThreshold = window.siteDefinition && levelSeverity[siteDefinition.logLevel] || levelSeverity['off'], logToConsole = function (level) { var prefix = levelPrefix[level]; if (levelSeverity[level] < severityThreshold) return function () {}; // level is below threshold, ignore if (typeof console[level] === 'function') return function (output) {console[level](output);}; if (typeof console.log === 'function') return function (output) {console.log(prefix + output);}; if (typeof dump === 'function') return function (output) {dump(prefix + output);}; return function () {}; // no way to log in browser }, consoleLogger = { 'debug': logToConsole('debug'), 'info': logToConsole('info'), 'warn': logToConsole('warn'), 'error': logToConsole('error') }, logger = {}, level; if (logServiceUrl) { // log to the service and to the console var logs = []; // buffered logs for (level in levelPrefix) { if (!levelPrefix.hasOwnProperty(level)) continue; logger[level] = (function (levelLogger, level) { if (levelSeverity[level] < severityThreshold) return function () {}; // level is below threshold, ignore return function (stuff, e) { var output = assembleOutput(stuff, e); levelLogger(output); logs.push({ 't': Date.now(), 'l': level, 'm': output }); }; })(consoleLogger[level], level); } setInterval(function () { // each second send any logs to the server var copyOfLogs; if (logs.length < 1) return; copyOfLogs = jsonize({'logs': logs}); logs = []; $.ajax({ contentType: 'application/json', type: 'POST', url: logServiceUrl, data: copyOfLogs, error: function (jqXHR, textStatus, errorThrown) { consoleLogger.error(assembleOutput('Posting of logs to ' + logServiceUrl + ' failed: ' + textStatus, errorThrown)); }, success: function (data, textStatus, jqXHR) {} }); }, 1000); } else { // log only to the console for (level in levelPrefix) { if (!levelPrefix.hasOwnProperty(level)) continue; logger[level] = (function (levelLogger) { if (levelSeverity[level] < severityThreshold) return function () {}; // level is below threshold, ignore return function (stuff, e) { levelLogger(assembleOutput(stuff, e)); }; })(consoleLogger[level]); } } return logger; })(); // uncaught error reporting window.onerror = function (msg, url, lineNumber) { if ((msg !== null) && (msg instanceof Object)) msg = msg.toString(); codeq.log.error('Uncaught error at line ' + lineNumber + ' in ' + url + ': ' + msg); }; // ================================================================================ // The core CodeQ module // ================================================================================ var jqDisabled = $('#disabled'), // used in codeq.wait() waitCssEnter = {'cursor': 'wait', 'display': ''}; // regular expressions for the templating function, etc. var regexpQuote = new RegExp('"', 'g'), regexpBackslash = new RegExp('\\\\', 'g'), regexpAmp = new RegExp('&', 'g'), regexpLt = new RegExp('<', 'g'), regexpGt = new RegExp('>', 'g'); // event dispatch var eventListeners = {}, // keyed by event name, value is an array of listeners asyncTimer = null, queuedEvents = [], fireEvents = function () { if (asyncTimer !== null) return; asyncTimer = setTimeout(function () { var N = queuedEvents.length, i, event, args, listeners, j; asyncTimer = null; for (i = 0; i < N; i++) { event = queuedEvents[i]; listeners = eventListeners[event.name]; if (!listeners) continue; args = event.args; listeners = listeners.slice(); // make a copy of the list, so we're unaffected of any changes for (j = 0; j < listeners.length; j++) { try { listeners[j](args); } catch (e) { codeq.log.error('Error while invoking an event handler for ' + event.name + ': ' + e, e); } } } queuedEvents.splice(0, N); if (queuedEvents.length > 0) fireEvents(); }, 0); }; var makeActivityHandler = function (editor, problem_id) { var lastActivityMillis = Date.now(), deltaActivityMillis = function deltaActivityMillisFunc () { var now = Date.now(), dt = now - lastActivityMillis; lastActivityMillis = now; return dt; }, queue = [], ts = null, timer = function () { var promise; ts = null; if (queue.length === 0) return Q(true); promise = codeq.comms.sendActivity(queue, editor.getDoc().getValue(), problem_id); queue.length = 0; return promise; }, flush = function () { clearTimeout(ts); return timer(); }; return { 'queueTrace': function (trace) { trace['dt'] = deltaActivityMillis(); queue.push(trace); if (ts === null) ts = setTimeout(timer, 10000); // flush every 10 seconds return this; }, 'flush': flush, 'addAndPurge': function (trace) { var accumulatedTrace = queue; queue = []; trace['dt'] = deltaActivityMillis(); accumulatedTrace.push(trace); if (ts !== null) { clearTimeout(ts); ts = null; } return accumulatedTrace; } }; }; // replace window.alert with custom message box (only used for app errors) var jqMessage = $('div#message'); jqMessage.find('.close').click(function () { jqMessage.slideUp('fast'); }); window.alert = function (message, type) { jqMessage.find('.text').text(message); jqMessage.removeClass().addClass(type || 'error').slideDown('fast'); }; window.codeq = { 'jsonize': jsonize, 'log': log, 'samlLogin' : false,//falg if we logged in via saml 'settings': { 'gui_lang': 'en', // boot sequence overrides this if the browser uses a supported language 'gui_layout': 'line-layout' }, 'experiments': [], // experiments the user is participating in 'availableLangs': [], // filled at boot from 'supportedLangs' 'supportedLangs': { 'en': 'English', 'sl': 'Slovenščina' }, 'supportedLayouts':[ 'line-layout', 'square-layout' ], 'runningOnPhone': false, // default value, will be changed if detected otherwise 'browserType': 'unknown', 'platformType': 'unknown', 'isWebApp': false, // this is a PhoneGap/Cordova build, will be overridden in cordova.js for webapp 'setLayout': function(newLayout){ codeq.settings['gui_layout'] = newLayout; codeq.fire('layoutchange'); }, 'getLayout': function(){ return codeq.settings['gui_layout']; }, 'setLang': function (newLang) { codeq.settings['gui_lang'] = newLang; codeq.fire('langchange', {'lang': newLang}); }, 'getLang': function(){ return codeq.settings['gui_lang']; }, 'escapeHtml': function (s) { return ('' + s).replace(regexpAmp, '&').replace(regexpLt, '<').replace(regexpGt, '>'); }, /** * Returns the number of Unicode code points in the given string. * * @param s {string} * @returns {number} */ 'codePointCount': function (s) { var n = 0, i, code; if (typeof s !== 'string') { code = 'codePointCount(): argument not a string: type = ' + typeof s + ', is null = ' + (s === null); if ((typeof s === 'object') && (s !== null) && s.constructor) code += ', constructor = ' + s.constructor.name; codeq.log.error(code); return 0; } for (i = 0; i < s.length; i++) { try { code = s.charCodeAt(i); } catch (e) { codeq.log.error('Invocation of charCodeAt() failed at iteration #' + i + ': ' + e, e); return 0; } if ((code >= 0xd800) && (code < 0xe000)) i++; n++; } return n; }, 'makeActivityHandler': makeActivityHandler, 'wait': function (promise) { jqDisabled.css(waitCssEnter); return promise.fin(function () { jqDisabled.css('display', 'none'); }); }, // resolve a promise not sooner than after [timeout] seconds // from https://github.com/kriskowal/q/commit/681f6d201842d4b99bab5451ebc57ef0c3c0d49a 'throttle': function (promise, timeout) { if (timeout === undefined) { timeout = promise; promise = undefined; } var deferred = Q.defer(); Q.when(promise, undefined, undefined, deferred.notify); setTimeout(function () { deferred.resolve(promise); }, timeout); return deferred.promise; }, // codeq event handling 'fire': function (eventName, args) { queuedEvents.push({'name': eventName, 'args': args}); fireEvents(); }, 'on': function (eventName, callback) { var listeners = eventListeners[eventName], i; if (listeners) { for (i = listeners.length - 1; i >= 0; i--) { if (listeners[i] === callback) return; // already registered } } else { listeners = []; eventListeners[eventName] = listeners; } listeners.push(callback); }, 'off': function (eventName, callback) { var listeners = eventListeners[eventName], i; if (listeners) { for (i = listeners.length - 1; i >= 0; i--) { if (listeners[i] === callback) listeners.splice(i, 1); } } }, // reset the app 'reset': function (reason) { codeq.log.info('App reset: ' + (reason || 'no reason given')); codeq.globalStateMachine.transition('login'); codeq.wait( codeq.comms.forceReset() ) .fin(function () { if (reason) { alert(reason); } }) .done(); } }; })();