Index: lams_build/conf/etherpad/etherpad-lite/src/node/hooks/express/webaccess.js =================================================================== diff -u --- lams_build/conf/etherpad/etherpad-lite/src/node/hooks/express/webaccess.js (revision 0) +++ lams_build/conf/etherpad/etherpad-lite/src/node/hooks/express/webaccess.js (revision c48b344f1b50c96265166bee0aad75c5fb6f599d) @@ -0,0 +1,207 @@ +const express = require('express'); +const log4js = require('log4js'); +const httpLogger = log4js.getLogger('http'); +const settings = require('../../utils/Settings'); +const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); +const ueberStore = require('../../db/SessionStore'); +const stats = require('ep_etherpad-lite/node/stats'); +const sessionModule = require('express-session'); +const cookieParser = require('cookie-parser'); + +exports.normalizeAuthzLevel = (level) => { + if (!level) return false; + switch (level) { + case true: + return 'create'; + case 'create': + return level; + default: + httpLogger.warn(`Unknown authorization level '${level}', denying access`); + } + return false; +}; + +exports.checkAccess = (req, res, next) => { + const hookResultMangle = (cb) => { + return (err, data) => { + return cb(!err && data.length && data[0]); + }; + }; + + // This may be called twice per access: once before authentication is checked and once after (if + // settings.requireAuthorization is true). + const authorize = (fail) => { + // Do not require auth for static paths and the API...this could be a bit brittle + if (req.path.match(/^\/(static|javascripts|pluginfw|api)/)) return next(); + + const grant = (level) => { + level = exports.normalizeAuthzLevel(level); + if (!level) return fail(); + const user = req.session.user; + if (user == null) return next(); // This will happen if authentication is not required. + const padID = (req.path.match(/^\/p\/(.*)$/) || [])[1]; + if (padID == null) return next(); + // The user was granted access to a pad. Remember the authorization level in the user's + // settings so that SecurityManager can approve or deny specific actions. + if (user.padAuthorizations == null) user.padAuthorizations = {}; + user.padAuthorizations[padID] = level; + return next(); + }; + + if (req.path.toLowerCase().indexOf('/admin') !== 0) { + if (!settings.requireAuthentication) return grant('create'); + if (!settings.requireAuthorization && req.session && req.session.user) return grant('create'); + } + + if (req.session && req.session.user && req.session.user.is_admin) return grant('create'); + + hooks.aCallFirst('authorize', {req, res, next, resource: req.path}, hookResultMangle(grant)); + }; + + /* Authentication OR authorization failed. */ + const failure = () => { + return hooks.aCallFirst('authFailure', {req, res, next}, hookResultMangle((ok) => { + if (ok) return; + // No plugin handled the authn/authz failure. Fall back to basic authentication. + res.header('WWW-Authenticate', 'Basic realm="Protected Area"'); + // Delay the error response for 1s to slow down brute force attacks. + setTimeout(() => { + res.status(401).send('Authentication Required'); + }, 1000); + })); + }; + + // Access checking is done in three steps: + // + // 1) Try to just access the thing. If access fails (perhaps authentication has not yet completed, + // or maybe different credentials are required), go to the next step. + // 2) Try to authenticate. (Or, if already logged in, reauthenticate with different credentials if + // supported by the authn scheme.) If authentication fails, give the user a 401 error to + // request new credentials. Otherwise, go to the next step. + // 3) Try to access the thing again. If this fails, give the user a 401 error. + // + // Plugins can use the 'next' callback (from the hook's context) to break out at any point (e.g., + // to process an OAuth callback). Plugins can use the authFailure hook to override the default + // error handling behavior (e.g., to redirect to a login page). + + let step1PreAuthenticate, step2Authenticate, step3Authorize; + + step1PreAuthenticate = () => authorize(step2Authenticate); + + step2Authenticate = () => { + if (settings.users == null) settings.users = {}; + const ctx = {req, res, users: settings.users, next}; + // If the HTTP basic auth header is present, extract the username and password so it can be + // given to authn plugins. + const httpBasicAuth = + req.headers.authorization && req.headers.authorization.search('Basic ') === 0; + if (httpBasicAuth) { + const userpass = + Buffer.from(req.headers.authorization.split(' ')[1], 'base64').toString().split(':'); + ctx.username = userpass.shift(); + ctx.password = userpass.join(':'); + } + hooks.aCallFirst('authenticate', ctx, hookResultMangle((ok) => { + if (!ok) { + // Fall back to HTTP basic auth. + if (!httpBasicAuth) return failure(); + if (!(ctx.username in settings.users)) { + httpLogger.info(`Failed authentication from IP ${req.ip} - no such user`); + return failure(); + } + if (settings.users[ctx.username].password !== ctx.password) { + httpLogger.info(`Failed authentication from IP ${req.ip} for user ${ctx.username} - incorrect password`); + return failure(); + } + httpLogger.info(`Successful authentication from IP ${req.ip} for user ${ctx.username}`); + settings.users[ctx.username].username = ctx.username; + req.session.user = settings.users[ctx.username]; + } + if (req.session.user == null) { + httpLogger.error('authenticate hook failed to add user settings to session'); + res.status(500).send('Internal Server Error'); + return; + } + step3Authorize(); + })); + }; + + step3Authorize = () => authorize(failure); + + step1PreAuthenticate(); +}; + +exports.secret = null; + +exports.expressConfigure = (hook_name, args, cb) => { + // Measure response time + args.app.use((req, res, next) => { + const stopWatch = stats.timer('httpRequests').start(); + const sendFn = res.send; + res.send = function() { // function, not arrow, due to use of 'arguments' + stopWatch.end(); + sendFn.apply(res, arguments); + }; + next(); + }); + + // If the log level specified in the config file is WARN or ERROR the application server never starts listening to requests as reported in issue #158. + // Not installing the log4js connect logger when the log level has a higher severity than INFO since it would not log at that level anyway. + if (!(settings.loglevel === 'WARN' || settings.loglevel === 'ERROR')) + args.app.use(log4js.connectLogger(httpLogger, {level: log4js.levels.DEBUG, format: ':status, :method :url'})); + + /* Do not let express create the session, so that we can retain a + * reference to it for socket.io to use. Also, set the key (cookie + * name) to a javascript identifier compatible string. Makes code + * handling it cleaner :) */ + + if (!exports.sessionStore) { + exports.sessionStore = new ueberStore(); + exports.secret = settings.sessionKey; + } + + const sameSite = 'None'; + + args.app.sessionStore = exports.sessionStore; + args.app.use(sessionModule({ + secret: exports.secret, + store: args.app.sessionStore, + resave: false, + saveUninitialized: true, + name: 'express_sid', + proxy: true, + cookie: { + /* + * Firefox started enforcing sameSite, see https://github.com/ether/etherpad-lite/issues/3989 + * for details. In response we set it based on if SSL certs are set in Etherpad. Note that if + * You use Nginx or so for reverse proxy this may cause problems. Use Certificate pinning to remedy. + */ + sameSite: sameSite, + /* + * The automatic express-session mechanism for determining if the + * application is being served over ssl is similar to the one used for + * setting the language cookie, which check if one of these conditions is + * true: + * + * 1. we are directly serving the nodejs application over SSL, using the + * "ssl" options in settings.json + * + * 2. we are serving the nodejs application in plaintext, but we are using + * a reverse proxy that terminates SSL for us. In this case, the user + * has to set trustProxy = true in settings.json, and the information + * wheter the application is over SSL or not will be extracted from the + * X-Forwarded-Proto HTTP header + * + * Please note that this will not be compatible with applications being + * served over http and https at the same time. + * + * reference: https://github.com/expressjs/session/blob/v1.17.0/README.md#cookiesecure + */ + secure: 'auto', + } + })); + + args.app.use(cookieParser(settings.sessionKey, {})); + + args.app.use(exports.checkAccess); +}; Index: lams_build/conf/etherpad/etherpad-lite/src/static/js/pad_cookie.js =================================================================== diff -u --- lams_build/conf/etherpad/etherpad-lite/src/static/js/pad_cookie.js (revision 0) +++ lams_build/conf/etherpad/etherpad-lite/src/static/js/pad_cookie.js (revision c48b344f1b50c96265166bee0aad75c5fb6f599d) @@ -0,0 +1,149 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/** + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +var padcookie = (function() +{ + var cookieName = isHttpsScheme() ? "prefs" : "prefsHttp"; + + function getRawCookie() + { + // returns null if can't get cookie text + if (!document.cookie) + { + return null; + } + // look for (start of string OR semicolon) followed by whitespace followed by prefs=(something); + var regexResult = document.cookie.match(new RegExp("(?:^|;)\\s*" + cookieName + "=([^;]*)(?:;|$)")); + if ((!regexResult) || (!regexResult[1])) + { + return null; + } + return regexResult[1]; + } + + function setRawCookie(safeText) + { + var expiresDate = new Date(); + expiresDate.setFullYear(3000); + var secure = isHttpsScheme() ? ";secure" : ""; + var sameSite = ";sameSite=None"; + document.cookie = (cookieName + "=" + safeText + ";expires=" + expiresDate.toGMTString() + secure + sameSite); + } + + function parseCookie(text) + { + // returns null if can't parse cookie. + try + { + var cookieData = JSON.parse(unescape(text)); + return cookieData; + } + catch (e) + { + return null; + } + } + + function stringifyCookie(data) + { + return escape(JSON.stringify(data)); + } + + function saveCookie() + { + if (!inited) + { + return; + } + setRawCookie(stringifyCookie(cookieData)); + + if ((!getRawCookie()) && (!alreadyWarnedAboutNoCookies)) + { + $.gritter.add({ + title: "Error", + text: html10n.get("pad.noCookie"), + sticky: true, + class_name: "error" + }); + alreadyWarnedAboutNoCookies = true; + } + } + + function isHttpsScheme() { + return window.location.protocol == "https:"; + } + + var wasNoCookie = true; + var cookieData = {}; + var alreadyWarnedAboutNoCookies = false; + var inited = false; + + var pad = undefined; + var self = { + init: function(prefsToSet, _pad) + { + pad = _pad; + + var rawCookie = getRawCookie(); + if (rawCookie) + { + var cookieObj = parseCookie(rawCookie); + if (cookieObj) + { + wasNoCookie = false; // there was a cookie + delete cookieObj.userId; + delete cookieObj.name; + delete cookieObj.colorId; + cookieData = cookieObj; + } + } + + for (var k in prefsToSet) + { + cookieData[k] = prefsToSet[k]; + } + + inited = true; + saveCookie(); + }, + wasNoCookie: function() + { + return wasNoCookie; + }, + isCookiesEnabled: function() { + return !!getRawCookie(); + }, + getPref: function(prefName) + { + return cookieData[prefName]; + }, + setPref: function(prefName, value) + { + cookieData[prefName] = value; + saveCookie(); + } + }; + return self; +}()); + +exports.padcookie = padcookie; Index: lams_build/conf/etherpad/etherpad-lite/src/static/js/pad_utils.js =================================================================== diff -u --- lams_build/conf/etherpad/etherpad-lite/src/static/js/pad_utils.js (revision 0) +++ lams_build/conf/etherpad/etherpad-lite/src/static/js/pad_utils.js (revision c48b344f1b50c96265166bee0aad75c5fb6f599d) @@ -0,0 +1,577 @@ +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ + +/** + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var Security = require('./security'); + +/** + * Generates a random String with the given length. Is needed to generate the Author, Group, readonly, session Ids + */ + +function randomString(len) +{ + var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + var randomstring = ''; + len = len || 20 + for (var i = 0; i < len; i++) + { + var rnum = Math.floor(Math.random() * chars.length); + randomstring += chars.substring(rnum, rnum + 1); + } + return randomstring; +} + +function createCookie(name, value, days, path){ /* Used by IE */ + if (days) + { + var date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + var expires = "; expires=" + date.toGMTString(); + } + else{ + var expires = ""; + } + + if(!path){ // IF the Path of the cookie isn't set then just create it on root + path = "/"; + } + + //Check if we accessed the pad over https + var secure = window.location.protocol == "https:" ? ";secure" : ""; + var isHttpsScheme = window.location.protocol === "https:"; + var sameSite = ";sameSite=None"; + + //Check if the browser is IE and if so make sure the full path is set in the cookie + if((navigator.appName == 'Microsoft Internet Explorer') || ((navigator.appName == 'Netscape') && (new RegExp("Trident/.*rv:([0-9]{1,}[\.0-9]{0,})").exec(navigator.userAgent) != null))){ + document.cookie = name + "=" + value + expires + "; path=/" + secure + sameSite; /* Note this bodge fix for IE is temporary until auth is rewritten */ + } + else{ + document.cookie = name + "=" + value + expires + "; path=" + path + secure + sameSite; + } + +} + +function readCookie(name) +{ + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for (var i = 0; i < ca.length; i++) + { + var c = ca[i]; + while (c.charAt(0) == ' ') c = c.substring(1, c.length); + if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length); + } + return null; +} + +var padutils = { + escapeHtml: function(x) + { + return Security.escapeHTML(String(x)); + }, + uniqueId: function() + { + var pad = require('./pad').pad; // Sidestep circular dependency + function encodeNum(n, width) + { + // returns string that is exactly 'width' chars, padding with zeros + // and taking rightmost digits + return (Array(width + 1).join('0') + Number(n).toString(35)).slice(-width); + } + return [pad.getClientIp(), encodeNum(+new Date, 7), encodeNum(Math.floor(Math.random() * 1e9), 4)].join('.'); + }, + uaDisplay: function(ua) + { + var m; + + function clean(a) + { + var maxlen = 16; + a = a.replace(/[^a-zA-Z0-9\.]/g, ''); + if (a.length > maxlen) + { + a = a.substr(0, maxlen); + } + return a; + } + + function checkver(name) + { + var m = ua.match(RegExp(name + '\\/([\\d\\.]+)')); + if (m && m.length > 1) + { + return clean(name + m[1]); + } + return null; + } + + // firefox + if (checkver('Firefox')) + { + return checkver('Firefox'); + } + + // misc browsers, including IE + m = ua.match(/compatible; ([^;]+);/); + if (m && m.length > 1) + { + return clean(m[1]); + } + + // iphone + if (ua.match(/\(iPhone;/)) + { + return 'iPhone'; + } + + // chrome + if (checkver('Chrome')) + { + return checkver('Chrome'); + } + + // safari + m = ua.match(/Safari\/[\d\.]+/); + if (m) + { + var v = '?'; + m = ua.match(/Version\/([\d\.]+)/); + if (m && m.length > 1) + { + v = m[1]; + } + return clean('Safari' + v); + } + + // everything else + var x = ua.split(' ')[0]; + return clean(x); + }, + // e.g. "Thu Jun 18 2009 13:09" + simpleDateTime: function(date) + { + var d = new Date(+date); // accept either number or date + var dayOfWeek = (['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'])[d.getDay()]; + var month = (['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'])[d.getMonth()]; + var dayOfMonth = d.getDate(); + var year = d.getFullYear(); + var hourmin = d.getHours() + ":" + ("0" + d.getMinutes()).slice(-2); + return dayOfWeek + ' ' + month + ' ' + dayOfMonth + ' ' + year + ' ' + hourmin; + }, + findURLs: function(text) + { + // copied from ACE + var _REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/; + var _REGEX_URLCHAR = new RegExp('(' + /[-:@a-zA-Z0-9_.,~%+\/?=&#;()$]/.source + '|' + _REGEX_WORDCHAR.source + ')'); + var _REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|nfs):\/\/|(about|geo|mailto|tel):)/.source + _REGEX_URLCHAR.source + '*(?![:.,;])' + _REGEX_URLCHAR.source, 'g'); + + // returns null if no URLs, or [[startIndex1, url1], [startIndex2, url2], ...] + + + function _findURLs(text) + { + _REGEX_URL.lastIndex = 0; + var urls = null; + var execResult; + while ((execResult = _REGEX_URL.exec(text))) + { + urls = (urls || []); + var startIndex = execResult.index; + var url = execResult[0]; + urls.push([startIndex, url]); + } + + return urls; + } + + return _findURLs(text); + }, + escapeHtmlWithClickableLinks: function(text, target) + { + var idx = 0; + var pieces = []; + var urls = padutils.findURLs(text); + + function advanceTo(i) + { + if (i > idx) + { + pieces.push(Security.escapeHTML(text.substring(idx, i))); + idx = i; + } + } + if (urls) + { + for (var j = 0; j < urls.length; j++) + { + var startIndex = urls[j][0]; + var href = urls[j][1]; + advanceTo(startIndex); + // Using rel="noreferrer" stops leaking the URL/location of the pad when clicking links in the document. + // Not all browsers understand this attribute, but it's part of the HTML5 standard. + // https://html.spec.whatwg.org/multipage/links.html#link-type-noreferrer + // Additionally, we do rel="noopener" to ensure a higher level of referrer security. + // https://html.spec.whatwg.org/multipage/links.html#link-type-noopener + // https://mathiasbynens.github.io/rel-noopener/ + // https://github.com/ether/etherpad-lite/pull/3636 + pieces.push(''); + advanceTo(startIndex + href.length); + pieces.push(''); + } + } + advanceTo(text.length); + return pieces.join(''); + }, + bindEnterAndEscape: function(node, onEnter, onEscape) + { + + // Use keypress instead of keyup in bindEnterAndEscape + // Keyup event is fired on enter in IME (Input Method Editor), But + // keypress is not. So, I changed to use keypress instead of keyup. + // It is work on Windows (IE8, Chrome 6.0.472), CentOs (Firefox 3.0) and Mac OSX (Firefox 3.6.10, Chrome 6.0.472, Safari 5.0). + if (onEnter) + { + node.keypress(function(evt) + { + if (evt.which == 13) + { + onEnter(evt); + } + }); + } + + if (onEscape) + { + node.keydown(function(evt) + { + if (evt.which == 27) + { + onEscape(evt); + } + }); + } + }, + timediff: function(d) + { + var pad = require('./pad').pad; // Sidestep circular dependency + function format(n, word) + { + n = Math.round(n); + return ('' + n + ' ' + word + (n != 1 ? 's' : '') + ' ago'); + } + d = Math.max(0, (+(new Date) - (+d) - pad.clientTimeOffset) / 1000); + if (d < 60) + { + return format(d, 'second'); + } + d /= 60; + if (d < 60) + { + return format(d, 'minute'); + } + d /= 60; + if (d < 24) + { + return format(d, 'hour'); + } + d /= 24; + return format(d, 'day'); + }, + makeAnimationScheduler: function(funcToAnimateOneStep, stepTime, stepsAtOnce) + { + if (stepsAtOnce === undefined) + { + stepsAtOnce = 1; + } + + var animationTimer = null; + + function scheduleAnimation() + { + if (!animationTimer) + { + animationTimer = window.setTimeout(function() + { + animationTimer = null; + var n = stepsAtOnce; + var moreToDo = true; + while (moreToDo && n > 0) + { + moreToDo = funcToAnimateOneStep(); + n--; + } + if (moreToDo) + { + // more to do + scheduleAnimation(); + } + }, stepTime * stepsAtOnce); + } + } + return { + scheduleAnimation: scheduleAnimation + }; + }, + makeShowHideAnimator: function(funcToArriveAtState, initiallyShown, fps, totalMs) + { + var animationState = (initiallyShown ? 0 : -2); // -2 hidden, -1 to 0 fade in, 0 to 1 fade out + var animationFrameDelay = 1000 / fps; + var animationStep = animationFrameDelay / totalMs; + + var scheduleAnimation = padutils.makeAnimationScheduler(animateOneStep, animationFrameDelay).scheduleAnimation; + + function doShow() + { + animationState = -1; + funcToArriveAtState(animationState); + scheduleAnimation(); + } + + function doQuickShow() + { // start showing without losing any fade-in progress + if (animationState < -1) + { + animationState = -1; + } + else if (animationState <= 0) + { + animationState = animationState; + } + else + { + animationState = Math.max(-1, Math.min(0, -animationState)); + } + funcToArriveAtState(animationState); + scheduleAnimation(); + } + + function doHide() + { + if (animationState >= -1 && animationState <= 0) + { + animationState = 1e-6; + scheduleAnimation(); + } + } + + function animateOneStep() + { + if (animationState < -1 || animationState == 0) + { + return false; + } + else if (animationState < 0) + { + // animate show + animationState += animationStep; + if (animationState >= 0) + { + animationState = 0; + funcToArriveAtState(animationState); + return false; + } + else + { + funcToArriveAtState(animationState); + return true; + } + } + else if (animationState > 0) + { + // animate hide + animationState += animationStep; + if (animationState >= 1) + { + animationState = 1; + funcToArriveAtState(animationState); + animationState = -2; + return false; + } + else + { + funcToArriveAtState(animationState); + return true; + } + } + } + + return { + show: doShow, + hide: doHide, + quickShow: doQuickShow + }; + }, + _nextActionId: 1, + uncanceledActions: {}, + getCancellableAction: function(actionType, actionFunc) + { + var o = padutils.uncanceledActions[actionType]; + if (!o) + { + o = {}; + padutils.uncanceledActions[actionType] = o; + } + var actionId = (padutils._nextActionId++); + o[actionId] = true; + return function() + { + var p = padutils.uncanceledActions[actionType]; + if (p && p[actionId]) + { + actionFunc(); + } + }; + }, + cancelActions: function(actionType) + { + var o = padutils.uncanceledActions[actionType]; + if (o) + { + // clear it + delete padutils.uncanceledActions[actionType]; + } + }, + makeFieldLabeledWhenEmpty: function(field, labelText) + { + field = $(field); + + function clear() + { + field.addClass('editempty'); + field.val(labelText); + } + field.focus(function() + { + if (field.hasClass('editempty')) + { + field.val(''); + } + field.removeClass('editempty'); + }); + field.blur(function() + { + if (!field.val()) + { + clear(); + } + }); + return { + clear: clear + }; + }, + getCheckbox: function(node) + { + return $(node).is(':checked'); + }, + setCheckbox: function(node, value) + { + if (value) + { + $(node).attr('checked', 'checked'); + } + else + { + $(node).removeAttr('checked'); + } + }, + bindCheckboxChange: function(node, func) + { + $(node).change(func); + }, + encodeUserId: function(userId) + { + return userId.replace(/[^a-y0-9]/g, function(c) + { + if (c == ".") return "-"; + return 'z' + c.charCodeAt(0) + 'z'; + }); + }, + decodeUserId: function(encodedUserId) + { + return encodedUserId.replace(/[a-y0-9]+|-|z.+?z/g, function(cc) + { + if (cc == '-') return '.'; + else if (cc.charAt(0) == 'z') + { + return String.fromCharCode(Number(cc.slice(1, -1))); + } + else + { + return cc; + } + }); + } +}; + +var globalExceptionHandler = undefined; +function setupGlobalExceptionHandler() { + if (!globalExceptionHandler) { + globalExceptionHandler = function test (msg, url, linenumber) + { + var errorId = randomString(20); + var userAgent = padutils.escapeHtml(navigator.userAgent); + + var msgAlreadyVisible = false; + $('.gritter-item .error-msg').each(function() { + if ($(this).text() === msg) { + msgAlreadyVisible = true; + } + }); + + if (!msgAlreadyVisible) { + errorMsg = "Please press and hold Ctrl and press F5 to reload this page
\ + If the problem persists please send this error message to your webmaster:

\ +
\ + ErrorId: " + errorId + "
\ + URL: " + padutils.escapeHtml(window.location.href) + "
\ + UserAgent: " + userAgent + "
\ + "+ msg + " in " + url + " at line " + linenumber + '
'; + + $.gritter.add({ + title: "An error occurred", + text: errorMsg, + class_name: "error", + position: 'bottom', + sticky: true, + }); + } + + //send javascript errors to the server + var errObj = {errorInfo: JSON.stringify({errorId: errorId, msg: msg, url: window.location.href, linenumber: linenumber, userAgent: navigator.userAgent})}; + var loc = document.location; + var url = loc.protocol + "//" + loc.hostname + ":" + loc.port + "/" + loc.pathname.substr(1, loc.pathname.indexOf("/p/")) + "jserror"; + + $.post(url, errObj); + + return false; + }; + window.onerror = globalExceptionHandler; + } +} + +padutils.setupGlobalExceptionHandler = setupGlobalExceptionHandler; + +padutils.binarySearch = require('./ace2_common').binarySearch; + +exports.randomString = randomString; +exports.createCookie = createCookie; +exports.readCookie = readCookie; +exports.padutils = padutils;