Move pad2 to pad and create bounce app

This commit is contained in:
Caleb James DeLisle 2017-08-28 12:25:05 +02:00
parent e62faf7c7b
commit 056285a7b6
15 changed files with 1043 additions and 2651 deletions

15
www/bounce/index.html Normal file
View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html class="cp">
<head>
<title data-localization="main_title">CryptPad: Zero Knowledge, Collaborative Real Time Editing</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" type="image/png" href="/customize/main-favicon.png" id="favicon"/>
<script async data-bootload="main.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
</head>
<body class="html">
<noscript>
<p><strong>OOPS</strong> In order to do encryption in your browser, Javascript is really <strong>really</strong> required.</p>
<p><strong>OUPS</strong> Afin de pouvoir réaliser le chiffrement dans votre navigateur, Javascript est <strong>vraiment</strong> nécessaire.</p>
</noscript>
</html>

10
www/bounce/main.js Normal file
View File

@ -0,0 +1,10 @@
define([], function () {
if (window.localStorage && window.localStorage.FS_hash) {
window.alert('The bounce application must only be used from the sandbox domain, ' +
'please report this issue on https://github.com/xwiki-labs/cryptpad');
return;
}
var bounceTo = decodeURIComponent(window.location.hash.slice(1));
if (!bounceTo) { return; }
window.location.href = bounceTo;
});

9
www/bounce/readme.md Normal file
View File

@ -0,0 +1,9 @@
# Bounce app
This app redirects you to a new URL.
This app must only be served from CryptPad's safe origin, if this app detects that it is being
served from the unsafe origin, it will throw an alert that it is misconfigured and it will refuse
to redirect.
If the URL is a javascript: URL, it will be trapped by CryptPad's Content Security Policy rules
or in the worst case, it will run in the context of the sandboxed origin.

View File

@ -1,17 +1,17 @@
<!DOCTYPE html>
<html class="cp pad">
<html>
<head>
<title>CryptPad</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer" />
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<script async data-bootload="main.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<style>
html, body {
margin: 0px;
padding: 0px;
}
#pad-iframe {
#sbox-iframe {
position:fixed;
top:0px;
left:0px;
@ -27,5 +27,4 @@
</style>
</head>
<body>
<iframe id="pad-iframe"></iframe><script src="/common/noscriptfix.js"></script>
<iframe id="sbox-iframe">

View File

@ -1,13 +1,8 @@
<!DOCTYPE html>
<html>
<html class="cp pad">
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<link rel="stylesheet" href="/bower_components/components-font-awesome/css/font-awesome.min.css">
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
<link rel="stylesheet" href="/bower_components/bootstrap/dist/css/bootstrap.min.css">
<script async data-bootload="/pad/inner.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<script src="/bower_components/ckeditor/ckeditor.js"></script>
<script src="/pad/wysiwygarea-plugin.js"></script>
<script async data-bootload="/pad/inner.js" data-main="/common/sframe-boot.js?ver=1.1" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<style>
html, body {
margin: 0px;

View File

@ -1,3 +1,791 @@
require(['/api/config'], function (ApiConfig) {
// see ckeditor_base.js getUrl()
window.CKEDITOR_GETURL = function (resource) {
if (resource.indexOf( '/' ) === 0) {
resource = window.CKEDITOR.basePath.replace(/\/bower_components\/.*/, '') + resource;
} else if (resource.indexOf(':/') === -1) {
resource = window.CKEDITOR.basePath + resource;
}
if (resource[resource.length - 1] !== '/' && resource.indexOf('ver=') === -1) {
var args = ApiConfig.requireConf.urlArgs;
if (resource.indexOf('/bower_components/') !== -1) {
args = 'ver=' + window.CKEDITOR.timestamp;
}
resource += (resource.indexOf('?') >= 0 ? '&' : '?') + args;
}
return resource;
};
require(['/bower_components/ckeditor/ckeditor.js']);
});
define([
'less!/customize/src/less/toolbar.less',
], function () {});
'jquery',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/hyperjson/hyperjson.js',
'/common/toolbar3.js',
'/common/cursor.js',
'/bower_components/chainpad-json-validator/json-ot.js',
'/common/TypingTests.js',
'json.sortify',
'/bower_components/textpatcher/TextPatcher.js',
'/common/cryptpad-common.js',
'/common/cryptget.js',
'/pad/links.js',
'/bower_components/nthen/index.js',
'/common/sframe-common.js',
'/api/config',
'/common/common-realtime.js',
'/bower_components/file-saver/FileSaver.min.js',
'/bower_components/diff-dom/diffDOM.js',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
'less!/customize/src/less/cryptpad.less',
'less!/customize/src/less/toolbar.less'
], function (
$,
Crypto,
Hyperjson,
Toolbar,
Cursor,
JsonOT,
TypingTest,
JSONSortify,
TextPatcher,
Cryptpad,
Cryptget,
Links,
nThen,
SFCommon,
ApiConfig,
CommonRealtime)
{
var saveAs = window.saveAs;
var Messages = Cryptpad.Messages;
var DiffDom = window.diffDOM;
var stringify = function (obj) { return JSONSortify(obj); };
window.Toolbar = Toolbar;
window.Hyperjson = Hyperjson;
var slice = function (coll) {
return Array.prototype.slice.call(coll);
};
var removeListeners = function (root) {
slice(root.attributes).map(function (attr) {
if (/^on/.test(attr.name)) {
root.attributes.removeNamedItem(attr.name);
}
});
slice(root.children).forEach(removeListeners);
};
var hjsonToDom = function (H) {
var dom = Hyperjson.toDOM(H);
removeListeners(dom);
return dom;
};
var module = window.REALTIME_MODULE = window.APP = {
Hyperjson: Hyperjson,
TextPatcher: TextPatcher,
logFights: true,
fights: [],
Cryptpad: Cryptpad,
Cursor: Cursor,
};
var emitResize = module.emitResize = function () {
var evt = window.document.createEvent('UIEvents');
evt.initUIEvent('resize', true, false, window, 0);
window.dispatchEvent(evt);
};
var toolbar;
var isNotMagicLine = function (el) {
return !(el && typeof(el.getAttribute) === 'function' &&
el.getAttribute('class') &&
el.getAttribute('class').split(' ').indexOf('non-realtime') !== -1);
};
/* catch `type="_moz"` before it goes over the wire */
var brFilter = function (hj) {
if (hj[1].type === '_moz') { hj[1].type = undefined; }
return hj;
};
var onConnectError = function () {
Cryptpad.errorLoadingScreen(Messages.websocketError);
};
var domFromHTML = function (html) {
return new DOMParser().parseFromString(html, 'text/html');
};
var forbiddenTags = [
'SCRIPT',
'IFRAME',
'OBJECT',
'APPLET',
'VIDEO',
'AUDIO'
];
var getHTML = function (inner) {
return ('<!DOCTYPE html>\n' + '<html>\n' + inner.innerHTML);
};
var CKEDITOR_CHECK_INTERVAL = 100;
var ckEditorAvailable = function (cb) {
var intr;
var check = function () {
if (window.CKEDITOR) {
clearTimeout(intr);
cb(window.CKEDITOR);
}
};
intr = setInterval(function () {
console.log("Ckeditor was not defined. Trying again in %sms", CKEDITOR_CHECK_INTERVAL);
check();
}, CKEDITOR_CHECK_INTERVAL);
check();
};
var mkDiffOptions = function (cursor, readOnly) {
return {
preDiffApply: function (info) {
/*
Don't accept attributes that begin with 'on'
these are probably listeners, and we don't want to
send scripts over the wire.
*/
if (['addAttribute', 'modifyAttribute'].indexOf(info.diff.action) !== -1) {
if (info.diff.name === 'href') {
// console.log(info.diff);
//var href = info.diff.newValue;
// TODO normalize HTML entities
if (/javascript *: */.test(info.diff.newValue)) {
// TODO remove javascript: links
}
}
if (/^on/.test(info.diff.name)) {
console.log("Rejecting forbidden element attribute with name (%s)", info.diff.name);
return true;
}
}
/*
Also reject any elements which would insert any one of
our forbidden tag types: script, iframe, object,
applet, video, or audio
*/
if (['addElement', 'replaceElement'].indexOf(info.diff.action) !== -1) {
if (info.diff.element && forbiddenTags.indexOf(info.diff.element.nodeName) !== -1) {
console.log("Rejecting forbidden tag of type (%s)", info.diff.element.nodeName);
return true;
} else if (info.diff.newValue && forbiddenTags.indexOf(info.diff.newValue.nodeType) !== -1) {
console.log("Rejecting forbidden tag of type (%s)", info.diff.newValue.nodeName);
return true;
}
}
if (info.node && info.node.tagName === 'BODY') {
if (info.diff.action === 'removeAttribute' &&
['class', 'spellcheck'].indexOf(info.diff.name) !== -1) {
return true;
}
}
/* DiffDOM will filter out magicline plugin elements
in practice this will make it impossible to use it
while someone else is typing, which could be annoying.
we should check when such an element is going to be
removed, and prevent that from happening. */
if (info.node && info.node.tagName === 'SPAN' &&
info.node.getAttribute('contentEditable') === "false") {
// it seems to be a magicline plugin element...
if (info.diff.action === 'removeElement') {
// and you're about to remove it...
// this probably isn't what you want
/*
I have never seen this in the console, but the
magic line is still getting removed on remote
edits. This suggests that it's getting removed
by something other than diffDom.
*/
console.log("preventing removal of the magic line!");
// return true to prevent diff application
return true;
}
}
// Do not change the contenteditable value in view mode
if (readOnly && info.node && info.node.tagName === 'BODY' &&
info.diff.action === 'modifyAttribute' && info.diff.name === 'contenteditable') {
return true;
}
// no use trying to recover the cursor if it doesn't exist
if (!cursor.exists()) { return; }
/* frame is either 0, 1, 2, or 3, depending on which
cursor frames were affected: none, first, last, or both
*/
var frame = info.frame = cursor.inNode(info.node);
if (!frame) { return; }
if (typeof info.diff.oldValue === 'string' && typeof info.diff.newValue === 'string') {
var pushes = cursor.pushDelta(info.diff.oldValue, info.diff.newValue);
if (frame & 1) {
// push cursor start if necessary
if (pushes.commonStart < cursor.Range.start.offset) {
cursor.Range.start.offset += pushes.delta;
}
}
if (frame & 2) {
// push cursor end if necessary
if (pushes.commonStart < cursor.Range.end.offset) {
cursor.Range.end.offset += pushes.delta;
}
}
}
},
postDiffApply: function (info) {
if (info.frame) {
if (info.node) {
if (info.frame & 1) { cursor.fixStart(info.node); }
if (info.frame & 2) { cursor.fixEnd(info.node); }
} else { console.error("info.node did not exist"); }
var sel = cursor.makeSelection();
var range = cursor.makeRange();
cursor.fixSelection(sel, range);
}
}
};
};
////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////
var andThen = function (editor, Ckeditor, common) {
//var $iframe = $('#pad-iframe').contents();
//var secret = Cryptpad.getSecrets();
//var readOnly = secret.keys && !secret.keys.editKeyStr;
//if (!secret.keys) {
// secret.keys = secret.key;
//}
var readOnly = false; // TODO
var cpNfInner;
var metadataMgr;
var onLocal;
var $bar = $('#cke_1_toolbox');
var $html = $bar.closest('html');
var $faLink = $html.find('head link[href*="/bower_components/components-font-awesome/css/font-awesome.min.css"]');
if ($faLink.length) {
$html.find('iframe').contents().find('head').append($faLink.clone());
}
var isHistoryMode = false;
if (readOnly) {
$('#cke_1_toolbox > .cke_toolbox_main').hide();
}
/* add a class to the magicline plugin so we can pick it out more easily */
var ml = Ckeditor.instances.editor1.plugins.magicline.backdoor.that.line.$;
[ml, ml.parentElement].forEach(function (el) {
el.setAttribute('class', 'non-realtime');
});
var ifrWindow = $html.find('iframe')[0].contentWindow;
var documentBody = ifrWindow.document.body;
var inner = window.inner = documentBody;
var cursor = module.cursor = Cursor(inner);
var openLink = function (e) {
var el = e.currentTarget;
if (!el || el.nodeName !== 'A') { return; }
var href = el.getAttribute('href');
var bounceHref = window.location.origin + '/bounce/#' + encodeURIComponent(href);
if (href) { ifrWindow.open(bounceHref, '_blank'); }
};
var setEditable = module.setEditable = function (bool) {
if (bool) {
$(inner).css({
color: '#333',
});
}
if (!readOnly || !bool) {
inner.setAttribute('contenteditable', bool);
}
};
CommonRealtime.onInfiniteSpinner(function () { setEditable(false); });
// don't let the user edit until the pad is ready
setEditable(false);
var initializing = true;
var Title;
//var UserList;
//var Metadata;
var getHeadingText = function () {
var text;
if (['h1', 'h2', 'h3'].some(function (t) {
var $header = $(inner).find(t + ':first-of-type');
if ($header.length && $header.text()) {
text = $header.text();
return true;
}
})) { return text; }
};
var DD = new DiffDom(mkDiffOptions(cursor, readOnly));
// apply patches, and try not to lose the cursor in the process!
var applyHjson = function (shjson) {
var userDocStateDom = hjsonToDom(JSON.parse(shjson));
if (!readOnly && !initializing) {
userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf
}
var patch = (DD).diff(inner, userDocStateDom);
(DD).apply(inner, patch);
if (readOnly) {
var $links = $(inner).find('a');
// off so that we don't end up with multiple identical handlers
$links.off('click', openLink).on('click', openLink);
}
};
var stringifyDOM = module.stringifyDOM = function (dom) {
var hjson = Hyperjson.fromDOM(dom, isNotMagicLine, brFilter);
hjson[3] = {
metadata: metadataMgr.getMetadataLazy()
};
/*hjson[3] = { TODO
users: UserList.userData,
defaultTitle: Title.defaultTitle,
type: 'pad'
}
};
if (!initializing) {
hjson[3].metadata.title = Title.title;
} else if (Cryptpad.initialName && !hjson[3].metadata.title) {
hjson[3].metadata.title = Cryptpad.initialName;
}*/
return stringify(hjson);
};
var realtimeOptions = {
readOnly: readOnly,
// really basic operational transform
transformFunction : JsonOT.validate,
// cryptpad debug logging (default is 1)
// logLevel: 0,
validateContent: function (content) {
try {
JSON.parse(content);
return true;
} catch (e) {
console.log("Failed to parse, rejecting patch");
return false;
}
}
};
var setHistory = function (bool, update) {
isHistoryMode = bool;
setEditable(!bool);
if (!bool && update) {
realtimeOptions.onRemote();
}
};
realtimeOptions.onRemote = function () {
if (initializing) { return; }
if (isHistoryMode) { return; }
var oldShjson = stringifyDOM(inner);
var shjson = module.realtime.getUserDoc();
// remember where the cursor is
cursor.update();
// Update the user list (metadata) from the hyperjson
// TODO Metadata.update(shjson);
var newInner = JSON.parse(shjson);
var newSInner;
if (newInner.length > 2) {
newSInner = stringify(newInner[2]);
}
if (newInner[3]) {
metadataMgr.updateMetadata(newInner[3].metadata);
}
// build a dom from HJSON, diff, and patch the editor
applyHjson(shjson);
if (!readOnly) {
var shjson2 = stringifyDOM(inner);
// TODO
//shjson = JSON.stringify(JSON.parse(shjson).slice(0,3));
if (shjson2 !== shjson) {
console.error("shjson2 !== shjson");
module.patchText(shjson2);
/* pushing back over the wire is necessary, but it can
result in a feedback loop, which we call a browser
fight */
if (module.logFights) {
// what changed?
var op = TextPatcher.diff(shjson, shjson2);
// log the changes
TextPatcher.log(shjson, op);
var sop = JSON.stringify(TextPatcher.format(shjson, op));
var index = module.fights.indexOf(sop);
if (index === -1) {
module.fights.push(sop);
console.log("Found a new type of browser disagreement");
console.log("You can inspect the list in your " +
"console at `REALTIME_MODULE.fights`");
console.log(module.fights);
} else {
console.log("Encountered a known browser disagreement: " +
"available at `REALTIME_MODULE.fights[%s]`", index);
}
}
}
}
// Notify only when the content has changed, not when someone has joined/left
var oldSInner = stringify(JSON.parse(oldShjson)[2]);
if (newSInner && newSInner !== oldSInner) {
Cryptpad.notify();
}
};
var exportFile = function () {
var html = getHTML(inner);
var suggestion = Title.suggestTitle('cryptpad-document');
Cryptpad.prompt(Messages.exportPrompt,
Cryptpad.fixFileName(suggestion) + '.html', function (filename) {
if (!(typeof(filename) === 'string' && filename)) { return; }
var blob = new Blob([html], {type: "text/html;charset=utf-8"});
saveAs(blob, filename);
});
};
var importFile = function (content) {
var shjson = stringify(Hyperjson.fromDOM(domFromHTML(content).body));
applyHjson(shjson);
realtimeOptions.onLocal();
};
realtimeOptions.onInit = function (info) {
readOnly = metadataMgr.getPrivateData().readOnly;
console.log('onInit');
var titleCfg = { getHeadingText: getHeadingText };
Title = common.createTitle(titleCfg, realtimeOptions.onLocal, common, metadataMgr);
var configTb = {
displayed: ['userlist', 'title', 'useradmin', 'spinner', 'newpad', 'share', 'limit'],
title: Title.getTitleConfig(),
metadataMgr: metadataMgr,
readOnly: readOnly,
ifrw: window,
realtime: info.realtime,
common: Cryptpad,
sfCommon: common,
$container: $bar,
$contentContainer: $('#cke_1_contents'),
};
toolbar = info.realtime.toolbar = Toolbar.create(configTb);
Title.setToolbar(toolbar);
var $rightside = toolbar.$rightside;
var $drawer = toolbar.$drawer;
var src = 'less!/customize/src/less/toolbar.less';
require([
src
], function () {
var $html = $bar.closest('html');
$html
.find('head style[data-original-src="' + src.replace(/less!/, '') + '"]')
.appendTo($html.find('head'));
});
$bar.find('#cke_1_toolbar_collapser').hide();
if (!readOnly) {
// Expand / collapse the toolbar
var $collapse = Cryptpad.createButton(null, true);
$collapse.removeClass('fa-question');
var updateIcon = function () {
$collapse.removeClass('fa-caret-down').removeClass('fa-caret-up');
var isCollapsed = !$bar.find('.cke_toolbox_main').is(':visible');
if (isCollapsed) {
if (!initializing) { common.feedback('HIDETOOLBAR_PAD'); }
$collapse.addClass('fa-caret-down');
}
else {
if (!initializing) { common.feedback('SHOWTOOLBAR_PAD'); }
$collapse.addClass('fa-caret-up');
}
};
updateIcon();
$collapse.click(function () {
$(window).trigger('resize');
$('.cke_toolbox_main').toggle();
$(window).trigger('cryptpad-ck-toolbar');
updateIcon();
});
$rightside.append($collapse);
} else {
$('.cke_toolbox_main').hide();
}
/* add a history button */
var histConfig = {
onLocal: realtimeOptions.onLocal,
onRemote: realtimeOptions.onRemote,
setHistory: setHistory,
applyVal: function (val) { applyHjson(val || '["BODY",{},[]]'); },
$toolbar: $bar
};
var $hist = common.createButton('history', true, {histConfig: histConfig});
$drawer.append($hist);
if (!metadataMgr.getPrivateData().isTemplate) {
var templateObj = {
rt: info.realtime,
getTitle: function () { return metadataMgr.getMetadata().title; }
};
var $templateButton = common.createButton('template', true, templateObj);
$rightside.append($templateButton);
}
/* add an export button */
var $export = Cryptpad.createButton('export', true, {}, exportFile);
$drawer.append($export);
if (!readOnly) {
/* add an import button */
var $import = Cryptpad.createButton('import', true, {
accept: 'text/html'
}, importFile);
$drawer.append($import);
}
/* add a forget button */
var forgetCb = function (err) {
if (err) { return; }
setEditable(false);
};
var $forgetPad = common.createButton('forget', true, {}, forgetCb);
$rightside.append($forgetPad);
};
// this should only ever get called once, when the chain syncs
realtimeOptions.onReady = function (info) {
console.log('onReady');
if (!module.isMaximized) {
module.isMaximized = true;
$('iframe.cke_wysiwyg_frame').css('width', '');
$('iframe.cke_wysiwyg_frame').css('height', '');
}
$('body').addClass('app-pad');
if (module.realtime !== info.realtime) {
module.patchText = TextPatcher.create({
realtime: info.realtime,
//logging: true,
});
}
module.realtime = info.realtime;
var shjson = module.realtime.getUserDoc();
var newPad = false;
if (shjson === '') { newPad = true; }
if (!newPad) {
applyHjson(shjson);
// Update the user list (metadata) from the hyperjson
// XXX Metadata.update(shjson);
var parsed = JSON.parse(shjson);
if (parsed[3] && parsed[3].metadata) {
metadataMgr.updateMetadata(parsed[3].metadata);
}
if (!readOnly) {
var shjson2 = stringifyDOM(inner);
var hjson2 = JSON.parse(shjson2).slice(0,3);
var hjson = JSON.parse(shjson).slice(0,3);
if (stringify(hjson2) !== stringify(hjson)) {
console.log('err');
console.error("shjson2 !== shjson");
console.log(stringify(hjson2));
console.log(stringify(hjson));
Cryptpad.errorLoadingScreen(Messages.wrongApp);
throw new Error();
}
}
} else {
Title.updateTitle(Cryptpad.initialName || Title.defaultTitle);
documentBody.innerHTML = Messages.initialState;
}
Cryptpad.removeLoadingScreen(emitResize);
setEditable(!readOnly);
initializing = false;
if (readOnly) { return; }
//TODO UserList.getLastName(toolbar.$userNameButton, newPad);
onLocal();
editor.focus();
if (newPad) {
cursor.setToEnd();
} else {
cursor.setToStart();
}
};
realtimeOptions.onConnectionChange = function (info) {
setEditable(info.state);
//toolbar.failed(); TODO
if (info.state) {
initializing = true;
//toolbar.reconnecting(info.myId); // TODO
Cryptpad.findOKButton().click();
} else {
Cryptpad.alert(Messages.common_connectionLost, undefined, true);
}
};
realtimeOptions.onError = onConnectError;
onLocal = realtimeOptions.onLocal = function () {
console.log('onlocal');
if (initializing) { return; }
if (isHistoryMode) { return; }
if (readOnly) { return; }
// stringify the json and send it into chainpad
var shjson = stringifyDOM(inner);
module.patchText(shjson);
if (module.realtime.getUserDoc() !== shjson) {
console.error("realtime.getUserDoc() !== shjson");
}
};
cpNfInner = common.startRealtime(realtimeOptions);
metadataMgr = cpNfInner.metadataMgr;
Cryptpad.onLogout(function () { setEditable(false); });
/* hitting enter makes a new line, but places the cursor inside
of the <br> instead of the <p>. This makes it such that you
cannot type until you click, which is rather unnacceptable.
If the cursor is ever inside such a <br>, you probably want
to push it out to the parent element, which ought to be a
paragraph tag. This needs to be done on keydown, otherwise
the first such keypress will not be inserted into the P. */
inner.addEventListener('keydown', cursor.brFix);
editor.on('change', onLocal);
// export the typing tests to the window.
// call like `test = easyTest()`
// terminate the test like `test.cancel()`
window.easyTest = function () {
cursor.update();
var start = cursor.Range.start;
var test = TypingTest.testInput(inner, start.el, start.offset, onLocal);
onLocal();
return test;
};
$bar.find('.cke_button').click(function () {
var e = this;
var classString = e.getAttribute('class');
var classes = classString.split(' ').filter(function (c) {
return /cke_button__/.test(c);
});
var id = classes[0];
if (typeof(id) === 'string') {
common.feedback(id.toUpperCase());
}
});
};
var main = function () {
var Ckeditor;
var editor;
var common;
nThen(function (waitFor) {
ckEditorAvailable(waitFor(function (ck) {
Ckeditor = ck;
require(['/pad/wysiwygarea-plugin.js'], waitFor());
}));
$(waitFor(function () {
Cryptpad.addLoadingScreen();
}));
SFCommon.create(waitFor(function (c) { module.common = common = c; }));
}).nThen(function (waitFor) {
Ckeditor.config.toolbarCanCollapse = true;
if (screen.height < 800) {
Ckeditor.config.toolbarStartupExpanded = false;
$('meta[name=viewport]').attr('content', 'width=device-width, initial-scale=1.0, user-scalable=no');
} else {
$('meta[name=viewport]').attr('content', 'width=device-width, initial-scale=1.0, user-scalable=yes');
}
// Used in ckeditor-config.js
Ckeditor.CRYPTPAD_URLARGS = ApiConfig.requireConf.urlArgs;
editor = Ckeditor.replace('editor1', {
customConfig: '/customize/ckeditor-config.js',
});
editor.on('instanceReady', waitFor());
}).nThen(function (/*waitFor*/) {
Links.addSupportForOpeningLinksInNewTab(Ckeditor);
Cryptpad.onError(function (info) {
if (info && info.type === "store") {
onConnectError();
}
});
andThen(editor, Ckeditor, common);
});
};
main();
});

