mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-01-21 23:09:51 +01:00
0a0b90c4d0
Pull request with discussion: https://github.com/ether/etherpad-lite/pull/3636 What's already there: * `meta name=referrer`: already done in 1.6.1: https://github.com/ether/etherpad-lite/pull/3044 https://caniuse.com/#feat=referrer-policy https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-delivery-meta (Chrome>=78, Firefox>=70, Safari>=13, Opera>=64, ~IE[1], ~Edge[1]) The previous two commits (by @joelpurra) I backported in this batch: * `<a rel=noreferrer>`: a pull request denied before: https://github.com/ether/etherpad-lite/pull/2498 https://html.spec.whatwg.org/multipage/links.html#link-type-noreferrer https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types (Firefox>=37, I can't find more info about support) This commit adds the following: * `<a rel="noopener">`: fixing a not-so-well-known way to extract referer https://html.spec.whatwg.org/multipage/links.html#link-type-noopener (Chrome>=49, Firefox>=52, Safari>=10.1, Opera>=36, !IE, !Edge) * `Referrer-Policy: same-origin`: the last bastion of referrer security https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy (Chrome>=61, Firefox>=52, Safari>=11.1, Opera>=48, !IE, !Edge) meta name=referrer wasn't enough. I happened to leak a few referrers with my Firefox browser, though for some browsers it could have been enough. [1] IE>=11, Edge>=18 use a different syntax for meta name=referrer, making it most probably incompatible (but I may be wrong on that, they may support both, but I have no way to test it currently). The next Edge release will be based on Chromium, so for that the Chrome version applies.
557 lines
15 KiB
JavaScript
557 lines
15 KiB
JavaScript
/**
|
|
* 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" : "";
|
|
|
|
//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; /* Note this bodge fix for IE is temporary until auth is rewritten */
|
|
}
|
|
else{
|
|
document.cookie = name + "=" + value + expires + "; path=" + path + secure;
|
|
}
|
|
|
|
}
|
|
|
|
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('<a ', (target ? 'target="' + Security.escapeHTMLAttribute(target) + '" ' : ''), 'href="', Security.escapeHTMLAttribute(href), '" rel="noreferrer noopener">');
|
|
advanceTo(startIndex + href.length);
|
|
pieces.push('</a>');
|
|
}
|
|
}
|
|
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);
|
|
if ($("#editorloadingbox").attr("display") != "none"){
|
|
//show javascript errors to the user
|
|
$("#editorloadingbox").css("padding", "10px");
|
|
$("#editorloadingbox").css("padding-top", "45px");
|
|
$("#editorloadingbox").html("<div style='text-align:left;color:red;font-size:16px;'><b>An error occurred</b><br>The error was reported with the following id: '" + errorId + "'<br><br><span style='color:black;font-weight:bold;font-size:16px'>Please press and hold Ctrl and press F5 to reload this page, if the problem persists please send this error message to your webmaster: </span><div style='color:black;font-size:14px'>'"
|
|
+ "ErrorId: " + errorId + "<br>URL: " + padutils.escapeHtml(window.location.href) + "<br>UserAgent: " + userAgent + "<br>" + msg + " in " + url + " at line " + linenumber + "'</div></div>");
|
|
}
|
|
|
|
//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;
|