/* CodeQ: an online programming tutor. Copyright (C) 2015 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'), regexpWhiteSpaceStart = new RegExp('^[ \r\n\t]+'), regexpWhiteSpaceEnd = new RegExp('[ \r\n\t]+$'), regexpWhiteSpaceTrim = new RegExp('^[ \\t\\r\\n]*(.*[^ \\t\\r\\n])[ \\t\\r\\n]*$', 'm'), regexpAmp = new RegExp('&', 'g'), regexpLt = new RegExp('<', 'g'), regexpGt = new RegExp('>', 'g'), regexpCR = new RegExp('\\r', 'g'), regexpLF = new RegExp('\\n', 'g'), regexpTab = new RegExp('\\t', 'g'); // convert a string into its definition (javascript literal) var stringToDef = function (str) { return str.replace(regexpBackslash, '\\\\').replace(regexpQuote, '\\"').replace(regexpCR, '\\r').replace(regexpLF, '\\n').replace(regexpTab, '\\t'); }; var resources = {}; // resource tree, loaded from data/resources.json in the boot sequence // 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 tokenize = function (line) { var result = [], N = line.length, i = 0, // character index into line storedAtom = null, // the previous atom, to decide whether to store a string or an object {key:'', value:''} atom = [], // currently parsed content isKeyValue = false, storeAtom = function () { if (atom.length == 0) return; if (storedAtom === null) storedAtom = atom.join(''); else if (isKeyValue) { result.push({'key': storedAtom, 'value': atom.join('')}); storedAtom = null; isKeyValue = false; } else { result.push(storedAtom); storedAtom = atom.join(''); } atom.length = 0; }, c, q; while (i < N) { c = line[i++]; switch (c) { // end-of-atom characters case ' ': case '\r': case '\n': case '\t': storeAtom(); break; // escape character case '\\': if (i < N) atom.push(line[i++]); break; // key-value delimiter character case '=': if (storedAtom) { if (isKeyValue) atom.push(c); else if (atom.length == 0) isKeyValue = true; // must have been a whitespace before "=" else { storeAtom(); isKeyValue = true; } } else atom.push(c); // "=" is delimiter only before the value, not before the key break; // quoted string case '"': case "'": stringConsume: while (i < N) { q = line[i++]; switch (q) { case '\\': if (i < N) atom.push(line[i++]); break; case c: break stringConsume; default: atom.push(q); break; } } break; default: atom.push(c); break; } } // purge anything in cache storeAtom(); if (storedAtom !== null) result.push(storedAtom); return result; }; var resolveResource = function (resourceName, resourceBranches) { var traversedPath = ['data'], // top-level directory branch = resources, candidate = null, i, fragment; if (!resourceName) { codeq.log.error('No resource name provided; path: "' + resourceBranches.join('/') + '"'); return null; } if (branch[resourceName]) candidate = 'data/' + resourceName; // top-level match for (i = 0; i < resourceBranches.length; i++) { fragment = resourceBranches[i]; branch = branch[fragment]; if (!branch) { codeq.log.error('Resource sub-branch ' + fragment + ' does not exist; resource: "' + resourceName + '", path: "' + resourceBranches.join('/') + '"'); break; } traversedPath.push(fragment); if (branch[resourceName]) candidate = traversedPath.join('/') + '/' + resourceName; } if (candidate) return codeq.ajaxPrefix + candidate; codeq.log.error('Resource ' + resourceName + ' was not found; path: "' + resourceBranches.join('/') + '"'); return null; }; var directiveHandlers = { 'resource': function (code, tokens, templatePath) { code.push('_result.push("', resolveResource(tokens[1], templatePath) || 'data/broken.png', '");\n'); }, 'img': function (code, tokens, templatePath) { var N = tokens.length, attrs = [], token, i; for (i = 1; i < N; i++) { token = tokens[i]; if (!token || typeof token !== 'object') { codeq.log.error('Invalid token at position ' + i + ' in @img'); continue; } switch (token.key) { case 'src': attrs.push('src="' + resolveResource(token.value, templatePath) + '"'); break; case 'alt': attrs.push('alt="' + codeq.escapeHtml(token.value) + '"'); break; case 'class': attrs.push('class="' + token.value + '"'); break; } } code.push('_result.push("");\n'); } }; 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; } }; }; 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' }, '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']; }, 'setResources': function (newResources) { resources = newResources; }, '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'); }); }, 'templator': function (str, templatePath, templateName) { var f, parts, i, subparts, subpart, src = [ 'var _result = [], echo = function (s) { _result.push(s); };\n' ], atoms, j, atom, debugPrefix, s, r, tokens, handler; if (!templateName) templateName = 'unknown template'; debugPrefix = '[' + templateName + ']'; if (!templatePath) templatePath = []; // remove comments parts = str.split('[%--'); // break on start-of-comment atoms = [ parts[0] ]; // first part is not a comment for (i = 1; i < parts.length; i++) { // iterate over start-of-comments atom = parts[i].split('--%]'); // break on end-of-comment if (atom.length > 1) { // if end-of-comment was encountered atoms.push(atom[1]); // add whatever is trailing it for (j = 2; j < atom.length; j++) { // re-add even dangling end-of-comments atoms.push('--%]'); atoms.push(atom[j]); } } } // start processing parts = atoms.join('').split('[%'); if (parts[0].length > 0) src.push('_result.push("', stringToDef(parts[0]), '");\n'); // the first part that doesn't begin with '[%' for (i = 1; i < parts.length; i++) { // for every part that begins with '[%' if (parts[i].slice(0, 2) === '--') { // a comment start subparts = parts[i].split('--%]'); // split at comment end } else { // a start of a statement or of a value reference subparts = parts[i].split('%]'); // there should be only one terminating '%]', find it subpart = subparts[0].replace(regexpWhiteSpaceStart, '').replace(regexpWhiteSpaceEnd, ''); // trim the white space if (subpart.length > 0) { if (subpart[0] === '=') { // a value reference s = subpart.slice(1); r = regexpWhiteSpaceTrim.exec(s); r = r && r[1] || s; src.push('_result.push(typeof this.', r, ' === \'undefined\' ? \'', r, ' missing\' : this.', r, ');\n'); } else if (subpart[0] === '@') { // a directive tokens = tokenize(subpart.slice(1)); if (tokens.length === 0) { codeq.log.error('An empty directive in ' + templateName); } else { handler = directiveHandlers[tokens[0]]; if (!handler) { codeq.log.error('An unknown directive in ' + templateName + ': ' + tokens[0]); } else { handler(src, tokens, templatePath); } } } else { // javascript statement(s) src.push(subpart, '\n'); } } } if ((subparts.length > 1) && (subparts[1].length > 0)) { // there's a trailing text src.push('_result.push("', stringToDef(subparts[1]), '");\n'); } } src.push('return _result.join("");'); // if (_internalDoLog_) codeq.log.debug('createTemplate(): ' + debugPrefix + ' creating templating function:\nfunction(_components_) {\n' + src.join('') + '\n}\n=== Created from template: ===\n' + str); try { f = new Function(src.join('')); // create a function based on the given template } catch (e) { codeq.log.error('createTemplate(): ' + debugPrefix + ' Failed to instantiate template function: ' + e + '\nfunction() {\n' + src.join('') + '\n}\n=== Created from template: ===\n' + str, e); throw e; } return function (args) { var esc = codeq.escapeHtml, escArgs = {}, key; if ((typeof args === 'object') && (args !== null)) { for (key in args) { if (!args.hasOwnProperty(key)) continue; escArgs[key] = esc(args[key]); } } try { return f.apply(escArgs); } catch (e) { codeq.log.error('Error evaluating template ' + templateName + ' function: ' + e + '\nfunction() {\n' + src.join('') + '\nCreated from template:\n' + str, e); throw e; } }; }, // 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.getSid() == null ? Q() : codeq.comms.logout() .finally( codeq.comms.disconnect ) .fail(function (e) { codeq.log.debug(e) }) // ignore errors*/ codeq.comms.forceReset() ) .fin(function () { if (reason) { alert(reason); } }) .done(); } }; })();