View File

@ -1,768 +1,223 @@
// Load #1, load as little as possible because we are in a race to get the loading screen up.
define([
'/bower_components/nthen/index.js',
'/api/config',
'jquery',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/chainpad-netflux/chainpad-netflux.js',
'/bower_components/hyperjson/hyperjson.js',
'/common/toolbar2.js',
'/common/cursor.js',
'/bower_components/chainpad-json-validator/json-ot.js',
'/common/TypingTests.js',
'json.sortify',
'/bower_components/textpatcher/TextPatcher.js',
'/common/requireconfig.js'
], function (nThen, ApiConfig, $, RequireConfig) {
var requireConfig = RequireConfig();
// Loaded in load #2
var CpNfOuter;
var Cryptpad;
var Crypto;
var Cryptget;
var sframeChan;
var secret;
var hashes;
nThen(function (waitFor) {
$(waitFor());
}).nThen(function (waitFor) {
var req = {
cfg: requireConfig,
req: [ '/common/loading.js' ],
pfx: window.location.origin
};
window.rc = requireConfig;
window.apiconf = ApiConfig;
$('#sbox-iframe').attr('src',
ApiConfig.httpSafeOrigin + '/pad/inner.html?' + requireConfig.urlArgs +
'#' + encodeURIComponent(JSON.stringify(req)));
// This is a cheap trick to avoid loading sframe-channel in parallel with the
// loading screen setup.
var done = waitFor();
var onMsg = function (msg) {
var data = JSON.parse(msg.data);
if (data.q !== 'READY') { return; }
window.removeEventListener('message', onMsg);
var _done = done;
done = function () { };
_done();
};
window.addEventListener('message', onMsg);
}).nThen(function (waitFor) {
// Load #2, the loading screen is up so grab whatever you need...
require([
'/common/sframe-chainpad-netflux-outer.js',
'/common/cryptpad-common.js',
'/bower_components/chainpad-crypto/crypto.js',
'/common/cryptget.js',
'/pad/links.js',
'/bower_components/file-saver/FileSaver.min.js',
'/bower_components/diff-dom/diffDOM.js',
'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
'less!/customize/src/less/cryptpad.less',
], function ($, Crypto, realtimeInput, Hyperjson,
Toolbar, Cursor, JsonOT, TypingTest, JSONSortify, TextPatcher, Cryptpad, Cryptget, Links) {
var saveAs = window.saveAs;
var Messages = Cryptpad.Messages;
$(function () {
var ifrw = $('#pad-iframe')[0].contentWindow;
var Ckeditor; // to be initialized later...
var DiffDom = window.diffDOM;
Cryptpad.addLoadingScreen();
var stringify = function (obj) {
return JSONSortify(obj);
};
window.Toolbar = Toolbar;
window.Hyperjson = Hyperjson;
var slice = function (coll) {
return Array.prototype.slice.call(coll);
};
var removeListeners = function (root) {
slice(root.attributes).map(function (attr) {
if (/^on/i.test(attr.name)) {
root.attributes.removeNamedItem(attr.name);
'/common/sframe-channel.js',
], waitFor(function (_CpNfOuter, _Cryptpad, _Crypto, _Cryptget, SFrameChannel) {
CpNfOuter = _CpNfOuter;
Cryptpad = _Cryptpad;
Crypto = _Crypto;
Cryptget = _Cryptget;
SFrameChannel.create($('#sbox-iframe')[0].contentWindow, waitFor(function (sfc) {
sframeChan = sfc;
}));
Cryptpad.ready(waitFor());
}));
}).nThen(function (waitFor) {
secret = Cryptpad.getSecrets();
if (!secret.channel) {
// New pad: create a new random channel id
secret.channel = Cryptpad.createChannelId();
}
});
slice(root.children).forEach(removeListeners);
};
var hjsonToDom = function (H) {
var dom = Hyperjson.toDOM(H);
removeListeners(dom);
return dom;
};
var module = window.REALTIME_MODULE = window.APP = {
Hyperjson: Hyperjson,
TextPatcher: TextPatcher,
logFights: true,
fights: [],
Cryptpad: Cryptpad,
Cursor: Cursor,
};
var emitResize = module.emitResize = function () {
var cw = $('#pad-iframe')[0].contentWindow;
var evt = cw.document.createEvent('UIEvents');
evt.initUIEvent('resize', true, false, cw, 0);
cw.dispatchEvent(evt);
};
var toolbar;
var isNotMagicLine = function (el) {
return !(el && typeof(el.getAttribute) === 'function' &&
el.getAttribute('class') &&
el.getAttribute('class').split(' ').indexOf('non-realtime') !== -1);
};
/* catch `type="_moz"` before it goes over the wire */
var brFilter = function (hj) {
if (hj[1].type === '_moz') { hj[1].type = undefined; }
return hj;
};
var onConnectError = function () {
Cryptpad.errorLoadingScreen(Messages.websocketError);
};
var andThen = function (Ckeditor) {
var $iframe = $('#pad-iframe').contents();
var secret = Cryptpad.getSecrets();
Cryptpad.getShareHashes(secret, waitFor(function (err, h) { hashes = h; }));
}).nThen(function (/*waitFor*/) {
var readOnly = secret.keys && !secret.keys.editKeyStr;
if (!secret.keys) {
secret.keys = secret.key;
}
var editor = window.editor = Ckeditor.replace('editor1', {
customConfig: '/customize/ckeditor-config.js',
});
editor.on('instanceReady', Links.addSupportForOpeningLinksInNewTab(Ckeditor));
editor.on('instanceReady', function () {
var $bar = $('#pad-iframe')[0].contentWindow.$('#cke_1_toolbox');
var $html = $bar.closest('html');
var $faLink = $html.find('head link[href*="/bower_components/components-font-awesome/css/font-awesome.min.css"]');
if ($faLink.length) {
$html.find('iframe').contents().find('head').append($faLink.clone());
}
var isHistoryMode = false;
if (readOnly) {
$('#pad-iframe')[0].contentWindow.$('#cke_1_toolbox > .cke_toolbox_main').hide();
}
/* add a class to the magicline plugin so we can pick it out more easily */
var ml = $('iframe')[0].contentWindow.CKEDITOR.instances.editor1.plugins.magicline
.backdoor.that.line.$;
[ml, ml.parentElement].forEach(function (el) {
el.setAttribute('class', 'non-realtime');
});
var documentBody = ifrw.$('iframe')[0].contentDocument.body;
var inner = window.inner = documentBody;
// hide all content until the realtime doc is ready
$(inner).css({
color: '#fff',
});
var cursor = module.cursor = Cursor(inner);
var setEditable = module.setEditable = function (bool) {
if (bool) {
$(inner).css({
color: '#333',
});
}
if (!readOnly || !bool) {
inner.setAttribute('contenteditable', bool);
}
};
// don't let the user edit until the pad is ready
setEditable(false);
var forbiddenTags = [
'SCRIPT',
'IFRAME',
'OBJECT',
'APPLET',
'VIDEO',
'AUDIO'
];
var diffOptions = {
preDiffApply: function (info) {
/*
Don't accept attributes that begin with 'on'
these are probably listeners, and we don't want to
send scripts over the wire.
*/
if (['addAttribute', 'modifyAttribute'].indexOf(info.diff.action) !== -1) {
if (info.diff.name === 'href') {
// console.log(info.diff);
//var href = info.diff.newValue;
// TODO normalize HTML entities
if (/javascript *: */.test(info.diff.newValue)) {
// TODO remove javascript: links
}
}
if (/^on/.test(info.diff.name)) {
console.log("Rejecting forbidden element attribute with name (%s)", info.diff.name);
return true;
}
}
/*
Also reject any elements which would insert any one of
our forbidden tag types: script, iframe, object,
applet, video, or audio
*/
if (['addElement', 'replaceElement'].indexOf(info.diff.action) !== -1) {
if (info.diff.element && forbiddenTags.indexOf(info.diff.element.nodeName) !== -1) {
console.log("Rejecting forbidden tag of type (%s)", info.diff.element.nodeName);
return true;
} else if (info.diff.newValue && forbiddenTags.indexOf(info.diff.newValue.nodeType) !== -1) {
console.log("Rejecting forbidden tag of type (%s)", info.diff.newValue.nodeName);
return true;
}
}
if (info.node && info.node.tagName === 'BODY') {
if (info.diff.action === 'removeAttribute' &&
['class', 'spellcheck'].indexOf(info.diff.name) !== -1) {
return true;
}
}
/* DiffDOM will filter out magicline plugin elements
in practice this will make it impossible to use it
while someone else is typing, which could be annoying.
we should check when such an element is going to be
removed, and prevent that from happening. */
if (info.node && info.node.tagName === 'SPAN' &&
info.node.getAttribute('contentEditable') === "false") {
// it seems to be a magicline plugin element...
if (info.diff.action === 'removeElement') {
// and you're about to remove it...
// this probably isn't what you want
/*
I have never seen this in the console, but the
magic line is still getting removed on remote
edits. This suggests that it's getting removed
by something other than diffDom.
*/
console.log("preventing removal of the magic line!");
// return true to prevent diff application
return true;
}
}
// Do not change the contenteditable value in view mode
if (readOnly && info.node && info.node.tagName === 'BODY' &&
info.diff.action === 'modifyAttribute' && info.diff.name === 'contenteditable') {
return true;
}
// no use trying to recover the cursor if it doesn't exist
if (!cursor.exists()) { return; }
/* frame is either 0, 1, 2, or 3, depending on which
cursor frames were affected: none, first, last, or both
*/
var frame = info.frame = cursor.inNode(info.node);
if (!frame) { return; }
if (typeof info.diff.oldValue === 'string' && typeof info.diff.newValue === 'string') {
var pushes = cursor.pushDelta(info.diff.oldValue, info.diff.newValue);
if (frame & 1) {
// push cursor start if necessary
if (pushes.commonStart < cursor.Range.start.offset) {
cursor.Range.start.offset += pushes.delta;
}
}
if (frame & 2) {
// push cursor end if necessary
if (pushes.commonStart < cursor.Range.end.offset) {
cursor.Range.end.offset += pushes.delta;
}
}
}
if (!secret.keys) { secret.keys = secret.key; }
var parsed = Cryptpad.parsePadUrl(window.location.href);
if (!parsed.type) { throw new Error(); }
var defaultTitle = Cryptpad.getDefaultName(parsed);
var updateMeta = function () {
//console.log('EV_METADATA_UPDATE');
var name;
nThen(function (waitFor) {
Cryptpad.getLastName(waitFor(function (err, n) {
if (err) { console.log(err); }
name = n;
}));
}).nThen(function (/*waitFor*/) {
sframeChan.event('EV_METADATA_UPDATE', {
doc: {
defaultTitle: defaultTitle,
type: parsed.type
},
postDiffApply: function (info) {
if (info.frame) {
if (info.node) {
if (info.frame & 1) { cursor.fixStart(info.node); }
if (info.frame & 2) { cursor.fixEnd(info.node); }
} else { console.error("info.node did not exist"); }
var sel = cursor.makeSelection();
var range = cursor.makeRange();
cursor.fixSelection(sel, range);
user: {
name: name,
uid: Cryptpad.getUid(),
avatar: Cryptpad.getAvatarUrl(),
profile: Cryptpad.getProfileUrl(),
curvePublic: Cryptpad.getProxy().curvePublic,
netfluxId: Cryptpad.getNetwork().webChannels[0].myID,
},
priv: {
accountName: Cryptpad.getAccountName(),
origin: window.location.origin,
pathname: window.location.pathname,
readOnly: readOnly,
availableHashes: hashes,
isTemplate: Cryptpad.isTemplate(window.location.href),
feedbackAllowed: Cryptpad.isFeedbackAllowed()
}
});
});
};
Cryptpad.onDisplayNameChanged(updateMeta);
sframeChan.onReg('EV_METADATA_UPDATE', updateMeta);
Cryptpad.onError(function (info) {
console.log('error');
console.log(info);
if (info && info.type === "store") {
//onConnectError();
}
});
sframeChan.on('Q_ANON_RPC_MESSAGE', function (data, cb) {
Cryptpad.anonRpcMsg(data.msg, data.content, function (err, response) {
cb({error: err, response: response});
});
});
sframeChan.on('Q_SET_PAD_TITLE_IN_DRIVE', function (newTitle, cb) {
Cryptpad.renamePad(newTitle, undefined, function (err) {
if (err) { cb('ERROR'); } else { cb(); }
});
});
sframeChan.on('Q_SETTINGS_SET_DISPLAY_NAME', function (newName, cb) {
Cryptpad.setAttribute('username', newName, function (err) {
if (err) {
console.log("Couldn't set username");
console.error(err);
cb('ERROR');
return;
}
Cryptpad.changeDisplayName(newName, true);
cb();
});
});
sframeChan.on('Q_LOGOUT', function (data, cb) {
Cryptpad.logout(cb);
});
sframeChan.on('Q_SET_LOGIN_REDIRECT', function (data, cb) {
sessionStorage.redirectTo = window.location.href;
cb();
});
sframeChan.on('Q_GET_PIN_LIMIT_STATUS', function (data, cb) {
Cryptpad.isOverPinLimit(function (e, overLimit, limits) {
cb({
error: e,
overLimit: overLimit,
limits: limits
});
});
});
sframeChan.on('Q_MOVE_TO_TRASH', function (data, cb) {
Cryptpad.moveToTrash(cb);
});
sframeChan.on('Q_SAVE_AS_TEMPLATE', function (data, cb) {
Cryptpad.saveAsTemplate(Cryptget.put, data, cb);
});
sframeChan.on('Q_GET_FULL_HISTORY', function (data, cb) {
var network = Cryptpad.getNetwork();
var hkn = network.historyKeeper;
var crypto = Crypto.createEncryptor(secret.keys);
// Get the history messages and send them to the iframe
var parse = function (msg) {
try {
return JSON.parse(msg);
} catch (e) {
return null;
}
};
var initializing = true;
var Title;
var UserList;
var Metadata;
var getHeadingText = function () {
var text;
if (['h1', 'h2', 'h3'].some(function (t) {
var $header = $(inner).find(t + ':first-of-type');
if ($header.length && $header.text()) {
text = $header.text();
return true;
var onMsg = function (msg) {
var parsed = parse(msg);
if (parsed[0] === 'FULL_HISTORY_END') {
console.log('END');
cb();
return;
}
})) { return text; }
};
var DD = new DiffDom(diffOptions);
var openLink = function (e) {
var el = e.currentTarget;
if (!el || el.nodeName !== 'A') { return; }
var href = el.getAttribute('href');
if (href) { window.open(href, '_blank'); }
};
// apply patches, and try not to lose the cursor in the process!
var applyHjson = function (shjson) {
var userDocStateDom = hjsonToDom(JSON.parse(shjson));
if (!readOnly && !initializing) {
userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf
if (parsed[0] !== 'FULL_HISTORY') { return; }
if (parsed[1] && parsed[1].validateKey) { // First message
secret.keys.validateKey = parsed[1].validateKey;
return;
}
$(userDocStateDom).find('script, applet, object, iframe').remove();
$(userDocStateDom).find('a').filter(function (i, x) {
return ! /^(https|http|ftp):\/\/[^\s\n]*$/.test(x.getAttribute('href'));
}).remove();
var patch = (DD).diff(inner, userDocStateDom);
(DD).apply(inner, patch);
if (readOnly) {
var $links = $(inner).find('a');
// off so that we don't end up with multiple identical handlers
$links.off('click', openLink).on('click', openLink);
msg = parsed[1][4];
if (msg) {
msg = msg.replace(/^cp\|/, '');
var decryptedMsg = crypto.decrypt(msg, secret.keys.validateKey);
sframeChan.event('EV_RT_HIST_MESSAGE', decryptedMsg);
}
};
network.on('message', onMsg);
network.sendto(hkn, JSON.stringify(['GET_FULL_HISTORY', secret.channel, secret.keys.validateKey]));
});
var stringifyDOM = module.stringifyDOM = function (dom) {
var hjson = Hyperjson.fromDOM(dom, isNotMagicLine, brFilter);
hjson[3] = {
metadata: {
users: UserList.userData,
defaultTitle: Title.defaultTitle,
type: 'pad'
}
};
if (!initializing) {
hjson[3].metadata.title = Title.title;
} else if (Cryptpad.initialName && !hjson[3].metadata.title) {
hjson[3].metadata.title = Cryptpad.initialName;
}
return stringify(hjson);
};
var realtimeOptions = {
// the websocket URL
websocketURL: Cryptpad.getWebsocketURL(),
// the channel we will communicate over
CpNfOuter.start({
sframeChan: sframeChan,
channel: secret.channel,
// the nework used for the file store if it exists
network: Cryptpad.getNetwork(),
// our public key
validateKey: secret.keys.validateKey || undefined,
readOnly: readOnly,
// Pass in encrypt and decrypt methods
crypto: Crypto.createEncryptor(secret.keys),
// really basic operational transform
transformFunction : JsonOT.validate,
// cryptpad debug logging (default is 1)
// logLevel: 0,
validateContent: function (content) {
try {
JSON.parse(content);
return true;
} catch (e) {
console.log("Failed to parse, rejecting patch");
return false;
}
}
};
var setHistory = function (bool, update) {
isHistoryMode = bool;
setEditable(!bool);
if (!bool && update) {
realtimeOptions.onRemote();
}
};
realtimeOptions.onRemote = function () {
if (initializing) { return; }
if (isHistoryMode) { return; }
var oldShjson = stringifyDOM(inner);
var shjson = module.realtime.getUserDoc();
// remember where the cursor is
cursor.update();
// Update the user list (metadata) from the hyperjson
Metadata.update(shjson);
var newInner = JSON.parse(shjson);
var newSInner;
if (newInner.length > 2) {
newSInner = stringify(newInner[2]);
}
// build a dom from HJSON, diff, and patch the editor
applyHjson(shjson);
if (!readOnly) {
var shjson2 = stringifyDOM(inner);
if (shjson2 !== shjson) {
console.error("shjson2 !== shjson");
module.patchText(shjson2);
/* pushing back over the wire is necessary, but it can
result in a feedback loop, which we call a browser
fight */
if (module.logFights) {
// what changed?
var op = TextPatcher.diff(shjson, shjson2);
// log the changes
TextPatcher.log(shjson, op);
var sop = JSON.stringify(TextPatcher.format(shjson, op));
var index = module.fights.indexOf(sop);
if (index === -1) {
module.fights.push(sop);
console.log("Found a new type of browser disagreement");
console.log("You can inspect the list in your " +
"console at `REALTIME_MODULE.fights`");
console.log(module.fights);
} else {
console.log("Encountered a known browser disagreement: " +
"available at `REALTIME_MODULE.fights[%s]`", index);
}
}
}
}
// Notify only when the content has changed, not when someone has joined/left
var oldSInner = stringify(JSON.parse(oldShjson)[2]);
if (newSInner && newSInner !== oldSInner) {
Cryptpad.notify();
}
};
var getHTML = function () {
return ('<!DOCTYPE html>\n' + '<html>\n' + inner.innerHTML);
};
var domFromHTML = function (html) {
return new DOMParser().parseFromString(html, 'text/html');
};
var exportFile = function () {
var html = getHTML();
var suggestion = Title.suggestTitle('cryptpad-document');
Cryptpad.prompt(Messages.exportPrompt,
Cryptpad.fixFileName(suggestion) + '.html', function (filename) {
if (!(typeof(filename) === 'string' && filename)) { return; }
var blob = new Blob([html], {type: "text/html;charset=utf-8"});
saveAs(blob, filename);
});
};
var importFile = function (content) {
var shjson = stringify(Hyperjson.fromDOM(domFromHTML(content).body));
applyHjson(shjson);
realtimeOptions.onLocal();
};
realtimeOptions.onInit = function (info) {
UserList = Cryptpad.createUserList(info, realtimeOptions.onLocal, Cryptget, Cryptpad);
var titleCfg = { getHeadingText: getHeadingText };
Title = Cryptpad.createTitle(titleCfg, realtimeOptions.onLocal, Cryptpad);
Metadata = Cryptpad.createMetadata(UserList, Title, null, Cryptpad);
var configTb = {
displayed: ['title', 'useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit', 'upgrade'],
userList: UserList.getToolbarConfig(),
share: {
secret: secret,
channel: info.channel
},
title: Title.getTitleConfig(),
common: Cryptpad,
readOnly: readOnly,
ifrw: ifrw,
realtime: info.realtime,
network: info.network,
$container: $bar,
$contentContainer: $iframe.find('#cke_1_contents'),
};
toolbar = info.realtime.toolbar = Toolbar.create(configTb);
var src = 'less!/customize/src/less/toolbar.less';
require([
src
], function () {
var $html = $bar.closest('html');
$html
.find('head style[data-original-src="' + src.replace(/less!/, '') + '"]')
.appendTo($html.find('head'));
});
Title.setToolbar(toolbar);
var $rightside = toolbar.$rightside;
var $drawer = toolbar.$drawer;
var editHash;
if (!readOnly) {
editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys);
}
$bar.find('#cke_1_toolbar_collapser').hide();
if (!readOnly) {
// Expand / collapse the toolbar
var $collapse = Cryptpad.createButton(null, true);
$collapse.removeClass('fa-question');
var updateIcon = function () {
$collapse.removeClass('fa-caret-down').removeClass('fa-caret-up');
$collapse.attr('title', '');
var isCollapsed = !$bar.find('.cke_toolbox_main').is(':visible');
if (isCollapsed) {
if (!initializing) { Cryptpad.feedback('HIDETOOLBAR_PAD'); }
$collapse.addClass('fa-caret-down');
$collapse.attr('title', Messages.pad_showToolbar);
}
else {
if (!initializing) { Cryptpad.feedback('SHOWTOOLBAR_PAD'); }
$collapse.addClass('fa-caret-up');
$collapse.attr('title', Messages.pad_hideToolbar);
}
};
updateIcon();
$collapse.click(function () {
$(window).trigger('resize');
$iframe.find('.cke_toolbox_main').toggle();
$(window).trigger('cryptpad-ck-toolbar');
updateIcon();
});
$rightside.append($collapse);
}
/* add a history button */
var histConfig = {
onLocal: realtimeOptions.onLocal,
onRemote: realtimeOptions.onRemote,
setHistory: setHistory,
applyVal: function (val) { applyHjson(val || '["BODY",{},[]]'); },
$toolbar: $bar
};
var $hist = Cryptpad.createButton('history', true, {histConfig: histConfig});
$drawer.append($hist);
/* save as template */
if (!Cryptpad.isTemplate(window.location.href)) {
var templateObj = {
rt: info.realtime,
Crypt: Cryptget,
getTitle: function () { return document.title; }
};
var $templateButton = Cryptpad.createButton('template', true, templateObj);
$rightside.append($templateButton);
}
/* add an export button */
var $export = Cryptpad.createButton('export', true, {}, exportFile);
$drawer.append($export);
if (!readOnly) {
/* add an import button */
var $import = Cryptpad.createButton('import', true, {
accept: 'text/html'
}, importFile);
$drawer.append($import);
}
/* add a forget button */
var forgetCb = function (err) {
if (err) { return; }
setEditable(false);
};
var $forgetPad = Cryptpad.createButton('forget', true, {}, forgetCb);
$rightside.append($forgetPad);
// set the hash
if (!readOnly) { Cryptpad.replaceHash(editHash); }
};
// this should only ever get called once, when the chain syncs
realtimeOptions.onReady = function (info) {
if (!module.isMaximized) {
module.isMaximized = true;
$iframe.find('iframe.cke_wysiwyg_frame').css('width', '');
$iframe.find('iframe.cke_wysiwyg_frame').css('height', '');
}
$iframe.find('body').addClass('app-pad');
if (module.realtime !== info.realtime) {
module.patchText = TextPatcher.create({
realtime: info.realtime,
//logging: true,
});
}
module.realtime = info.realtime;
var shjson = module.realtime.getUserDoc();
var newPad = false;
if (shjson === '') { newPad = true; }
if (!newPad) {
applyHjson(shjson);
// Update the user list (metadata) from the hyperjson
Metadata.update(shjson);
if (!readOnly) {
var shjson2 = stringifyDOM(inner);
var hjson2 = JSON.parse(shjson2).slice(0,-1);
var hjson = JSON.parse(shjson).slice(0,-1);
if (stringify(hjson2) !== stringify(hjson)) {
console.log('err');
console.error("shjson2 !== shjson");
// TODO(cjd): This is removed because the XSS filter in applyHjson()
// is applied on incoming content so it causes this to fail.
//Cryptpad.errorLoadingScreen(Messages.wrongApp);
//throw new Error();
}
}
} else {
Title.updateTitle(Cryptpad.initialName || Title.defaultTitle);
documentBody.innerHTML = Messages.initialState;
}
Cryptpad.removeLoadingScreen(emitResize);
setEditable(!readOnly);
initializing = false;
onConnect: function (wc) {
if (readOnly) { return; }
UserList.getLastName(toolbar.$userNameButton, newPad);
editor.focus();
if (newPad) {
cursor.setToEnd();
} else {
cursor.setToStart();
}
};
realtimeOptions.onAbort = function () {
console.log("Aborting the session!");
// stop the user from continuing to edit
setEditable(false);
toolbar.failed();
Cryptpad.alert(Messages.common_connectionLost, undefined, true);
};
realtimeOptions.onConnectionChange = function (info) {
setEditable(info.state);
toolbar.failed();
if (info.state) {
initializing = true;
toolbar.reconnecting(info.myId);
Cryptpad.findOKButton().click();
} else {
Cryptpad.alert(Messages.common_connectionLost, undefined, true);
}
};
realtimeOptions.onError = onConnectError;
var onLocal = realtimeOptions.onLocal = function () {
if (initializing) { return; }
if (isHistoryMode) { return; }
if (readOnly) { return; }
// stringify the json and send it into chainpad
var shjson = stringifyDOM(inner);
module.patchText(shjson);
if (module.realtime.getUserDoc() !== shjson) {
console.error("realtime.getUserDoc() !== shjson");
}
};
module.realtimeInput = realtimeInput.start(realtimeOptions);
Cryptpad.onLogout(function () { setEditable(false); });
/* hitting enter makes a new line, but places the cursor inside
of the <br> instead of the <p>. This makes it such that you
cannot type until you click, which is rather unnacceptable.
If the cursor is ever inside such a <br>, you probably want
to push it out to the parent element, which ought to be a
paragraph tag. This needs to be done on keydown, otherwise
the first such keypress will not be inserted into the P. */
inner.addEventListener('keydown', cursor.brFix);
editor.on('change', onLocal);
// export the typing tests to the window.
// call like `test = easyTest()`
// terminate the test like `test.cancel()`
window.easyTest = function () {
cursor.update();
var start = cursor.Range.start;
var test = TypingTest.testInput(inner, start.el, start.offset, onLocal);
onLocal();
return test;
};
$bar.find('.cke_button').click(function () {
var e = this;
var classString = e.getAttribute('class');
var classes = classString.split(' ').filter(function (c) {
return /cke_button__/.test(c);
});
var id = classes[0];
if (typeof(id) === 'string') {
Cryptpad.feedback(id.toUpperCase());
Cryptpad.replaceHash(Cryptpad.getEditHashFromKeys(wc.id, secret.keys));
}
});
});
};
var interval = 100;
var second = function (Ckeditor) {
Cryptpad.ready(function () {
andThen(Ckeditor);
Cryptpad.reportAppUsage();
});
Cryptpad.onError(function (info) {
if (info && info.type === "store") {
onConnectError();
}
});
};
var first = function () {
Ckeditor = ifrw.CKEDITOR;
if (Ckeditor) {
// mobile configuration
Ckeditor.config.toolbarCanCollapse = true;
if (screen.height < 800) {
Ckeditor.config.toolbarStartupExpanded = false;
$('meta[name=viewport]').attr('content', 'width=device-width, initial-scale=1.0, user-scalable=no');
} else {
$('meta[name=viewport]').attr('content', 'width=device-width, initial-scale=1.0, user-scalable=yes');
}
second(Ckeditor);
} else {
console.log("Ckeditor was not defined. Trying again in %sms",interval);
setTimeout(first, interval);
}
};
$(first);
});
});

View File

@ -8,7 +8,7 @@
* mode, which handles the main editing area space.
*/
( function() {
define(['/api/config'], function (ApiConfig) {
var framedWysiwyg;
var iframe;
@ -39,7 +39,7 @@
}
// CryptPad
src = '/pad/ckeditor-inner.html';
src = '/pad/ckeditor-inner.html?' + ApiConfig.requireConf.urlArgs;
iframe = CKEDITOR.dom.element.createFromHtml( '<iframe src="' + src + '" frameBorder="0"></iframe>' );
iframe.setStyles( { width: '100%', height: '100%' } );
@ -56,6 +56,11 @@
// Asynchronous iframe loading is only required in IE>8 and Gecko (other reasons probably).
// Do not use it on WebKit as it'll break the browser-back navigation.
var useOnloadEvent = ( CKEDITOR.env.ie && !CKEDITOR.env.edge ) || CKEDITOR.env.gecko;
// CryptPad
// This breaks Edge so lets use async all of the time
useOnloadEvent = true;
if ( useOnloadEvent )
iframe.on( 'load', onLoad );
@ -641,7 +646,7 @@
return css.join( '\n' );
}
} )();
});
/**
* Disables the ability to resize objects (images and tables) in the editing area.

View File

@ -1,3 +0,0 @@
<!DOCTYPE html>
<html dir="ltr" lang="en"><head><title>Rich Text Editor, editor1</title><style data-cke-temp="1">html{cursor:text;*cursor:auto}
img,input,textarea{cursor:default}</style><link type="text/css" rel="stylesheet" href="/customize/ckeditor-contents.css"><link type="text/css" rel="stylesheet" href="/bower_components/ckeditor/plugins/tableselection/styles/tableselection.css"></head><body><p><br></p></body></html>

View File

@ -1,30 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>CryptPad</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer" />
<script async data-bootload="/pad2/outer.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<style>
html, body {
margin: 0px;
padding: 0px;
}
#sbox-iframe {
position:fixed;
top:0px;
left:0px;
bottom:0px;
right:0px;
width:100%;
height:100%;
border:none;
margin:0;
padding:0;
overflow:hidden;
}
</style>
</head>
<body>
<iframe id="sbox-iframe">

View File

@ -1,37 +0,0 @@
<!DOCTYPE html>
<html class="cp pad">
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<script async data-bootload="/pad2/main.js" data-main="/common/sframe-boot.js?ver=1.1" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<style>
html, body {
margin: 0px;
}
#cke_1_top {
overflow: visible;
padding: 0px;
display: flex;
}
#cke_1_toolbox {
display: inline-block;
width: 100%;
background-color: #c1e7ff;
}
#cke_1_toolbox .cke_toolbar {
height: 28px;
padding: 2px 0;
}
#cke_1_top .cryptpad-toolbar {
padding: 0;
display: block;
}
.cke_wysiwyg_frame {
min-width: 60%;
}
</style>
</head>
<body class="app-pad">
<textarea style="display:none" id="editor1" name="editor1"></textarea>
</body>
</html>

View File

@ -1,60 +0,0 @@
define(['/common/cryptpad-common.js'], function (Cryptpad) {
// Adds a context menu entry to open the selected link in a new tab.
// See https://github.com/xwiki-contrib/application-ckeditor/commit/755d193497bf23ed874d874b4ae92fbee887fc10
var Messages = Cryptpad.Messages;
return {
addSupportForOpeningLinksInNewTab : function (Ckeditor) {
// Returns the DOM element of the active (currently focused) link. It has also support for linked image widgets.
// @return {CKEDITOR.dom.element}
var getActiveLink = function(editor) {
var anchor = Ckeditor.plugins.link.getSelectedLink(editor),
// We need to do some special checking against widgets availability.
activeWidget = editor.widgets && editor.widgets.focused;
// If default way of getting links didn't return anything useful..
if (!anchor && activeWidget && activeWidget.name === 'image' && activeWidget.parts.link) {
// Since CKEditor 4.4.0 image widgets may be linked.
anchor = activeWidget.parts.link;
}
return anchor;
};
return function(event) {
var editor = event.editor;
if (!Ckeditor.plugins.link) {
return;
}
editor.addCommand( 'openLink', {
exec: function(editor) {
var anchor = getActiveLink(editor);
if (anchor) {
var href = anchor.getAttribute('href');
if (href) {
window.open(href);
}
}
}
});
if (typeof editor.addMenuItem === 'function') {
editor.addMenuItem('openLink', {
label: Messages.openLinkInNewTab,
command: 'openLink',
group: 'link',
order: -1
});
}
if (editor.contextMenu) {
editor.contextMenu.addListener(function(startElement) {
if (startElement) {
var anchor = getActiveLink(editor);
if (anchor && anchor.getAttribute('href')) {
return {openLink: Ckeditor.TRISTATE_OFF};
}
}
});
editor.contextMenu._.panelDefinition.css.push('.cke_button__openLink_icon {' +
Ckeditor.skin.getIconStyle('link') + '}');
}
};
}
};
});

View File

@ -1,790 +0,0 @@
require(['/api/config'], function (ApiConfig) {
// see ckeditor_base.js getUrl()
window.CKEDITOR_GETURL = function (resource) {
if (resource.indexOf( '/' ) === 0) {
resource = window.CKEDITOR.basePath.replace(/\/bower_components\/.*/, '') + resource;
} else if (resource.indexOf(':/') === -1) {
resource = window.CKEDITOR.basePath + resource;
}
if (resource[resource.length - 1] !== '/' && resource.indexOf('ver=') === -1) {
var args = ApiConfig.requireConf.urlArgs;
if (resource.indexOf('/bower_components/') !== -1) {
args = 'ver=' + window.CKEDITOR.timestamp;
}
resource += (resource.indexOf('?') >= 0 ? '&' : '?') + args;
}
return resource;
};
require(['/bower_components/ckeditor/ckeditor.js']);
});
define([
'jquery',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/hyperjson/hyperjson.js',
'/common/toolbar3.js',
'/common/cursor.js',
'/bower_components/chainpad-json-validator/json-ot.js',
'/common/TypingTests.js',
'json.sortify',
'/bower_components/textpatcher/TextPatcher.js',
'/common/cryptpad-common.js',
'/common/cryptget.js',
'/pad/links.js',
'/bower_components/nthen/index.js',
'/common/sframe-common.js',
'/api/config',
'/common/common-realtime.js',
'/bower_components/file-saver/FileSaver.min.js',
'/bower_components/diff-dom/diffDOM.js',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
'less!/customize/src/less/cryptpad.less',
'less!/customize/src/less/toolbar.less'
], function (
$,
Crypto,
Hyperjson,
Toolbar,
Cursor,
JsonOT,
TypingTest,
JSONSortify,
TextPatcher,
Cryptpad,
Cryptget,
Links,
nThen,
SFCommon,
ApiConfig,
CommonRealtime)
{
var saveAs = window.saveAs;
var Messages = Cryptpad.Messages;
var DiffDom = window.diffDOM;
var stringify = function (obj) { return JSONSortify(obj); };
window.Toolbar = Toolbar;
window.Hyperjson = Hyperjson;
var slice = function (coll) {
return Array.prototype.slice.call(coll);
};
var removeListeners = function (root) {
slice(root.attributes).map(function (attr) {
if (/^on/.test(attr.name)) {
root.attributes.removeNamedItem(attr.name);
}
});
slice(root.children).forEach(removeListeners);
};
var hjsonToDom = function (H) {
var dom = Hyperjson.toDOM(H);
removeListeners(dom);
return dom;
};
var module = window.REALTIME_MODULE = window.APP = {
Hyperjson: Hyperjson,
TextPatcher: TextPatcher,
logFights: true,
fights: [],
Cryptpad: Cryptpad,
Cursor: Cursor,
};
var emitResize = module.emitResize = function () {
var evt = window.document.createEvent('UIEvents');
evt.initUIEvent('resize', true, false, window, 0);
window.dispatchEvent(evt);
};
var toolbar;
var isNotMagicLine = function (el) {
return !(el && typeof(el.getAttribute) === 'function' &&
el.getAttribute('class') &&
el.getAttribute('class').split(' ').indexOf('non-realtime') !== -1);
};
/* catch `type="_moz"` before it goes over the wire */
var brFilter = function (hj) {
if (hj[1].type === '_moz') { hj[1].type = undefined; }
return hj;
};
var onConnectError = function () {
Cryptpad.errorLoadingScreen(Messages.websocketError);
};
var domFromHTML = function (html) {
return new DOMParser().parseFromString(html, 'text/html');
};
var forbiddenTags = [
'SCRIPT',
'IFRAME',
'OBJECT',
'APPLET',
'VIDEO',
'AUDIO'
];
var getHTML = function (inner) {
return ('<!DOCTYPE html>\n' + '<html>\n' + inner.innerHTML);
};
var CKEDITOR_CHECK_INTERVAL = 100;
var ckEditorAvailable = function (cb) {
var intr;
var check = function () {
if (window.CKEDITOR) {
clearTimeout(intr);
cb(window.CKEDITOR);
}
};
intr = setInterval(function () {
console.log("Ckeditor was not defined. Trying again in %sms", CKEDITOR_CHECK_INTERVAL);
check();
}, CKEDITOR_CHECK_INTERVAL);
check();
};
var mkDiffOptions = function (cursor, readOnly) {
return {
preDiffApply: function (info) {
/*
Don't accept attributes that begin with 'on'
these are probably listeners, and we don't want to
send scripts over the wire.
*/
if (['addAttribute', 'modifyAttribute'].indexOf(info.diff.action) !== -1) {
if (info.diff.name === 'href') {
// console.log(info.diff);
//var href = info.diff.newValue;
// TODO normalize HTML entities
if (/javascript *: */.test(info.diff.newValue)) {
// TODO remove javascript: links
}
}
if (/^on/.test(info.diff.name)) {
console.log("Rejecting forbidden element attribute with name (%s)", info.diff.name);
return true;
}
}
/*
Also reject any elements which would insert any one of
our forbidden tag types: script, iframe, object,
applet, video, or audio
*/
if (['addElement', 'replaceElement'].indexOf(info.diff.action) !== -1) {
if (info.diff.element && forbiddenTags.indexOf(info.diff.element.nodeName) !== -1) {
console.log("Rejecting forbidden tag of type (%s)", info.diff.element.nodeName);
return true;
} else if (info.diff.newValue && forbiddenTags.indexOf(info.diff.newValue.nodeType) !== -1) {
console.log("Rejecting forbidden tag of type (%s)", info.diff.newValue.nodeName);
return true;
}
}
if (info.node && info.node.tagName === 'BODY') {
if (info.diff.action === 'removeAttribute' &&
['class', 'spellcheck'].indexOf(info.diff.name) !== -1) {
return true;
}
}
/* DiffDOM will filter out magicline plugin elements
in practice this will make it impossible to use it
while someone else is typing, which could be annoying.
we should check when such an element is going to be
removed, and prevent that from happening. */
if (info.node && info.node.tagName === 'SPAN' &&
info.node.getAttribute('contentEditable') === "false") {
// it seems to be a magicline plugin element...
if (info.diff.action === 'removeElement') {
// and you're about to remove it...
// this probably isn't what you want
/*
I have never seen this in the console, but the
magic line is still getting removed on remote
edits. This suggests that it's getting removed
by something other than diffDom.
*/
console.log("preventing removal of the magic line!");
// return true to prevent diff application
return true;
}
}
// Do not change the contenteditable value in view mode
if (readOnly && info.node && info.node.tagName === 'BODY' &&
info.diff.action === 'modifyAttribute' && info.diff.name === 'contenteditable') {
return true;
}
// no use trying to recover the cursor if it doesn't exist
if (!cursor.exists()) { return; }
/* frame is either 0, 1, 2, or 3, depending on which
cursor frames were affected: none, first, last, or both
*/
var frame = info.frame = cursor.inNode(info.node);
if (!frame) { return; }
if (typeof info.diff.oldValue === 'string' && typeof info.diff.newValue === 'string') {
var pushes = cursor.pushDelta(info.diff.oldValue, info.diff.newValue);
if (frame & 1) {
// push cursor start if necessary
if (pushes.commonStart < cursor.Range.start.offset) {
cursor.Range.start.offset += pushes.delta;
}
}
if (frame & 2) {
// push cursor end if necessary
if (pushes.commonStart < cursor.Range.end.offset) {
cursor.Range.end.offset += pushes.delta;
}
}
}
},
postDiffApply: function (info) {
if (info.frame) {
if (info.node) {
if (info.frame & 1) { cursor.fixStart(info.node); }
if (info.frame & 2) { cursor.fixEnd(info.node); }
} else { console.error("info.node did not exist"); }
var sel = cursor.makeSelection();
var range = cursor.makeRange();
cursor.fixSelection(sel, range);
}
}
};
};
////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////
var andThen = function (editor, Ckeditor, common) {
//var $iframe = $('#pad-iframe').contents();
//var secret = Cryptpad.getSecrets();
//var readOnly = secret.keys && !secret.keys.editKeyStr;
//if (!secret.keys) {
// secret.keys = secret.key;
//}
var readOnly = false; // TODO
var cpNfInner;
var metadataMgr;
var onLocal;
var $bar = $('#cke_1_toolbox');
var $html = $bar.closest('html');
var $faLink = $html.find('head link[href*="/bower_components/components-font-awesome/css/font-awesome.min.css"]');
if ($faLink.length) {
$html.find('iframe').contents().find('head').append($faLink.clone());
}
var isHistoryMode = false;
if (readOnly) {
$('#cke_1_toolbox > .cke_toolbox_main').hide();
}
/* add a class to the magicline plugin so we can pick it out more easily */
var ml = Ckeditor.instances.editor1.plugins.magicline.backdoor.that.line.$;
[ml, ml.parentElement].forEach(function (el) {
el.setAttribute('class', 'non-realtime');
});
var ifrWindow = $html.find('iframe')[0].contentWindow;
var documentBody = ifrWindow.document.body;
var inner = window.inner = documentBody;
var cursor = module.cursor = Cursor(inner);
var openLink = function (e) {
var el = e.currentTarget;
if (!el || el.nodeName !== 'A') { return; }
var href = el.getAttribute('href');
if (href) { ifrWindow.open(href, '_blank'); }
};
var setEditable = module.setEditable = function (bool) {
if (bool) {
$(inner).css({
color: '#333',
});
}
if (!readOnly || !bool) {
inner.setAttribute('contenteditable', bool);
}
};
CommonRealtime.onInfiniteSpinner(function () { setEditable(false); });
// don't let the user edit until the pad is ready
setEditable(false);
var initializing = true;
var Title;
//var UserList;
//var Metadata;
var getHeadingText = function () {
var text;
if (['h1', 'h2', 'h3'].some(function (t) {
var $header = $(inner).find(t + ':first-of-type');
if ($header.length && $header.text()) {
text = $header.text();
return true;
}
})) { return text; }
};
var DD = new DiffDom(mkDiffOptions(cursor, readOnly));
// apply patches, and try not to lose the cursor in the process!
var applyHjson = function (shjson) {
var userDocStateDom = hjsonToDom(JSON.parse(shjson));
if (!readOnly && !initializing) {
userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf
}
var patch = (DD).diff(inner, userDocStateDom);
(DD).apply(inner, patch);
if (readOnly) {
var $links = $(inner).find('a');
// off so that we don't end up with multiple identical handlers
$links.off('click', openLink).on('click', openLink);
}
};
var stringifyDOM = module.stringifyDOM = function (dom) {
var hjson = Hyperjson.fromDOM(dom, isNotMagicLine, brFilter);
hjson[3] = {
metadata: metadataMgr.getMetadataLazy()
};
/*hjson[3] = { TODO
users: UserList.userData,
defaultTitle: Title.defaultTitle,
type: 'pad'
}
};
if (!initializing) {
hjson[3].metadata.title = Title.title;
} else if (Cryptpad.initialName && !hjson[3].metadata.title) {
hjson[3].metadata.title = Cryptpad.initialName;
}*/
return stringify(hjson);
};
var realtimeOptions = {
readOnly: readOnly,
// really basic operational transform
transformFunction : JsonOT.validate,
// cryptpad debug logging (default is 1)
// logLevel: 0,
validateContent: function (content) {
try {
JSON.parse(content);
return true;
} catch (e) {
console.log("Failed to parse, rejecting patch");
return false;
}
}
};
var setHistory = function (bool, update) {
isHistoryMode = bool;
setEditable(!bool);
if (!bool && update) {
realtimeOptions.onRemote();
}
};
realtimeOptions.onRemote = function () {
if (initializing) { return; }
if (isHistoryMode) { return; }
var oldShjson = stringifyDOM(inner);
var shjson = module.realtime.getUserDoc();
// remember where the cursor is
cursor.update();
// Update the user list (metadata) from the hyperjson
// TODO Metadata.update(shjson);
var newInner = JSON.parse(shjson);
var newSInner;
if (newInner.length > 2) {
newSInner = stringify(newInner[2]);
}
if (newInner[3]) {
metadataMgr.updateMetadata(newInner[3].metadata);
}
// build a dom from HJSON, diff, and patch the editor
applyHjson(shjson);
if (!readOnly) {
var shjson2 = stringifyDOM(inner);
// TODO
//shjson = JSON.stringify(JSON.parse(shjson).slice(0,3));
if (shjson2 !== shjson) {
console.error("shjson2 !== shjson");
module.patchText(shjson2);
/* pushing back over the wire is necessary, but it can
result in a feedback loop, which we call a browser
fight */
if (module.logFights) {
// what changed?
var op = TextPatcher.diff(shjson, shjson2);
// log the changes
TextPatcher.log(shjson, op);
var sop = JSON.stringify(TextPatcher.format(shjson, op));
var index = module.fights.indexOf(sop);
if (index === -1) {
module.fights.push(sop);
console.log("Found a new type of browser disagreement");
console.log("You can inspect the list in your " +
"console at `REALTIME_MODULE.fights`");
console.log(module.fights);
} else {
console.log("Encountered a known browser disagreement: " +
"available at `REALTIME_MODULE.fights[%s]`", index);
}
}
}
}
// Notify only when the content has changed, not when someone has joined/left
var oldSInner = stringify(JSON.parse(oldShjson)[2]);
if (newSInner && newSInner !== oldSInner) {
Cryptpad.notify();
}
};
var exportFile = function () {
var html = getHTML(inner);
var suggestion = Title.suggestTitle('cryptpad-document');
Cryptpad.prompt(Messages.exportPrompt,
Cryptpad.fixFileName(suggestion) + '.html', function (filename) {
if (!(typeof(filename) === 'string' && filename)) { return; }
var blob = new Blob([html], {type: "text/html;charset=utf-8"});
saveAs(blob, filename);
});
};
var importFile = function (content) {
var shjson = stringify(Hyperjson.fromDOM(domFromHTML(content).body));
applyHjson(shjson);
realtimeOptions.onLocal();
};
realtimeOptions.onInit = function (info) {
readOnly = metadataMgr.getPrivateData().readOnly;
console.log('onInit');
var titleCfg = { getHeadingText: getHeadingText };
Title = common.createTitle(titleCfg, realtimeOptions.onLocal, common, metadataMgr);
var configTb = {
displayed: ['userlist', 'title', 'useradmin', 'spinner', 'newpad', 'share', 'limit'],
title: Title.getTitleConfig(),
metadataMgr: metadataMgr,
readOnly: readOnly,
ifrw: window,
realtime: info.realtime,
common: Cryptpad,
sfCommon: common,
$container: $bar,
$contentContainer: $('#cke_1_contents'),
};
toolbar = info.realtime.toolbar = Toolbar.create(configTb);
Title.setToolbar(toolbar);
var $rightside = toolbar.$rightside;
var $drawer = toolbar.$drawer;
var src = 'less!/customize/src/less/toolbar.less';
require([
src
], function () {
var $html = $bar.closest('html');
$html
.find('head style[data-original-src="' + src.replace(/less!/, '') + '"]')
.appendTo($html.find('head'));
});
$bar.find('#cke_1_toolbar_collapser').hide();
if (!readOnly) {
// Expand / collapse the toolbar
var $collapse = Cryptpad.createButton(null, true);
$collapse.removeClass('fa-question');
var updateIcon = function () {
$collapse.removeClass('fa-caret-down').removeClass('fa-caret-up');
var isCollapsed = !$bar.find('.cke_toolbox_main').is(':visible');
if (isCollapsed) {
if (!initializing) { common.feedback('HIDETOOLBAR_PAD'); }
$collapse.addClass('fa-caret-down');
}
else {
if (!initializing) { common.feedback('SHOWTOOLBAR_PAD'); }
$collapse.addClass('fa-caret-up');
}
};
updateIcon();
$collapse.click(function () {
$(window).trigger('resize');
$('.cke_toolbox_main').toggle();
$(window).trigger('cryptpad-ck-toolbar');
updateIcon();
});
$rightside.append($collapse);
} else {
$('.cke_toolbox_main').hide();
}
/* add a history button */
var histConfig = {
onLocal: realtimeOptions.onLocal,
onRemote: realtimeOptions.onRemote,
setHistory: setHistory,
applyVal: function (val) { applyHjson(val || '["BODY",{},[]]'); },
$toolbar: $bar
};
var $hist = common.createButton('history', true, {histConfig: histConfig});
$drawer.append($hist);
if (!metadataMgr.getPrivateData().isTemplate) {
var templateObj = {
rt: info.realtime,
getTitle: function () { return metadataMgr.getMetadata().title; }
};
var $templateButton = common.createButton('template', true, templateObj);
$rightside.append($templateButton);
}
/* add an export button */
var $export = Cryptpad.createButton('export', true, {}, exportFile);
$drawer.append($export);
if (!readOnly) {
/* add an import button */
var $import = Cryptpad.createButton('import', true, {
accept: 'text/html'
}, importFile);
$drawer.append($import);
}
/* add a forget button */
var forgetCb = function (err) {
if (err) { return; }
setEditable(false);
};
var $forgetPad = common.createButton('forget', true, {}, forgetCb);
$rightside.append($forgetPad);
};
// this should only ever get called once, when the chain syncs
realtimeOptions.onReady = function (info) {
console.log('onReady');
if (!module.isMaximized) {
module.isMaximized = true;
$('iframe.cke_wysiwyg_frame').css('width', '');
$('iframe.cke_wysiwyg_frame').css('height', '');
}
$('body').addClass('app-pad');
if (module.realtime !== info.realtime) {
module.patchText = TextPatcher.create({
realtime: info.realtime,
//logging: true,
});
}
module.realtime = info.realtime;
var shjson = module.realtime.getUserDoc();
var newPad = false;
if (shjson === '') { newPad = true; }
if (!newPad) {
applyHjson(shjson);
// Update the user list (metadata) from the hyperjson
// XXX Metadata.update(shjson);
var parsed = JSON.parse(shjson);
if (parsed[3] && parsed[3].metadata) {
metadataMgr.updateMetadata(parsed[3].metadata);
}
if (!readOnly) {
var shjson2 = stringifyDOM(inner);
var hjson2 = JSON.parse(shjson2).slice(0,3);
var hjson = JSON.parse(shjson).slice(0,3);
if (stringify(hjson2) !== stringify(hjson)) {
console.log('err');
console.error("shjson2 !== shjson");
console.log(stringify(hjson2));
console.log(stringify(hjson));
Cryptpad.errorLoadingScreen(Messages.wrongApp);
throw new Error();
}
}
} else {
Title.updateTitle(Cryptpad.initialName || Title.defaultTitle);
documentBody.innerHTML = Messages.initialState;
}
Cryptpad.removeLoadingScreen(emitResize);
setEditable(!readOnly);
initializing = false;
if (readOnly) { return; }
//TODO UserList.getLastName(toolbar.$userNameButton, newPad);
onLocal();
editor.focus();
if (newPad) {
cursor.setToEnd();
} else {
cursor.setToStart();
}
};
realtimeOptions.onConnectionChange = function (info) {
setEditable(info.state);
//toolbar.failed(); TODO
if (info.state) {
initializing = true;
//toolbar.reconnecting(info.myId); // TODO
Cryptpad.findOKButton().click();
} else {
Cryptpad.alert(Messages.common_connectionLost, undefined, true);
}
};
realtimeOptions.onError = onConnectError;
onLocal = realtimeOptions.onLocal = function () {
console.log('onlocal');
if (initializing) { return; }
if (isHistoryMode) { return; }
if (readOnly) { return; }
// stringify the json and send it into chainpad
var shjson = stringifyDOM(inner);
module.patchText(shjson);
if (module.realtime.getUserDoc() !== shjson) {
console.error("realtime.getUserDoc() !== shjson");
}
};
cpNfInner = common.startRealtime(realtimeOptions);
metadataMgr = cpNfInner.metadataMgr;
Cryptpad.onLogout(function () { setEditable(false); });
/* hitting enter makes a new line, but places the cursor inside
of the <br> instead of the <p>. This makes it such that you
cannot type until you click, which is rather unnacceptable.
If the cursor is ever inside such a <br>, you probably want
to push it out to the parent element, which ought to be a
paragraph tag. This needs to be done on keydown, otherwise
the first such keypress will not be inserted into the P. */
inner.addEventListener('keydown', cursor.brFix);
editor.on('change', onLocal);
// export the typing tests to the window.
// call like `test = easyTest()`
// terminate the test like `test.cancel()`
window.easyTest = function () {
cursor.update();
var start = cursor.Range.start;
var test = TypingTest.testInput(inner, start.el, start.offset, onLocal);
onLocal();
return test;
};
$bar.find('.cke_button').click(function () {
var e = this;
var classString = e.getAttribute('class');
var classes = classString.split(' ').filter(function (c) {
return /cke_button__/.test(c);
});
var id = classes[0];
if (typeof(id) === 'string') {
common.feedback(id.toUpperCase());
}
});
};
var main = function () {
var Ckeditor;
var editor;
var common;
nThen(function (waitFor) {
ckEditorAvailable(waitFor(function (ck) {
Ckeditor = ck;
require(['/pad2/wysiwygarea-plugin.js'], waitFor());
}));
$(waitFor(function () {
Cryptpad.addLoadingScreen();
}));
SFCommon.create(waitFor(function (c) { module.common = common = c; }));
}).nThen(function (waitFor) {
Ckeditor.config.toolbarCanCollapse = true;
if (screen.height < 800) {
Ckeditor.config.toolbarStartupExpanded = false;
$('meta[name=viewport]').attr('content', 'width=device-width, initial-scale=1.0, user-scalable=no');
} else {
$('meta[name=viewport]').attr('content', 'width=device-width, initial-scale=1.0, user-scalable=yes');
}
// Used in ckeditor-config.js
Ckeditor.CRYPTPAD_URLARGS = ApiConfig.requireConf.urlArgs;
editor = Ckeditor.replace('editor1', {
customConfig: '/customize/ckeditor-config.js',
});
editor.on('instanceReady', waitFor());
}).nThen(function (/*waitFor*/) {
Links.addSupportForOpeningLinksInNewTab(Ckeditor);
Cryptpad.onError(function (info) {
if (info && info.type === "store") {
onConnectError();
}
});
andThen(editor, Ckeditor, common);
});
};
main();
});

View File

@ -1,224 +0,0 @@
// Load #1, load as little as possible because we are in a race to get the loading screen up.
define([
'/bower_components/nthen/index.js',
'/api/config',
'jquery',
'/common/requireconfig.js'
], function (nThen, ApiConfig, $, RequireConfig) {
var requireConfig = RequireConfig();
// Loaded in load #2
var CpNfOuter;
var Cryptpad;
var Crypto;
var Cryptget;
var sframeChan;
var secret;
var hashes;
nThen(function (waitFor) {
$(waitFor());
}).nThen(function (waitFor) {
var req = {
cfg: requireConfig,
req: [ '/common/loading.js' ],
pfx: window.location.origin
};
window.rc = requireConfig;
window.apiconf = ApiConfig;
$('#sbox-iframe').attr('src',
ApiConfig.httpSafeOrigin + '/pad2/inner.html?' + requireConfig.urlArgs +
'#' + encodeURIComponent(JSON.stringify(req)));
// This is a cheap trick to avoid loading sframe-channel in parallel with the
// loading screen setup.
var done = waitFor();
var onMsg = function (msg) {
var data = JSON.parse(msg.data);
if (data.q !== 'READY') { return; }
window.removeEventListener('message', onMsg);
var _done = done;
done = function () { };
_done();
};
window.addEventListener('message', onMsg);
}).nThen(function (waitFor) {
// Load #2, the loading screen is up so grab whatever you need...
require([
'/common/sframe-chainpad-netflux-outer.js',
'/common/cryptpad-common.js',
'/bower_components/chainpad-crypto/crypto.js',
'/common/cryptget.js',
'/common/sframe-channel.js',
], waitFor(function (_CpNfOuter, _Cryptpad, _Crypto, _Cryptget, SFrameChannel) {
CpNfOuter = _CpNfOuter;
Cryptpad = _Cryptpad;
Crypto = _Crypto;
Cryptget = _Cryptget;
SFrameChannel.create($('#sbox-iframe')[0].contentWindow, waitFor(function (sfc) {
sframeChan = sfc;
}));
Cryptpad.ready(waitFor());
}));
}).nThen(function (waitFor) {
secret = Cryptpad.getSecrets();
if (!secret.channel) {
// New pad: create a new random channel id
secret.channel = Cryptpad.createChannelId();
}
Cryptpad.getShareHashes(secret, waitFor(function (err, h) { hashes = h; }));
}).nThen(function (/*waitFor*/) {
var readOnly = secret.keys && !secret.keys.editKeyStr;
if (!secret.keys) { secret.keys = secret.key; }
var parsed = Cryptpad.parsePadUrl(window.location.href);
parsed.type = parsed.type.replace('pad2', 'pad');
if (!parsed.type) { throw new Error(); }
var defaultTitle = Cryptpad.getDefaultName(parsed);
var updateMeta = function () {
//console.log('EV_METADATA_UPDATE');
var name;
nThen(function (waitFor) {
Cryptpad.getLastName(waitFor(function (err, n) {
if (err) { console.log(err); }
name = n;
}));
}).nThen(function (/*waitFor*/) {
sframeChan.event('EV_METADATA_UPDATE', {
doc: {
defaultTitle: defaultTitle,
type: parsed.type
},
user: {
name: name,
uid: Cryptpad.getUid(),
avatar: Cryptpad.getAvatarUrl(),
profile: Cryptpad.getProfileUrl(),
curvePublic: Cryptpad.getProxy().curvePublic,
netfluxId: Cryptpad.getNetwork().webChannels[0].myID,
},
priv: {
accountName: Cryptpad.getAccountName(),
origin: window.location.origin,
pathname: window.location.pathname,
readOnly: readOnly,
availableHashes: hashes,
isTemplate: Cryptpad.isTemplate(window.location.href),
feedbackAllowed: Cryptpad.isFeedbackAllowed()
}
});
});
};
Cryptpad.onDisplayNameChanged(updateMeta);
sframeChan.onReg('EV_METADATA_UPDATE', updateMeta);
Cryptpad.onError(function (info) {
console.log('error');
console.log(info);
if (info && info.type === "store") {
//onConnectError();
}
});
sframeChan.on('Q_ANON_RPC_MESSAGE', function (data, cb) {
Cryptpad.anonRpcMsg(data.msg, data.content, function (err, response) {
cb({error: err, response: response});
});
});
sframeChan.on('Q_SET_PAD_TITLE_IN_DRIVE', function (newTitle, cb) {
Cryptpad.renamePad(newTitle, undefined, function (err) {
if (err) { cb('ERROR'); } else { cb(); }
});
});
sframeChan.on('Q_SETTINGS_SET_DISPLAY_NAME', function (newName, cb) {
Cryptpad.setAttribute('username', newName, function (err) {
if (err) {
console.log("Couldn't set username");
console.error(err);
cb('ERROR');
return;
}
Cryptpad.changeDisplayName(newName, true);
cb();
});
});
sframeChan.on('Q_LOGOUT', function (data, cb) {
Cryptpad.logout(cb);
});
sframeChan.on('Q_SET_LOGIN_REDIRECT', function (data, cb) {
sessionStorage.redirectTo = window.location.href;
cb();
});
sframeChan.on('Q_GET_PIN_LIMIT_STATUS', function (data, cb) {
Cryptpad.isOverPinLimit(function (e, overLimit, limits) {
cb({
error: e,
overLimit: overLimit,
limits: limits
});
});
});
sframeChan.on('Q_MOVE_TO_TRASH', function (data, cb) {
Cryptpad.moveToTrash(cb);
});
sframeChan.on('Q_SAVE_AS_TEMPLATE', function (data, cb) {
Cryptpad.saveAsTemplate(Cryptget.put, data, cb);
});
sframeChan.on('Q_GET_FULL_HISTORY', function (data, cb) {
var network = Cryptpad.getNetwork();
var hkn = network.historyKeeper;
var crypto = Crypto.createEncryptor(secret.keys);
// Get the history messages and send them to the iframe
var parse = function (msg) {
try {
return JSON.parse(msg);
} catch (e) {
return null;
}
};
var onMsg = function (msg) {
var parsed = parse(msg);
if (parsed[0] === 'FULL_HISTORY_END') {
console.log('END');
cb();
return;
}
if (parsed[0] !== 'FULL_HISTORY') { return; }
if (parsed[1] && parsed[1].validateKey) { // First message
secret.keys.validateKey = parsed[1].validateKey;
return;
}
msg = parsed[1][4];
if (msg) {
msg = msg.replace(/^cp\|/, '');
var decryptedMsg = crypto.decrypt(msg, secret.keys.validateKey);
sframeChan.event('EV_RT_HIST_MESSAGE', decryptedMsg);
}
};
network.on('message', onMsg);
network.sendto(hkn, JSON.stringify(['GET_FULL_HISTORY', secret.channel, secret.keys.validateKey]));
});
CpNfOuter.start({
sframeChan: sframeChan,
channel: secret.channel,
network: Cryptpad.getNetwork(),
validateKey: secret.keys.validateKey || undefined,
readOnly: readOnly,
crypto: Crypto.createEncryptor(secret.keys),
onConnect: function (wc) {
if (readOnly) { return; }
Cryptpad.replaceHash(Cryptpad.getEditHashFromKeys(wc.id, secret.keys));
}
});
});
});

View File

@ -1,740 +0,0 @@
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
/**
* @fileOverview The WYSIWYG Area plugin. It registers the "wysiwyg" editing
* mode, which handles the main editing area space.
*/
define(['/api/config'], function (ApiConfig) {
var framedWysiwyg;
var iframe;
CKEDITOR.plugins.registered.wysiwygarea.init = function( editor ) {
if ( editor.config.fullPage ) {
editor.addFeature( {
allowedContent: 'html head title; style [media,type]; body (*)[id]; meta link [*]',
requiredContent: 'body'
} );
}
editor.addMode( 'wysiwyg', function( callback ) {
var src = 'document.open();' +
// In IE, the document domain must be set any time we call document.open().
( CKEDITOR.env.ie ? '(' + CKEDITOR.tools.fixDomain + ')();' : '' ) +
'document.close();';
// With IE, the custom domain has to be taken care at first,
// for other browers, the 'src' attribute should be left empty to
// trigger iframe's 'load' event.
// Microsoft Edge throws "Permission Denied" if treated like an IE (http://dev.ckeditor.com/ticket/13441).
if ( CKEDITOR.env.air ) {
src = 'javascript:void(0)'; // jshint ignore:line
} else if ( CKEDITOR.env.ie && !CKEDITOR.env.edge ) {
src = 'javascript:void(function(){' + encodeURIComponent( src ) + '}())'; // jshint ignore:line
} else {
src = '';
}
// CryptPad
src = '/pad/ckeditor-inner.html?' + ApiConfig.requireConf.urlArgs;
iframe = CKEDITOR.dom.element.createFromHtml( '<iframe src="' + src + '" frameBorder="0"></iframe>' );
iframe.setStyles( { width: '100%', height: '100%' } );
iframe.addClass( 'cke_wysiwyg_frame' ).addClass( 'cke_reset' );
// CryptPad
// this is impossible because ckeditor uses some (non-inline) script inside of the iframe...
//iframe.setAttribute('sandbox', 'allow-same-origin');
var contentSpace = editor.ui.space( 'contents' );
contentSpace.append( iframe );
// Asynchronous iframe loading is only required in IE>8 and Gecko (other reasons probably).
// Do not use it on WebKit as it'll break the browser-back navigation.
var useOnloadEvent = ( CKEDITOR.env.ie && !CKEDITOR.env.edge ) || CKEDITOR.env.gecko;
// CryptPad
// This breaks Edge so lets use async all of the time
useOnloadEvent = true;
if ( useOnloadEvent )
iframe.on( 'load', onLoad );
var frameLabel = editor.title,
helpLabel = editor.fire( 'ariaEditorHelpLabel', {} ).label;
if ( frameLabel ) {
if ( CKEDITOR.env.ie && helpLabel )
frameLabel += ', ' + helpLabel;
iframe.setAttribute( 'title', frameLabel );
}
if ( helpLabel ) {
var labelId = CKEDITOR.tools.getNextId(),
desc = CKEDITOR.dom.element.createFromHtml( '<span id="' + labelId + '" class="cke_voice_label">' + helpLabel + '</span>' );
contentSpace.append( desc, 1 );
iframe.setAttribute( 'aria-describedby', labelId );
}
// Remove the ARIA description.
editor.on( 'beforeModeUnload', function( evt ) {
evt.removeListener();
if ( desc )
desc.remove();
} );
iframe.setAttributes( {
tabIndex: editor.tabIndex,
allowTransparency: 'true'
} );
// Execute onLoad manually for all non IE||Gecko browsers.
!useOnloadEvent && onLoad();
editor.fire( 'ariaWidget', iframe );
function onLoad( evt ) {
evt && evt.removeListener();
var fw = new framedWysiwyg( editor, iframe.$.contentWindow.document.body );
editor.editable( fw );
editor.setData( editor.getData( 1 ), callback );
}
} );
};
/**
* Adds the path to a stylesheet file to the exisiting {@link CKEDITOR.config#contentsCss} value.
*
* **Note:** This method is available only with the `wysiwygarea` plugin and only affects
* classic editors based on it (so it does not affect inline editors).
*
* editor.addContentsCss( 'assets/contents.css' );
*
* @since 4.4
* @param {String} cssPath The path to the stylesheet file which should be added.
* @member CKEDITOR.editor
*/
CKEDITOR.editor.prototype.addContentsCss = function( cssPath ) {
var cfg = this.config,
curContentsCss = cfg.contentsCss;
// Convert current value into array.
if ( !CKEDITOR.tools.isArray( curContentsCss ) )
cfg.contentsCss = curContentsCss ? [ curContentsCss ] : [];
cfg.contentsCss.push( cssPath );
};
function onDomReady( win ) {
var editor = this.editor,
doc = win.document,
body = doc.body;
// Remove helper scripts from the DOM.
var script = doc.getElementById( 'cke_actscrpt' );
script && script.parentNode.removeChild( script );
script = doc.getElementById( 'cke_shimscrpt' );
script && script.parentNode.removeChild( script );
script = doc.getElementById( 'cke_basetagscrpt' );
script && script.parentNode.removeChild( script );
body.contentEditable = true;
if ( CKEDITOR.env.ie ) {
// Don't display the focus border.
body.hideFocus = true;
// Disable and re-enable the body to avoid IE from
// taking the editing focus at startup. (http://dev.ckeditor.com/ticket/141 / http://dev.ckeditor.com/ticket/523)
body.disabled = true;
body.removeAttribute( 'disabled' );
}
delete this._.isLoadingData;
// Play the magic to alter element reference to the reloaded one.
this.$ = body;
doc = new CKEDITOR.dom.document( doc );
this.setup();
this.fixInitialSelection();
var editable = this;
// Without it IE8 has problem with removing selection in nested editable. (http://dev.ckeditor.com/ticket/13785)
if ( CKEDITOR.env.ie && !CKEDITOR.env.edge ) {
doc.getDocumentElement().addClass( doc.$.compatMode );
}
// Prevent IE/Edge from leaving a new paragraph/div after deleting all contents in body. (http://dev.ckeditor.com/ticket/6966, http://dev.ckeditor.com/ticket/13142)
if ( CKEDITOR.env.ie && !CKEDITOR.env.edge && editor.enterMode != CKEDITOR.ENTER_P ) {
removeSuperfluousElement( 'p' );
} else if ( CKEDITOR.env.edge && editor.enterMode != CKEDITOR.ENTER_DIV ) {
removeSuperfluousElement( 'div' );
}
// Fix problem with cursor not appearing in Webkit and IE11+ when clicking below the body (http://dev.ckeditor.com/ticket/10945, http://dev.ckeditor.com/ticket/10906).
// Fix for older IEs (8-10 and QM) is placed inside selection.js.
if ( CKEDITOR.env.webkit || ( CKEDITOR.env.ie && CKEDITOR.env.version > 10 ) ) {
doc.getDocumentElement().on( 'mousedown', function( evt ) {
if ( evt.data.getTarget().is( 'html' ) ) {
// IE needs this timeout. Webkit does not, but it does not cause problems too.
setTimeout( function() {
editor.editable().focus();
} );
}
} );
}
// Config props: disableObjectResizing and disableNativeTableHandles handler.
objectResizeDisabler( editor );
// Enable dragging of position:absolute elements in IE.
try {
editor.document.$.execCommand( '2D-position', false, true );
} catch ( e ) {}
if ( CKEDITOR.env.gecko || CKEDITOR.env.ie && editor.document.$.compatMode == 'CSS1Compat' ) {
this.attachListener( this, 'keydown', function( evt ) {
var keyCode = evt.data.getKeystroke();
// PageUp OR PageDown
if ( keyCode == 33 || keyCode == 34 ) {
// PageUp/PageDown scrolling is broken in document
// with standard doctype, manually fix it. (http://dev.ckeditor.com/ticket/4736)
if ( CKEDITOR.env.ie ) {
setTimeout( function() {
editor.getSelection().scrollIntoView();
}, 0 );
}
// Page up/down cause editor selection to leak
// outside of editable thus we try to intercept
// the behavior, while it affects only happen
// when editor contents are not overflowed. (http://dev.ckeditor.com/ticket/7955)
else if ( editor.window.$.innerHeight > this.$.offsetHeight ) {
var range = editor.createRange();
range[ keyCode == 33 ? 'moveToElementEditStart' : 'moveToElementEditEnd' ]( this );
range.select();
evt.data.preventDefault();
}
}
} );
}
if ( CKEDITOR.env.ie ) {
// [IE] Iframe will still keep the selection when blurred, if
// focus is moved onto a non-editing host, e.g. link or button, but
// it becomes a problem for the object type selection, since the resizer
// handler attached on it will mark other part of the UI, especially
// for the dialog. (http://dev.ckeditor.com/ticket/8157)
// [IE<8 & Opera] Even worse For old IEs, the cursor will not vanish even if
// the selection has been moved to another text input in some cases. (http://dev.ckeditor.com/ticket/4716)
//
// Now the range restore is disabled, so we simply force IE to clean
// up the selection before blur.
this.attachListener( doc, 'blur', function() {
// Error proof when the editor is not visible. (http://dev.ckeditor.com/ticket/6375)
try {
doc.$.selection.empty();
} catch ( er ) {}
} );
}
if ( CKEDITOR.env.iOS ) {
// [iOS] If touch is bound to any parent of the iframe blur happens on any touch
// event and body becomes the focused element (http://dev.ckeditor.com/ticket/10714).
this.attachListener( doc, 'touchend', function() {
win.focus();
} );
}
var title = editor.document.getElementsByTag( 'title' ).getItem( 0 );
// document.title is malfunctioning on Chrome, so get value from the element (http://dev.ckeditor.com/ticket/12402).
title.data( 'cke-title', title.getText() );
// [IE] JAWS will not recognize the aria label we used on the iframe
// unless the frame window title string is used as the voice label,
// backup the original one and restore it on output.
if ( CKEDITOR.env.ie )
editor.document.$.title = this._.docTitle;
CKEDITOR.tools.setTimeout( function() {
// Editable is ready after first setData.
if ( this.status == 'unloaded' )
this.status = 'ready';
editor.fire( 'contentDom' );
if ( this._.isPendingFocus ) {
editor.focus();
this._.isPendingFocus = false;
}
setTimeout( function() {
editor.fire( 'dataReady' );
}, 0 );
}, 0, this );
function removeSuperfluousElement( tagName ) {
var lockRetain = false;
// Superfluous elements appear after keydown
// and before keyup, so the procedure is as follows:
// 1. On first keydown mark all elements with
// a specified tag name as non-superfluous.
editable.attachListener( editable, 'keydown', function() {
var body = doc.getBody(),
retained = body.getElementsByTag( tagName );
if ( !lockRetain ) {
for ( var i = 0; i < retained.count(); i++ ) {
retained.getItem( i ).setCustomData( 'retain', true );
}
lockRetain = true;
}
}, null, null, 1 );
// 2. On keyup remove all elements that were not marked
// as non-superfluous (which means they must have had appeared in the meantime).
// Also we should preserve all temporary elements inserted by editor otherwise we'd likely
// leak fake selection's content into editable due to removing hidden selection container (http://dev.ckeditor.com/ticket/14831).
editable.attachListener( editable, 'keyup', function() {
var elements = doc.getElementsByTag( tagName );
if ( lockRetain ) {
if ( elements.count() == 1 && !elements.getItem( 0 ).getCustomData( 'retain' ) &&
!elements.getItem( 0 ).hasAttribute( 'data-cke-temp' ) ) {
elements.getItem( 0 ).remove( 1 );
}
lockRetain = false;
}
} );
}
}
framedWysiwyg = CKEDITOR.tools.createClass( {
$: function() {
this.base.apply( this, arguments );
this._.frameLoadedHandler = CKEDITOR.tools.addFunction( function( win ) {
// Avoid opening design mode in a frame window thread,
// which will cause host page scrolling.(http://dev.ckeditor.com/ticket/4397)
CKEDITOR.tools.setTimeout( onDomReady, 0, this, win );
}, this );
this._.docTitle = this.getWindow().getFrame().getAttribute( 'title' );
},
base: CKEDITOR.editable,
proto: {
setData: function( data, isSnapshot ) {
var editor = this.editor;
if ( isSnapshot ) {
this.setHtml( data );
this.fixInitialSelection();
// Fire dataReady for the consistency with inline editors
// and because it makes sense. (http://dev.ckeditor.com/ticket/10370)
editor.fire( 'dataReady' );
}
else {
this._.isLoadingData = true;
editor._.dataStore = { id: 1 };
var config = editor.config,
fullPage = config.fullPage,
docType = config.docType;
// Build the additional stuff to be included into <head>.
var headExtra = CKEDITOR.tools.buildStyleHtml( iframeCssFixes() ).replace( /<style>/, '<style data-cke-temp="1">' );
if ( !fullPage )
headExtra += CKEDITOR.tools.buildStyleHtml( editor.config.contentsCss );
var baseTag = config.baseHref ? '<base href="' + config.baseHref + '" data-cke-temp="1" />' : '';
if ( fullPage ) {
// Search and sweep out the doctype declaration.
data = data.replace( /<!DOCTYPE[^>]*>/i, function( match ) {
editor.docType = docType = match;
return '';
} ).replace( /<\?xml\s[^\?]*\?>/i, function( match ) {
editor.xmlDeclaration = match;
return '';
} );
}
// Get the HTML version of the data.
data = editor.dataProcessor.toHtml( data );
if ( fullPage ) {
// Check if the <body> tag is available.
if ( !( /<body[\s|>]/ ).test( data ) )
data = '<body>' + data;
// Check if the <html> tag is available.
if ( !( /<html[\s|>]/ ).test( data ) )
data = '<html>' + data + '</html>';
// Check if the <head> tag is available.
if ( !( /<head[\s|>]/ ).test( data ) )
data = data.replace( /<html[^>]*>/, '$&<head><title></title></head>' );
else if ( !( /<title[\s|>]/ ).test( data ) )
data = data.replace( /<head[^>]*>/, '$&<title></title>' );
// The base must be the first tag in the HEAD, e.g. to get relative
// links on styles.
baseTag && ( data = data.replace( /<head[^>]*?>/, '$&' + baseTag ) );
// Inject the extra stuff into <head>.
// Attention: do not change it before testing it well. (V2)
// This is tricky... if the head ends with <meta ... content type>,
// Firefox will break. But, it works if we place our extra stuff as
// the last elements in the HEAD.
data = data.replace( /<\/head\s*>/, headExtra + '$&' );
// Add the DOCTYPE back to it.
data = docType + data;
} else {
data = config.docType +
'<html dir="' + config.contentsLangDirection + '"' +
' lang="' + ( config.contentsLanguage || editor.langCode ) + '">' +
'<head>' +
'<title>' + this._.docTitle + '</title>' +
baseTag +
headExtra +
'</head>' +
'<body' + ( config.bodyId ? ' id="' + config.bodyId + '"' : '' ) +
( config.bodyClass ? ' class="' + config.bodyClass + '"' : '' ) +
'>' +
data +
'</body>' +
'</html>';
}
if ( CKEDITOR.env.gecko ) {
// Hack to make Fx put cursor at the start of doc on fresh focus.
data = data.replace( /<body/, '<body contenteditable="true" ' );
// Another hack which is used by onDomReady to remove a leading
// <br> which is inserted by Firefox 3.6 when document.write is called.
// This additional <br> is present because of contenteditable="true"
if ( CKEDITOR.env.version < 20000 )
data = data.replace( /<body[^>]*>/, '$&<!-- cke-content-start -->' );
}
// The script that launches the bootstrap logic on 'domReady', so the document
// is fully editable even before the editing iframe is fully loaded (http://dev.ckeditor.com/ticket/4455).
var bootstrapCode =
'<script id="cke_actscrpt" type="text/javascript"' + ( CKEDITOR.env.ie ? ' defer="defer" ' : '' ) + '>' +
'var wasLoaded=0;' + // It must be always set to 0 as it remains as a window property.
'function onload(){' +
'if(!wasLoaded)' + // FF3.6 calls onload twice when editor.setData. Stop that.
'window.parent.CKEDITOR.tools.callFunction(' + this._.frameLoadedHandler + ',window);' +
'wasLoaded=1;' +
'}' +
( CKEDITOR.env.ie ? 'onload();' : 'document.addEventListener("DOMContentLoaded", onload, false );' ) +
'</script>';
// For IE<9 add support for HTML5's elements.
// Note: this code must not be deferred.
if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) {
bootstrapCode +=
'<script id="cke_shimscrpt">' +
'window.parent.CKEDITOR.tools.enableHtml5Elements(document)' +
'</script>';
}
// IE<10 needs this hack to properly enable <base href="...">.
// See: http://stackoverflow.com/a/13373180/1485219 (http://dev.ckeditor.com/ticket/11910).
if ( baseTag && CKEDITOR.env.ie && CKEDITOR.env.version < 10 ) {
bootstrapCode +=
'<script id="cke_basetagscrpt">' +
'var baseTag = document.querySelector( "base" );' +
'baseTag.href = baseTag.href;' +
'</script>';
}
data = data.replace( /(?=\s*<\/(:?head)>)/, bootstrapCode );
// Current DOM will be deconstructed by document.write, cleanup required.
this.clearCustomData();
this.clearListeners();
editor.fire( 'contentDomUnload' );
var doc = this.getDocument();
// CryptPad
var _iframe = window._iframe = iframe.$;
var fw = this;
_iframe.contentWindow.onload = function () {}
var intr = setInterval(function () {
//console.log(_iframe.contentWindow.document.body);
if (!_iframe.contentWindow) { return; }
if (!_iframe.contentWindow.document) { return; }
if (_iframe.contentWindow.document.readyState !== 'complete') { return; }
if (!_iframe.contentWindow.document.getElementsByTagName('title').length) { return; }
clearInterval(intr);
CKEDITOR.tools.callFunction(fw._.frameLoadedHandler, _iframe.contentWindow);
}, 10);
return;
// Work around Firefox bug - error prune when called from XUL (http://dev.ckeditor.com/ticket/320),
// defer it thanks to the async nature of this method.
try {
doc.write( data );
} catch ( e ) {
setTimeout( function() {
doc.write( data );
}, 0 );
}
}
},
getData: function( isSnapshot ) {
if ( isSnapshot )
return this.getHtml();
else {
var editor = this.editor,
config = editor.config,
fullPage = config.fullPage,
docType = fullPage && editor.docType,
xmlDeclaration = fullPage && editor.xmlDeclaration,
doc = this.getDocument();
var data = fullPage ? doc.getDocumentElement().getOuterHtml() : doc.getBody().getHtml();
// BR at the end of document is bogus node for Mozilla. (http://dev.ckeditor.com/ticket/5293).
// Prevent BRs from disappearing from the end of the content
// while enterMode is ENTER_BR (http://dev.ckeditor.com/ticket/10146).
if ( CKEDITOR.env.gecko && config.enterMode != CKEDITOR.ENTER_BR )
data = data.replace( /<br>(?=\s*(:?$|<\/body>))/, '' );
data = editor.dataProcessor.toDataFormat( data );
if ( xmlDeclaration )
data = xmlDeclaration + '\n' + data;
if ( docType )
data = docType + '\n' + data;
return data;
}
},
focus: function() {
if ( this._.isLoadingData )
this._.isPendingFocus = true;
else
framedWysiwyg.baseProto.focus.call( this );
},
detach: function() {
var editor = this.editor,
doc = editor.document,
iframe,
onResize;
// Trying to access window's frameElement property on Edge throws an exception
// when frame was already removed from DOM. (http://dev.ckeditor.com/ticket/13850, http://dev.ckeditor.com/ticket/13790)
try {
iframe = editor.window.getFrame();
} catch ( e ) {}
framedWysiwyg.baseProto.detach.call( this );
// Memory leak proof.
this.clearCustomData();
doc.getDocumentElement().clearCustomData();
CKEDITOR.tools.removeFunction( this._.frameLoadedHandler );
// On IE, iframe is returned even after remove() method is called on it.
// Checking if parent is present fixes this issue. (http://dev.ckeditor.com/ticket/13850)
if ( iframe && iframe.getParent() ) {
iframe.clearCustomData();
onResize = iframe.removeCustomData( 'onResize' );
onResize && onResize.removeListener();
// IE BUG: When destroying editor DOM with the selection remains inside
// editing area would break IE7/8's selection system, we have to put the editing
// iframe offline first. (http://dev.ckeditor.com/ticket/3812 and http://dev.ckeditor.com/ticket/5441)
iframe.remove();
} else {
CKEDITOR.warn( 'editor-destroy-iframe' );
}
}
}
} );
function objectResizeDisabler( editor ) {
if ( CKEDITOR.env.gecko ) {
// FF allows to change resizing preferences by calling execCommand.
try {
var doc = editor.document.$;
doc.execCommand( 'enableObjectResizing', false, !editor.config.disableObjectResizing );
doc.execCommand( 'enableInlineTableEditing', false, !editor.config.disableNativeTableHandles );
} catch ( e ) {}
} else if ( CKEDITOR.env.ie && CKEDITOR.env.version < 11 && editor.config.disableObjectResizing ) {
// It's possible to prevent resizing up to IE10.
blockResizeStart( editor );
}
// Disables resizing by preventing default action on resizestart event.
function blockResizeStart() {
var lastListeningElement;
// We'll attach only one listener at a time, instead of adding it to every img, input, hr etc.
// Listener will be attached upon selectionChange, we'll also check if there was any element that
// got listener before (lastListeningElement) - if so we need to remove previous listener.
editor.editable().attachListener( editor, 'selectionChange', function() {
var selectedElement = editor.getSelection().getSelectedElement();
if ( selectedElement ) {
if ( lastListeningElement ) {
lastListeningElement.detachEvent( 'onresizestart', resizeStartListener );
lastListeningElement = null;
}
// IE requires using attachEvent, because it does not work using W3C compilant addEventListener,
// tested with IE10.
selectedElement.$.attachEvent( 'onresizestart', resizeStartListener );
lastListeningElement = selectedElement.$;
}
} );
}
function resizeStartListener( evt ) {
evt.returnValue = false;
}
}
function iframeCssFixes() {
var css = [];
// IE>=8 stricts mode doesn't have 'contentEditable' in effect
// on element unless it has layout. (http://dev.ckeditor.com/ticket/5562)
if ( CKEDITOR.document.$.documentMode >= 8 ) {
css.push( 'html.CSS1Compat [contenteditable=false]{min-height:0 !important}' );
var selectors = [];
for ( var tag in CKEDITOR.dtd.$removeEmpty )
selectors.push( 'html.CSS1Compat ' + tag + '[contenteditable=false]' );
css.push( selectors.join( ',' ) + '{display:inline-block}' );
}
// Set the HTML style to 100% to have the text cursor in affect (http://dev.ckeditor.com/ticket/6341)
else if ( CKEDITOR.env.gecko ) {
css.push( 'html{height:100% !important}' );
css.push( 'img:-moz-broken{-moz-force-broken-image-icon:1;min-width:24px;min-height:24px}' );
}
// http://dev.ckeditor.com/ticket/6341: The text cursor must be set on the editor area.
// http://dev.ckeditor.com/ticket/6632: Avoid having "text" shape of cursor in IE7 scrollbars.
css.push( 'html{cursor:text;*cursor:auto}' );
// Use correct cursor for these elements
css.push( 'img,input,textarea{cursor:default}' );
return css.join( '\n' );
}
});
/**
* Disables the ability to resize objects (images and tables) in the editing area.
*
* config.disableObjectResizing = true;
*
* **Note:** Because of incomplete implementation of editing features in browsers
* this option does not work for inline editors (see ticket [#10197](http://dev.ckeditor.com/ticket/10197)),
* does not work in Internet Explorer 11+ (see [#9317](http://dev.ckeditor.com/ticket/9317#comment:16) and
* [IE11+ issue](https://connect.microsoft.com/IE/feedback/details/742593/please-respect-execcommand-enableobjectresizing-in-contenteditable-elements)).
* In Internet Explorer 8-10 this option only blocks resizing, but it is unable to hide the resize handles.
*
* @cfg
* @member CKEDITOR.config
*/
CKEDITOR.config.disableObjectResizing = false;
/**
* Disables the "table tools" offered natively by the browser (currently
* Firefox only) to perform quick table editing operations, like adding or
* deleting rows and columns.
*
* config.disableNativeTableHandles = false;
*
* @cfg
* @member CKEDITOR.config
*/
CKEDITOR.config.disableNativeTableHandles = true;
/**
* Disables the built-in spell checker if the browser provides one.
*
* **Note:** Although word suggestions provided natively by the browsers will
* not appear in CKEditor's default context menu,
* users can always reach the native context menu by holding the
* *Ctrl* key when right-clicking if {@link #browserContextMenuOnCtrl}
* is enabled or you are simply not using the
* [context menu](http://ckeditor.com/addon/contextmenu) plugin.
*
* config.disableNativeSpellChecker = false;
*
* @cfg
* @member CKEDITOR.config
*/
CKEDITOR.config.disableNativeSpellChecker = true;
/**
* Language code of the writing language which is used to author the editor
* content. This option accepts one single entry value in the format defined in the
* [Tags for Identifying Languages (BCP47)](http://www.ietf.org/rfc/bcp/bcp47.txt)
* IETF document and is used in the `lang` attribute.
*
* config.contentsLanguage = 'fr';
*
* @cfg {String} [contentsLanguage=same value with editor's UI language]
* @member CKEDITOR.config
*/
/**
* The base href URL used to resolve relative and absolute URLs in the
* editor content.
*
* config.baseHref = 'http://www.example.com/path/';
*
* @cfg {String} [baseHref='']
* @member CKEDITOR.config
*/
/**
* Whether to automatically create wrapping blocks around inline content inside the document body.
* This helps to ensure the integrity of the block *Enter* mode.
*
* **Note:** This option is deprecated. Changing the default value might introduce unpredictable usability issues and is
* highly unrecommended.
*
* config.autoParagraph = false;
*
* @deprecated
* @since 3.6
* @cfg {Boolean} [autoParagraph=true]
* @member CKEDITOR.config
*/
/**
* Fired when some elements are added to the document.
*
* @event ariaWidget
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
* @param {CKEDITOR.dom.element} data The element being added.
*/