Merge branch 'staging' of github.com:xwiki-labs/cryptpad into staging

This commit is contained in:
ansuz
2017-08-25 14:42:23 +02:00
70 changed files with 5124 additions and 176 deletions

View File

@@ -1,24 +1,8 @@
// This is stage 1, it can be changed but you must bump the version of the project.
define([], function () {
// fix up locations so that relative urls work.
require.config({
baseUrl: window.location.pathname,
paths: {
// jquery declares itself as literally "jquery" so it cannot be pulled by path :(
"jquery": "/bower_components/jquery/dist/jquery.min",
// json.sortify same
"json.sortify": "/bower_components/json.sortify/dist/JSON.sortify",
//"pdfjs-dist/build/pdf": "/bower_components/pdfjs-dist/build/pdf",
//"pdfjs-dist/build/pdf.worker": "/bower_components/pdfjs-dist/build/pdf.worker"
cm: '/bower_components/codemirror'
},
map: {
'*': {
'css': '/bower_components/require-css/css.js',
'less': '/common/RequireLess.js',
}
}
});
define([
'/common/requireconfig.js'
], function (RequireConfig) {
require.config(RequireConfig());
// most of CryptPad breaks if you don't support isArray
if (!Array.isArray) {

View File

@@ -38,13 +38,13 @@ define([
var parsed = config.href ? common.parsePadUrl(config.href) : {};
var secret = common.getSecrets(parsed.type, parsed.hash);
History.readOnly = 1;
History.readOnly = 0;
if (!secret.keys) {
secret.keys = secret.key;
History.readOnly = 2;
History.readOnly = 0;
}
else if (!secret.keys.validateKey) {
History.readOnly = 0;
History.readOnly = 1;
}
var crypto = Crypto.createEncryptor(secret.keys);
@@ -203,7 +203,7 @@ define([
'class':'revertHistory buttonSuccess',
title: Messages.history_restoreTitle
}).text(Messages.history_restore).appendTo($nav);
if (!History.readOnly) { $rev.hide(); }
if (History.readOnly) { $rev.hide(); }
onUpdate = function () {
$cur.attr('max', states.length);

View File

@@ -9,38 +9,46 @@ define([
AppConfig.badStateTimeout: 30000;
var connected = false;
var intr;
var infiniteSpinnerHandlers = [];
/*
TODO make this not blow up when disconnected or lagging...
*/
common.whenRealtimeSyncs = function (realtime, cb) {
realtime.sync();
common.whenRealtimeSyncs = function (Cryptpad, realtime, cb) {
window.setTimeout(function () {
if (realtime.getAuthDoc() === realtime.getUserDoc()) {
return void cb();
} else {
realtime.onSettle(cb);
}
var to = setTimeout(function () {
if (!connected) { return; }
if (intr) { return; }
intr = window.setInterval(function () {
var l;
try {
l = realtime.getLag();
} catch (e) {
throw new Error("ChainPad.getLag() does not exist, please `bower update`");
}
if (l.lag < BAD_STATE_TIMEOUT || !connected) { return; }
realtime.abort();
// don't launch more than one popup
if (common.infiniteSpinnerDetected) { return; }
infiniteSpinnerHandlers.forEach(function (ish) { ish(); });
// inform the user their session is in a bad state
common.confirm(Messages.realtime_unrecoverableError, function (yes) {
Cryptpad.confirm(Messages.realtime_unrecoverableError, function (yes) {
if (!yes) { return; }
window.location.reload();
});
common.infiniteSpinnerDetected = true;
}, BAD_STATE_TIMEOUT);
realtime.onSettle(function () {
clearTimeout(to);
cb();
});
}, 2000);
}, 0);
};
common.onInfiniteSpinner = function (f) { infiniteSpinnerHandlers.push(f); };
common.setConnectionState = function (bool) {
if (typeof(bool) !== 'boolean') { return; }
connected = bool;

View File

@@ -0,0 +1,112 @@
define(function () {
var module = {};
module.create = function (info, onLocal, Cryptget, Cryptpad) {
var exp = {};
var userData = exp.userData = {};
var userList = exp.userList = info.userList;
var myData = exp.myData = {};
exp.myUserName = info.myID;
exp.myNetfluxId = info.myID;
var network = Cryptpad.getNetwork();
var parsed = Cryptpad.parsePadUrl(window.location.href);
var appType = parsed ? parsed.type : undefined;
var addToUserData = exp.addToUserData = function(data) {
var users = userList.users;
for (var attrname in data) { userData[attrname] = data[attrname]; }
if (users && users.length) {
for (var userKey in userData) {
if (users.indexOf(userKey) === -1) {
delete userData[userKey];
}
}
}
if(userList && typeof userList.onChange === "function") {
userList.onChange(userData);
}
};
exp.getToolbarConfig = function () {
return {
data: userData,
list: userList,
userNetfluxId: exp.myNetfluxId
};
};
var setName = exp.setName = function (newName, cb) {
if (typeof(newName) !== 'string') { return; }
var myUserNameTemp = newName.trim();
if(myUserNameTemp.length > 32) {
myUserNameTemp = myUserNameTemp.substr(0, 32);
}
exp.myUserName = myUserNameTemp;
myData = {};
myData[exp.myNetfluxId] = {
name: exp.myUserName,
uid: Cryptpad.getUid(),
avatar: Cryptpad.getAvatarUrl(),
profile: Cryptpad.getProfileUrl(),
curvePublic: Cryptpad.getProxy().curvePublic
};
addToUserData(myData);
/*Cryptpad.setAttribute('username', exp.myUserName, function (err) {
if (err) {
console.log("Couldn't set username");
console.error(err);
return;
}
if (typeof cb === "function") { cb(); }
});*/
if (typeof cb === "function") { cb(); }
};
exp.getLastName = function ($changeNameButton, isNew) {
Cryptpad.getLastName(function (err, lastName) {
if (err) {
console.log("Could not get previous name");
console.error(err);
return;
}
// Update the toolbar list:
// Add the current user in the metadata
if (typeof(lastName) === 'string') {
setName(lastName, onLocal);
} else {
myData[exp.myNetfluxId] = {
name: "",
uid: Cryptpad.getUid(),
avatar: Cryptpad.getAvatarUrl(),
profile: Cryptpad.getProfileUrl(),
curvePublic: Cryptpad.getProxy().curvePublic
};
addToUserData(myData);
onLocal();
$changeNameButton.click();
}
if (isNew && appType) {
Cryptpad.selectTemplate(appType, info.realtime, Cryptget);
}
});
};
Cryptpad.onDisplayNameChanged(function (newName) {
setName(newName, onLocal);
});
network.on('reconnect', function (uid) {
exp.myNetfluxId = uid;
exp.setName(exp.myUserName);
});
return exp;
};
return module;
});

View File

@@ -132,7 +132,9 @@ define([
common.initMessagingUI = Messaging.UI.init;
// Realtime
var whenRealtimeSyncs = common.whenRealtimeSyncs = Realtime.whenRealtimeSyncs;
var whenRealtimeSyncs = common.whenRealtimeSyncs = function (realtime, cb) {
Realtime.whenRealtimeSyncs(common, realtime, cb);
};
// Userlist
common.createUserList = UserList.create;
@@ -198,10 +200,20 @@ define([
}
return '';
};
common.getAccountName = function () {
return localStorage[common.userNameKey];
};
var randomToken = function () {
return Math.random().toString(16).replace(/0./, '');
};
common.isFeedbackAllowed = function () {
try {
if (!getStore().getProxy().proxy.allowUserFeedback) { return; }
return true;
} catch (e) { return void console.error(e); }
};
var feedback = common.feedback = function (action, force) {
if (force !== true) {
if (!action) { return; }
@@ -827,6 +839,13 @@ define([
});
};
// SFRAME: talk to anon_rpc from the iframe
common.anonRpcMsg = function (msg, data, cb) {
if (!msg) { return; }
if (!anon_rpc) { return void cb('ANON_RPC_NOT_READY'); }
anon_rpc.send(msg, data, cb);
};
common.getFileSize = function (href, cb) {
if (!anon_rpc) { return void cb('ANON_RPC_NOT_READY'); }
//if (!pinsReady()) { return void cb('RPC_NOT_READY'); }
@@ -1030,6 +1049,41 @@ define([
};
};
// Forget button
var moveToTrash = common.moveToTrash = function (cb) {
var href = window.location.href;
common.forgetPad(href, function (err) {
if (err) {
console.log("unable to forget pad");
console.error(err);
cb(err, null);
return;
}
var n = getNetwork();
var r = getRealtime();
if (n && r) {
whenRealtimeSyncs(r, function () {
n.disconnect();
cb();
});
} else {
cb();
}
});
};
var saveAsTemplate = common.saveAsTemplate = function (Cryptput, data, cb) {
var p = parsePadUrl(window.location.href);
if (!p.type) { return; }
var hash = createRandomHash();
var href = '/' + p.type + '/#' + hash;
Cryptput(hash, data.toSave, function (e) {
if (e) { throw new Error(e); }
common.addTemplate(makePad(href, data.title));
whenRealtimeSyncs(getStore().getProxy().info.realtime, function () {
cb();
});
});
};
common.createButton = function (type, rightside, data, callback) {
var button;
var size = "17px";
@@ -1118,17 +1172,12 @@ define([
console.error("Parse error while setting the title", e);
}
}
var p = parsePadUrl(window.location.href);
if (!p.type) { return; }
var hash = createRandomHash();
var href = '/' + p.type + '/#' + hash;
data.Crypt.put(hash, toSave, function (e) {
if (e) { throw new Error(e); }
common.addTemplate(makePad(href, title));
whenRealtimeSyncs(getStore().getProxy().info.realtime, function () {
common.alert(Messages.templateSaved);
common.feedback('TEMPLATE_CREATED');
});
saveAsTemplate(data.Crypt.put, {
title: title,
toSave: toSave
}, function () {
common.alert(Messages.templateSaved);
common.feedback('TEMPLATE_CREATED');
});
};
common.prompt(Messages.saveTemplatePrompt, title || document.title, todo);
@@ -1151,29 +1200,14 @@ define([
button
.click(prepareFeedback(type))
.click(function() {
var href = window.location.href;
var msg = isLoggedIn() ? Messages.forgetPrompt : Messages.fm_removePermanentlyDialog;
common.confirm(msg, function (yes) {
if (!yes) { return; }
common.forgetPad(href, function (err) {
if (err) {
console.log("unable to forget pad");
console.error(err);
callback(err, null);
return;
}
var n = getNetwork();
var r = getRealtime();
if (n && r) {
whenRealtimeSyncs(r, function () {
n.disconnect();
callback();
});
} else {
callback();
}
moveToTrash(function (err) {
if (err) { return void callback(err); }
var cMsg = isLoggedIn() ? Messages.movedToTrash : Messages.deleted;
common.alert(cMsg, undefined, true);
callback();
return;
});
});
@@ -1254,7 +1288,7 @@ define([
}
return arr;
};
var getFirstEmojiOrCharacter = function (str) {
var getFirstEmojiOrCharacter = common.getFirstEmojiOrCharacter = function (str) {
if (!str || !str.trim()) { return '?'; }
var emojis = emojiStringToArray(str);
return isEmoji(emojis[0])? emojis[0]: str[0];
@@ -1305,6 +1339,7 @@ define([
'image/jpg',
'image/gif',
];
// SFRAME: copied to sframe-common-interface.js
common.displayAvatar = function ($container, href, name, cb) {
var MutationObserver = window.MutationObserver;
var displayDefault = function () {
@@ -1643,6 +1678,7 @@ define([
return $block;
};
// SFRAME: moved to sframe-common-interface.js
common.createUserAdminMenu = function (config) {
var $displayedName = $('<span>', {'class': config.displayNameCls || 'displayName'});
var accountName = localStorage[common.userNameKey];
@@ -1792,6 +1828,27 @@ define([
return $userAdmin;
};
common.getShareHashes = function (secret, cb) {
if (!window.location.hash) {
var hashes = common.getHashes(secret.channel, secret);
return void cb(null, hashes);
}
common.getRecentPads(function (err, recent) {
var parsed = parsePadUrl(window.location.href);
if (!parsed.type || !parsed.hashData) { return void cb('E_INVALID_HREF'); }
var hashes = common.getHashes(secret.channel, secret);
// If we have a stronger version in drive, add it and add a redirect button
var stronger = recent && common.findStronger(null, recent);
if (stronger) {
var parsed2 = parsePadUrl(stronger);
hashes.editHash = parsed2.hash;
}
cb(null, hashes);
});
};
var CRYPTPAD_VERSION = 'cryptpad-version';
var updateLocalVersion = function () {
// Check for CryptPad updates

31
www/common/loading.js Normal file
View File

@@ -0,0 +1,31 @@
define([
'less!/customize/src/less/loading.less'
], function () {
var urlArgs = window.location.href.replace(/^.*\?([^\?]*)$/, function (all, x) { return x; });
var elem = document.createElement('div');
elem.setAttribute('id', 'loading');
elem.innerHTML = [
'<div class="loadingContainer">',
'<img class="cryptofist" src="/customize/cryptpad-new-logo-colors-logoonly.png?' + urlArgs + '">',
'<div class="spinnerContainer">',
'<span class="fa fa-circle-o-notch fa-spin fa-4x fa-fw"></span>',
'</div>',
'<p id="cp-loading-message"></p>',
'</div>'
].join('');
var intr;
var append = function () {
if (!document.body) { return; }
clearInterval(intr);
document.body.appendChild(elem);
require([
'/customize/messages.js',
'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
], function (Messages) {
document.getElementById('cp-loading-message').innerText = Messages.loading;
});
};
intr = setInterval(append, 100);
append();
});

View File

@@ -0,0 +1,139 @@
define([], function () {
var UNINIT = 'uninitialized';
var create = function (sframeChan) {
var meta = UNINIT;
var members = [];
var metadataObj = UNINIT;
// This object reflects the metadata which is in the document at this moment.
// Normally when a person leaves the pad, everybody sees them leave and updates
// their metadata, this causes everyone to fight to change the document and
// operational transform doesn't like it. So this is a lazy object which is
// only updated either:
// 1. On changes to the metadata that come in from someone else
// 2. On changes connects, disconnects or changes to your own metadata
var metadataLazyObj = UNINIT;
var priv = {};
var dirty = true;
var changeHandlers = [];
var lazyChangeHandlers = [];
var titleChangeHandlers = [];
var rememberedTitle;
var checkUpdate = function (lazy) {
if (!dirty) { return; }
if (meta === UNINIT) { throw new Error(); }
if (metadataObj === UNINIT) {
metadataObj = {
defaultTitle: meta.doc.defaultTitle,
title: meta.doc.defaultTitle,
type: meta.doc.type,
users: {}
};
metadataLazyObj = JSON.parse(JSON.stringify(metadataObj));
}
if (!metadataObj.users) { metadataObj.users = {}; }
if (!metadataLazyObj.users) { metadataLazyObj.users = {}; }
var mdo = {};
// We don't want to add our user data to the object multiple times.
//var containsYou = false;
//console.log(metadataObj);
Object.keys(metadataObj.users).forEach(function (x) {
if (members.indexOf(x) === -1) { return; }
mdo[x] = metadataObj.users[x];
/*if (metadataObj.users[x].uid === meta.user.uid) {
//console.log('document already contains you');
containsYou = true;
}*/
});
//if (!containsYou) { mdo[meta.user.netfluxId] = meta.user; }
mdo[meta.user.netfluxId] = meta.user;
metadataObj.users = mdo;
var lazyUserStr = JSON.stringify(metadataLazyObj.users[meta.user.netfluxId]);
dirty = false;
if (lazy || lazyUserStr !== JSON.stringify(meta.user)) {
metadataLazyObj = JSON.parse(JSON.stringify(metadataObj));
lazyChangeHandlers.forEach(function (f) { f(); });
}
if (metadataObj.title !== rememberedTitle) {
console.log("Title update\n" + metadataObj.title + '\n');
rememberedTitle = metadataObj.title;
titleChangeHandlers.forEach(function (f) { f(metadataObj.title); });
}
changeHandlers.forEach(function (f) { f(); });
};
var change = function (lazy) {
dirty = true;
setTimeout(function () {
checkUpdate(lazy);
});
};
sframeChan.on('EV_METADATA_UPDATE', function (ev) {
meta = ev;
if (ev.priv) {
priv = ev.priv;
}
change(true);
});
sframeChan.on('EV_RT_CONNECT', function (ev) {
meta.user.netfluxId = ev.myID;
members = ev.members;
change(true);
});
sframeChan.on('EV_RT_JOIN', function (ev) {
members.push(ev);
change(false);
});
sframeChan.on('EV_RT_LEAVE', function (ev) {
var idx = members.indexOf(ev);
if (idx === -1) { console.log('Error: ' + ev + ' not in members'); return; }
members.splice(idx, 1);
change(false);
});
sframeChan.on('EV_RT_DISCONNECT', function () {
members = [];
change(true);
});
return Object.freeze({
updateMetadata: function (m) {
if (JSON.stringify(metadataObj) === JSON.stringify(m)) { return; }
metadataObj = JSON.parse(JSON.stringify(m));
metadataLazyObj = JSON.parse(JSON.stringify(m));
change(false);
},
updateTitle: function (t) {
metadataObj.title = t;
change(true);
},
getMetadata: function () {
checkUpdate(false);
return Object.freeze(JSON.parse(JSON.stringify(metadataObj)));
},
getMetadataLazy: function () {
return metadataLazyObj;
},
onTitleChange: function (f) { titleChangeHandlers.push(f); },
onChange: function (f) { changeHandlers.push(f); },
onChangeLazy: function (f) { lazyChangeHandlers.push(f); },
isConnected : function () {
return members.indexOf(meta.user.netfluxId) !== -1;
},
getViewers : function () {
checkUpdate(false);
var list = members.slice().filter(function (m) { return m.length === 32; });
return list.length - Object.keys(metadataObj.users).length;
},
getPrivateData : function () {
return priv;
},
getNetfluxId : function () {
return meta.user.netfluxId;
}
});
};
return Object.freeze({ create: create });
});

View File

@@ -1,11 +1,4 @@
(function () {
var Mod = function (ApiConfig) {
var requireConf;
if (ApiConfig && ApiConfig.requireConf) {
requireConf = ApiConfig.requireConf;
}
var urlArgs = typeof(requireConf.urlArgs) === 'string'? '?' + urlArgs: '';
define(['/api/config'], function (ApiConfig) {
var Module = {};
var isSupported = Module.isSupported = function () {
@@ -48,8 +41,8 @@
}
};
var DEFAULT_MAIN = '/customize/main-favicon.png' + urlArgs;
var DEFAULT_ALT = '/customize/alt-favicon.png' + urlArgs;
var DEFAULT_MAIN = '/customize/main-favicon.png?' + ApiConfig.requireConf.urlArgs;
var DEFAULT_ALT = '/customize/alt-favicon.png?' + ApiConfig.requireConf.urlArgs;
var createFavicon = function () {
console.log("creating favicon");
@@ -117,14 +110,6 @@
cancel: cancel,
};
};
return Module;
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = Mod();
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define(['/api/config'], Mod);
} else {
window.Visible = Mod();
}
}());
return Module;
});

View File

@@ -0,0 +1,27 @@
define([
'/api/config'
], function (ApiConfig) {
var out = {
// fix up locations so that relative urls work.
baseUrl: window.location.pathname,
paths: {
// jquery declares itself as literally "jquery" so it cannot be pulled by path :(
"jquery": "/bower_components/jquery/dist/jquery.min",
// json.sortify same
"json.sortify": "/bower_components/json.sortify/dist/JSON.sortify",
//"pdfjs-dist/build/pdf": "/bower_components/pdfjs-dist/build/pdf",
//"pdfjs-dist/build/pdf.worker": "/bower_components/pdfjs-dist/build/pdf.worker"
cm: '/bower_components/codemirror'
},
map: {
'*': {
'css': '/bower_components/require-css/css.js',
'less': '/common/RequireLess.js',
}
}
};
Object.keys(ApiConfig.requireConf).forEach(function (k) { out[k] = ApiConfig.requireConf[k]; });
return function () {
return JSON.parse(JSON.stringify(out));
};
});

30
www/common/sframe-boot.js Normal file
View File

@@ -0,0 +1,30 @@
// Stage 0, this gets cached which means we can't change it. boot2-sframe.js is changable.
// Note that this file is meant to be executed only inside of a sandbox iframe.
;(function () {
var req = JSON.parse(decodeURIComponent(window.location.hash.substring(1)));
req.cfg = req.cfg || {};
if (req.pfx) {
req.cfg.onNodeCreated = function (node /*, config, module, path*/) {
node.setAttribute('src', req.pfx + node.getAttribute('src'));
};
}
require.config(req.cfg);
var txid = Math.random().toString(16).replace('0.', '');
var intr;
var ready = function () {
intr = setInterval(function () {
if (typeof(txid) !== 'string') { return; }
window.parent.postMessage(JSON.stringify({ q: 'READY', txid: txid }), '*');
}, 1);
};
if (req.req) { require(req.req, ready); } else { ready(); }
var onReply = function (msg) {
var data = JSON.parse(msg.data);
if (data.txid !== txid) { return; }
clearInterval(intr);
txid = {};
window.removeEventListener('message', onReply);
require(['/common/sframe-boot2.js'], function () { });
};
window.addEventListener('message', onReply);
}());

View File

@@ -0,0 +1,35 @@
// This is stage 1, it can be changed but you must bump the version of the project.
// Note: This must only be loaded from inside of a sandbox-iframe.
define(['/common/requireconfig.js'], function (RequireConfig) {
require.config(RequireConfig());
// most of CryptPad breaks if you don't support isArray
if (!Array.isArray) {
Array.isArray = function(arg) { // CRYPTPAD_SHIM
return Object.prototype.toString.call(arg) === '[object Array]';
};
}
// In the event that someone clicks a link in the iframe, it's going to cause the iframe
// to navigate away from the pad which is going to be a mess. Instead we'll just reload
// the top level and then it will be simply that a link doesn't work properly.
window.onunload = function () {
window.parent.location.reload();
};
// Make sure anything which might have leaked to the localstorage is always cleaned up.
try { window.localStorage.clear(); } catch (e) { }
try { window.sessionStorage.clear(); } catch (e) { }
var mkFakeStore = function () {
var fakeStorage = {
getItem: function (k) { return fakeStorage[k]; },
setItem: function (k, v) { fakeStorage[k] = v; return v; }
};
return fakeStorage;
};
window.__defineGetter__('localStorage', function () { return mkFakeStore(); });
window.__defineGetter__('sessionStorage', function () { return mkFakeStore(); });
require([document.querySelector('script[data-bootload]').getAttribute('data-bootload')]);
});

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2014 XWiki SAS
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
define([
'/bower_components/chainpad/chainpad.dist.js'
], function () {
var ChainPad = window.ChainPad;
var module = { exports: {} };
var verbose = function (x) { console.log(x); };
verbose = function () {}; // comment out to enable verbose logging
module.exports.start = function (config) {
var onConnectionChange = config.onConnectionChange || function () { };
var onRemote = config.onRemote || function () { };
var onInit = config.onInit || function () { };
var onLocal = config.onLocal || function () { };
var setMyID = config.setMyID || function () { };
var onReady = config.onReady || function () { };
var userName = config.userName;
var initialState = config.initialState;
var transformFunction = config.transformFunction;
var validateContent = config.validateContent;
var avgSyncMilliseconds = config.avgSyncMilliseconds;
var logLevel = typeof(config.logLevel) !== 'undefined'? config.logLevel : 1;
var readOnly = config.readOnly || false;
var sframeChan = config.sframeChan;
var metadataMgr = config.metadataMgr;
config = undefined;
var chainpad;
var myID;
var isReady = false;
sframeChan.on('EV_RT_DISCONNECT', function () {
isReady = false;
onConnectionChange({ state: false });
});
sframeChan.on('EV_RT_CONNECT', function (content) {
//content.members.forEach(userList.onJoin);
myID = content.myID;
isReady = false;
if (chainpad) {
// it's a reconnect
onConnectionChange({ state: true, myId: myID });
return;
}
chainpad = ChainPad.create({
userName: userName,
initialState: initialState,
transformFunction: transformFunction,
validateContent: validateContent,
avgSyncMilliseconds: avgSyncMilliseconds,
logLevel: logLevel
});
chainpad.onMessage(function(message, cb) {
sframeChan.query('Q_RT_MESSAGE', message, cb);
});
chainpad.onPatch(function () {
onRemote({ realtime: chainpad });
});
onInit({
myID: myID,
realtime: chainpad,
readOnly: readOnly
});
});
sframeChan.on('Q_RT_MESSAGE', function (content, cb) {
if (isReady) {
onLocal(); // should be onBeforeMessage
}
chainpad.message(content);
cb('OK');
});
sframeChan.on('EV_RT_READY', function () {
if (isReady) { return; }
isReady = true;
chainpad.start();
setMyID({ myID: myID });
onReady({ realtime: chainpad });
});
return Object.freeze({
getMyID: function () { return myID; },
metadataMgr: metadataMgr
});
};
return Object.freeze(module.exports);
});

View File

@@ -0,0 +1,246 @@
/*
* Copyright 2014 XWiki SAS
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
define([], function () {
var USE_HISTORY = true;
var verbose = function (x) { console.log(x); };
verbose = function () {}; // comment out to enable verbose logging
var unBencode = function (str) { return str.replace(/^\d+:/, ''); };
var start = function (conf) {
var channel = conf.channel;
var Crypto = conf.crypto;
var validateKey = conf.validateKey;
var readOnly = conf.readOnly || false;
var network = conf.network;
var sframeChan = conf.sframeChan;
var onConnect = conf.onConnect || function () { };
conf = undefined;
var initializing = true;
var lastKnownHash;
var queue = [];
var messageFromInner = function (m, cb) { queue.push([ m, cb ]); };
sframeChan.on('Q_RT_MESSAGE', function (message, cb) {
messageFromInner(message, cb);
});
var onReady = function () {
// Trigger onReady only if not ready yet. This is important because the history keeper sends a direct
// message through "network" when it is synced, and it triggers onReady for each channel joined.
if (!initializing) { return; }
sframeChan.event('EV_RT_READY', null);
// we're fully synced
initializing = false;
};
// shim between chainpad and netflux
var msgIn = function (peerId, msg) {
msg = msg.replace(/^cp\|/, '');
try {
var decryptedMsg = Crypto.decrypt(msg, validateKey);
return decryptedMsg;
} catch (err) {
console.error(err);
return msg;
}
};
var msgOut = function (msg) {
if (readOnly) { return; }
try {
var cmsg = Crypto.encrypt(msg);
if (msg.indexOf('[4') === 0) { cmsg = 'cp|' + cmsg; }
return cmsg;
} catch (err) {
console.log(msg);
throw err;
}
};
var onMessage = function(peer, msg, wc, network, direct) {
// unpack the history keeper from the webchannel
var hk = network.historyKeeper;
if (direct && peer !== hk) {
return;
}
if (direct) {
var parsed = JSON.parse(msg);
if (parsed.validateKey && parsed.channel) {
if (parsed.channel === wc.id && !validateKey) {
validateKey = parsed.validateKey;
}
// We have to return even if it is not the current channel:
// we don't want to continue with other channels messages here
return;
}
if (parsed.state && parsed.state === 1 && parsed.channel) {
if (parsed.channel === wc.id) {
onReady(wc);
}
// We have to return even if it is not the current channel:
// we don't want to continue with other channels messages here
return;
}
}
// The history keeper is different for each channel :
// no need to check if the message is related to the current channel
if (peer === hk) {
// if the peer is the 'history keeper', extract their message
var parsed1 = JSON.parse(msg);
msg = parsed1[4];
// Check that this is a message for us
if (parsed1[3] !== wc.id) { return; }
}
lastKnownHash = msg.slice(0,64);
var message = msgIn(peer, msg);
verbose(message);
// slice off the bencoded header
// Why are we getting bencoded stuff to begin with?
// FIXME this shouldn't be necessary
message = unBencode(message);//.slice(message.indexOf(':[') + 1);
// pass the message into Chainpad
sframeChan.query('Q_RT_MESSAGE', message, function () { });
};
// We use an object to store the webchannel so that we don't have to push new handlers to chainpad
// and remove the old ones when reconnecting and keeping the same 'realtime' object
// See realtime.onMessage below: we call wc.bcast(...) but wc may change
var wcObject = {};
var onOpen = function(wc, network, firstConnection) {
wcObject.wc = wc;
channel = wc.id;
onConnect(wc);
onConnect = function () { };
// Add the existing peers in the userList
sframeChan.event('EV_RT_CONNECT', { myID: wc.myID, members: wc.members, readOnly: readOnly });
// Add the handlers to the WebChannel
wc.on('message', function (msg, sender) { //Channel msg
onMessage(sender, msg, wc, network);
});
wc.on('join', function (m) { sframeChan.event('EV_RT_JOIN', m); });
wc.on('leave', function (m) { sframeChan.event('EV_RT_LEAVE', m); });
if (firstConnection) {
// Sending a message...
messageFromInner = function(message, cb) {
// Filter messages sent by Chainpad to make it compatible with Netflux
message = msgOut(message);
if (message) {
// Do not remove wcObject, it allows us to use a new 'wc' without changing the handler if we
// want to keep the same chainpad (realtime) object
try {
wcObject.wc.bcast(message).then(function() {
cb();
}, function(err) {
// The message has not been sent, display the error.
console.error(err);
});
} catch (e) {
console.log(e);
// Just skip calling back and it will fail on the inside.
}
}
};
queue.forEach(function (arr) { messageFromInner(arr[0], arr[1]); });
}
// Get the channel history
if (USE_HISTORY) {
var hk;
wc.members.forEach(function (p) {
if (p.length === 16) { hk = p; }
});
network.historyKeeper = hk;
var msg = ['GET_HISTORY', wc.id];
// Add the validateKey if we are the channel creator and we have a validateKey
msg.push(validateKey);
msg.push(lastKnownHash);
if (hk) { network.sendto(hk, JSON.stringify(msg)); }
} else {
onReady(wc);
}
};
var isIntentionallyLeaving = false;
window.addEventListener("beforeunload", function () {
isIntentionallyLeaving = true;
});
var findChannelById = function (webChannels, channelId) {
var webChannel;
// Array.some terminates once a truthy value is returned
// best case is faster than forEach, though webchannel arrays seem
// to consistently have a length of 1
webChannels.some(function(chan) {
if(chan.id === channelId) { webChannel = chan; return true;}
});
return webChannel;
};
var connectTo = function (network, firstConnection) {
// join the netflux network, promise to handle opening of the channel
network.join(channel || null).then(function(wc) {
onOpen(wc, network, firstConnection);
}, function(error) {
console.error(error);
});
};
network.on('disconnect', function (reason) {
console.log('disconnect');
if (isIntentionallyLeaving) { return; }
if (reason === "network.disconnect() called") { return; }
sframeChan.event('EV_RT_DISCONNECT');
});
network.on('reconnect', function () {
initializing = true;
connectTo(network, false);
});
network.on('message', function (msg, sender) { // Direct message
var wchan = findChannelById(network.webChannels, channel);
if (wchan) {
onMessage(sender, msg, wchan, network, true);
}
});
connectTo(network, true);
};
return {
start: function (config) {
config.sframeChan.whenReg('EV_RT_READY', function () {
start(config);
});
}
};
});

View File

@@ -0,0 +1,141 @@
// This file provides the API for the channel for talking to and from the sandbox iframe.
define([
'/common/sframe-protocol.js'
], function (SFrameProtocol) {
var mkTxid = function () {
return Math.random().toString(16).replace('0.', '') + Math.random().toString(16).replace('0.', '');
};
var create = function (ow, cb) {
var otherWindow;
var handlers = {};
var queries = {};
// list of handlers which are registered from the other side...
var insideHandlers = [];
var callWhenRegistered = {};
var chan = {};
// Send a query. channel.query('Q_SOMETHING', { args: "whatever" }, function (reply) { ... });
chan.query = function (q, content, cb) {
if (!otherWindow) { throw new Error('not yet initialized'); }
if (!SFrameProtocol[q]) {
throw new Error('please only make queries are defined in sframe-protocol.js');
}
var txid = mkTxid();
var timeout = setTimeout(function () {
delete queries[txid];
console.log("Timeout making query " + q);
}, 30000);
queries[txid] = function (data, msg) {
clearTimeout(timeout);
delete queries[txid];
cb(undefined, data.content, msg);
};
otherWindow.postMessage(JSON.stringify({
txid: txid,
content: content,
q: q
}), '*');
};
// Fire an event. channel.event('EV_SOMETHING', { args: "whatever" });
var event = chan.event = function (e, content) {
if (!otherWindow) { throw new Error('not yet initialized'); }
if (!SFrameProtocol[e]) {
throw new Error('please only fire events that are defined in sframe-protocol.js');
}
if (e.indexOf('EV_') !== 0) {
throw new Error('please only use events (starting with EV_) for event messages');
}
otherWindow.postMessage(JSON.stringify({ content: content, q: e }), '*');
};
// Be notified on query or event. channel.on('EV_SOMETHING', function (args, reply) { ... });
// If the type is a query, your handler will be invoked with a reply function that takes
// one argument (the content to reply with).
chan.on = function (queryType, handler, quiet) {
if (!otherWindow && !quiet) { throw new Error('not yet initialized'); }
if (!SFrameProtocol[queryType]) {
throw new Error('please only register handlers which are defined in sframe-protocol.js');
}
(handlers[queryType] = handlers[queryType] || []).push(function (data, msg) {
handler(data.content, function (replyContent) {
if (queryType.indexOf('Q_') !== 0) { throw new Error("replies to events are invalid"); }
msg.source.postMessage(JSON.stringify({
txid: data.txid,
content: replyContent
}), '*');
}, msg);
});
if (!quiet) {
event('EV_REGISTER_HANDLER', queryType);
}
};
// If a particular handler is registered, call the callback immediately, otherwise it will be called
// when that handler is first registered.
// channel.whenReg('Q_SOMETHING', function () { ...query Q_SOMETHING?... });
chan.whenReg = function (queryType, cb, always) {
if (!otherWindow) { throw new Error('not yet initialized'); }
if (!SFrameProtocol[queryType]) {
throw new Error('please only register handlers which are defined in sframe-protocol.js');
}
var reg = always;
if (insideHandlers.indexOf(queryType) > -1) {
cb();
} else {
reg = true;
}
if (reg) {
(callWhenRegistered[queryType] = callWhenRegistered[queryType] || []).push(cb);
}
};
// Same as whenReg except it will invoke every time there is another registration, not just once.
chan.onReg = function (queryType, cb) { chan.whenReg(queryType, cb, true); };
chan.on('EV_REGISTER_HANDLER', function (content) {
if (callWhenRegistered[content]) {
callWhenRegistered[content].forEach(function (f) { f(); });
delete callWhenRegistered[content];
}
insideHandlers.push(content);
}, true);
var txid;
window.addEventListener('message', function (msg) {
var data = JSON.parse(msg.data);
if (ow !== msg.source) {
console.log("DROP Message from unexpected source");
console.log(msg);
} else if (!otherWindow) {
otherWindow = ow;
ow.postMessage(JSON.stringify({ txid: data.txid }), '*');
cb(chan);
} else if (typeof(data.q) === 'string' && handlers[data.q]) {
handlers[data.q].forEach(function (f) {
f(data || JSON.parse(msg.data), msg);
data = undefined;
});
} else if (typeof(data.q) === 'undefined' && queries[data.txid]) {
queries[data.txid](data, msg);
} else if (data.txid === txid) {
// stray message from init
return;
} else {
console.log("DROP Unhandled message");
console.log(msg);
}
});
if (window !== window.top) {
// we're in the sandbox
otherWindow = ow;
cb(chan);
}
};
return { create: create };
});

View File

@@ -0,0 +1,223 @@
define([
'jquery',
'/bower_components/chainpad-json-validator/json-ot.js',
'/bower_components/chainpad/chainpad.dist.js',
], function ($, JsonOT) {
var ChainPad = window.ChainPad;
var History = {};
var getStates = function (rt) {
var states = [];
var b = rt.getAuthBlock();
if (b) { states.unshift(b); }
while (b.getParent()) {
b = b.getParent();
states.unshift(b);
}
return states;
};
var loadHistory = function (config, common, cb) {
var createRealtime = function () {
return ChainPad.create({
userName: 'history',
initialState: '',
transformFunction: JsonOT.validate,
logLevel: 0,
noPrune: true
});
};
var realtime = createRealtime();
History.readOnly = common.getMetadataMgr().getPrivateData().readOnly;
var to = window.setTimeout(function () {
cb('[GET_FULL_HISTORY_TIMEOUT]');
}, 30000);
common.getFullHistory(realtime, function () {
window.clearTimeout(to);
cb(null, realtime);
});
};
History.create = function (common, config) {
if (!config.$toolbar) { return void console.error("config.$toolbar is undefined");}
if (History.loading) { return void console.error("History is already being loaded..."); }
History.loading = true;
var $toolbar = config.$toolbar;
if (!config.applyVal || !config.setHistory || !config.onLocal || !config.onRemote) {
throw new Error("Missing config element: applyVal, onLocal, onRemote, setHistory");
}
// config.setHistory(bool, bool)
// - bool1: history value
// - bool2: reset old content?
var render = function (val) {
if (typeof val === "undefined") { return; }
try {
config.applyVal(val);
} catch (e) {
// Probably a parse error
console.error(e);
}
};
var onClose = function () { config.setHistory(false, true); };
var onRevert = function () {
config.setHistory(false, false);
config.onLocal();
config.onRemote();
};
var onReady = function () {
config.setHistory(true);
};
var Messages = common.Messages;
var Cryptpad = common.getCryptpadCommon();
var realtime;
var states = [];
var c = states.length - 1;
var $hist = $toolbar.find('.cryptpad-toolbar-history');
var $left = $toolbar.find('.cryptpad-toolbar-leftside');
var $right = $toolbar.find('.cryptpad-toolbar-rightside');
var $cke = $toolbar.find('.cke_toolbox_main');
$hist.html('').show();
$left.hide();
$right.hide();
$cke.hide();
Cryptpad.spinner($hist).get().show();
var onUpdate;
var update = function () {
if (!realtime) { return []; }
states = getStates(realtime);
if (typeof onUpdate === "function") { onUpdate(); }
return states;
};
// Get the content of the selected version, and change the version number
var get = function (i) {
i = parseInt(i);
if (isNaN(i)) { return; }
if (i < 0) { i = 0; }
if (i > states.length - 1) { i = states.length - 1; }
var val = states[i].getContent().doc;
c = i;
if (typeof onUpdate === "function") { onUpdate(); }
$hist.find('.next, .previous').css('visibility', '');
if (c === states.length - 1) { $hist.find('.next').css('visibility', 'hidden'); }
if (c === 0) { $hist.find('.previous').css('visibility', 'hidden'); }
return val || '';
};
var getNext = function (step) {
return typeof step === "number" ? get(c + step) : get(c + 1);
};
var getPrevious = function (step) {
return typeof step === "number" ? get(c - step) : get(c - 1);
};
// Create the history toolbar
var display = function () {
$hist.html('');
var $prev =$('<button>', {
'class': 'previous fa fa-step-backward buttonPrimary',
title: Messages.history_prev
}).appendTo($hist);
var $nav = $('<div>', {'class': 'goto'}).appendTo($hist);
var $next = $('<button>', {
'class': 'next fa fa-step-forward buttonPrimary',
title: Messages.history_next
}).appendTo($hist);
$('<label>').text(Messages.history_version).appendTo($nav);
var $cur = $('<input>', {
'class' : 'gotoInput',
'type' : 'number',
'min' : '1',
'max' : states.length
}).val(c + 1).appendTo($nav).mousedown(function (e) {
// stopPropagation because the event would be cancelled by the dropdown menus
e.stopPropagation();
});
var $label2 = $('<label>').text(' / '+ states.length).appendTo($nav);
$('<br>').appendTo($nav);
var $close = $('<button>', {
'class':'closeHistory',
title: Messages.history_closeTitle
}).text(Messages.history_closeTitle).appendTo($nav);
var $rev = $('<button>', {
'class':'revertHistory buttonSuccess',
title: Messages.history_restoreTitle
}).text(Messages.history_restore).appendTo($nav);
if (History.readOnly) { $rev.hide(); }
onUpdate = function () {
$cur.attr('max', states.length);
$cur.val(c+1);
$label2.text(' / ' + states.length);
};
var close = function () {
$hist.hide();
$left.show();
$right.show();
$cke.show();
};
// Buttons actions
$prev.click(function () { render(getPrevious()); });
$next.click(function () { render(getNext()); });
$cur.keydown(function (e) {
var p = function () { e.preventDefault(); };
if (e.which === 13) { p(); return render( get($cur.val() - 1) ); } // Enter
if ([37, 40].indexOf(e.which) >= 0) { p(); return render(getPrevious()); } // Left
if ([38, 39].indexOf(e.which) >= 0) { p(); return render(getNext()); } // Right
if (e.which === 33) { p(); return render(getNext(10)); } // PageUp
if (e.which === 34) { p(); return render(getPrevious(10)); } // PageUp
if (e.which === 27) { p(); $close.click(); }
}).keyup(function (e) { e.stopPropagation(); }).focus();
$cur.on('change', function () {
render( get($cur.val() - 1) );
});
$close.click(function () {
states = [];
close();
onClose();
});
$rev.click(function () {
Cryptpad.confirm(Messages.history_restorePrompt, function (yes) {
if (!yes) { return; }
close();
onRevert();
Cryptpad.log(Messages.history_restoreDone);
});
});
// Display the latest content
render(get(c));
};
// Load all the history messages into a new chainpad object
loadHistory(config, common, function (err, newRt) {
History.loading = false;
if (err) { throw new Error(err); }
realtime = newRt;
update();
c = states.length - 1;
display();
onReady();
});
};
return History;
});

View File

@@ -0,0 +1,264 @@
define([
'jquery',
'/common/cryptpad-common.js',
'/common/media-tag.js',
], function ($, Cryptpad, MediaTag) {
var UI = {};
var Messages = Cryptpad.Messages;
/**
* Requirements from cryptpad-common.js
* getFileSize
* - hrefToHexChannelId
* displayAvatar
* - getFirstEmojiOrCharacter
* - parsePadUrl
* - getSecrets
* - base64ToHex
* - getBlobPathFromHex
* - bytesToMegabytes
* createUserAdminMenu
* - fixHTML
* - createDropdown
*/
UI.getFileSize = function (Common, href, cb) {
var channelId = Cryptpad.hrefToHexChannelId(href);
Common.sendAnonRpcMsg("GET_FILE_SIZE", channelId, function (data) {
if (!data) { return void cb("No response"); }
if (data.error) { return void cb(data.error); }
if (data.response && data.response.length && typeof(data.response[0]) === 'number') {
return void cb(void 0, data.response[0]);
} else {
cb('INVALID_RESPONSE');
}
});
};
UI.displayAvatar = function (Common, $container, href, name, cb) {
var MutationObserver = window.MutationObserver;
var displayDefault = function () {
var text = Cryptpad.getFirstEmojiOrCharacter(name);
var $avatar = $('<span>', {'class': 'default'}).text(text);
$container.append($avatar);
if (cb) { cb(); }
};
if (!href) { return void displayDefault(); }
var parsed = Cryptpad.parsePadUrl(href);
var secret = Cryptpad.getSecrets('file', parsed.hash);
if (secret.keys && secret.channel) {
var cryptKey = secret.keys && secret.keys.fileKeyStr;
var hexFileName = Cryptpad.base64ToHex(secret.channel);
var src = Cryptpad.getBlobPathFromHex(hexFileName);
UI.getFileSize(Common, href, function (e, data) {
if (e) {
displayDefault();
return void console.error(e);
}
if (typeof data !== "number") { return void displayDefault(); }
if (Cryptpad.bytesToMegabytes(data) > 0.5) { return void displayDefault(); }
var $img = $('<media-tag>').appendTo($container);
$img.attr('src', src);
$img.attr('data-crypto-key', 'cryptpad:' + cryptKey);
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'childList' && mutation.addedNodes.length) {
if (mutation.addedNodes.length > 1 ||
mutation.addedNodes[0].nodeName !== 'IMG') {
$img.remove();
return void displayDefault();
}
var $image = $img.find('img');
var onLoad = function () {
var img = new Image();
img.onload = function () {
var w = img.width;
var h = img.height;
if (w>h) {
$image.css('max-height', '100%');
$img.css('flex-direction', 'column');
if (cb) { cb($img); }
return;
}
$image.css('max-width', '100%');
$img.css('flex-direction', 'row');
if (cb) { cb($img); }
};
img.src = $image.attr('src');
};
if ($image[0].complete) { onLoad(); }
$image.on('load', onLoad);
}
});
});
observer.observe($img[0], {
attributes: false,
childList: true,
characterData: false
});
MediaTag($img[0]);
});
}
};
UI.createUserAdminMenu = function (config) {
var Common = config.Common;
var metadataMgr = config.metadataMgr;
var displayNameCls = config.displayNameCls || 'displayName';
var $displayedName = $('<span>', {'class': displayNameCls});
var accountName = metadataMgr.getPrivateData().accountName;
var origin = metadataMgr.getPrivateData().origin;
var padType = metadataMgr.getMetadata().type;
var $userName = $('<span>', {'class': 'userDisplayName'});
var options = [];
if (config.displayNameCls) {
var $userAdminContent = $('<p>');
if (accountName) {
var $userAccount = $('<span>', {'class': 'userAccount'}).append(Messages.user_accountName + ': ' + Cryptpad.fixHTML(accountName));
$userAdminContent.append($userAccount);
$userAdminContent.append($('<br>'));
}
if (config.displayName) {
// Hide "Display name:" in read only mode
$userName.append(Messages.user_displayName + ': ');
$userName.append($displayedName);
}
$userAdminContent.append($userName);
options.push({
tag: 'p',
attributes: {'class': 'accountData'},
content: $userAdminContent.html()
});
}
if (padType !== 'drive') {
options.push({
tag: 'a',
attributes: {
'target': '_blank',
'href': origin+'/drive/'
},
content: Messages.login_accessDrive
});
}
// Add the change display name button if not in read only mode
if (config.changeNameButtonCls && config.displayChangeName) {
options.push({
tag: 'a',
attributes: {'class': config.changeNameButtonCls},
content: Messages.user_rename
});
}
if (accountName) {
options.push({
tag: 'a',
attributes: {'class': 'profile'},
content: Messages.profileButton
});
}
if (padType !== 'settings') {
options.push({
tag: 'a',
attributes: {'class': 'settings'},
content: Messages.settingsButton
});
}
// Add login or logout button depending on the current status
if (accountName) {
options.push({
tag: 'a',
attributes: {'class': 'logout'},
content: Messages.logoutButton
});
} else {
options.push({
tag: 'a',
attributes: {'class': 'login'},
content: Messages.login_login
});
options.push({
tag: 'a',
attributes: {'class': 'register'},
content: Messages.login_register
});
}
var $icon = $('<span>', {'class': 'fa fa-user-secret'});
//var $userbig = $('<span>', {'class': 'big'}).append($displayedName.clone());
var $userButton = $('<div>').append($icon);//.append($userbig);
if (accountName) {
$userButton = $('<div>').append(accountName);
}
/*if (account && config.displayNameCls) {
$userbig.append($('<span>', {'class': 'account-name'}).text('(' + accountName + ')'));
} else if (account) {
// If no display name, do not display the parentheses
$userbig.append($('<span>', {'class': 'account-name'}).text(accountName));
}*/
var dropdownConfigUser = {
text: $userButton.html(), // Button initial text
options: options, // Entries displayed in the menu
left: true, // Open to the left of the button
container: config.$initBlock, // optional
feedback: "USER_ADMIN",
};
var $userAdmin = Cryptpad.createDropdown(dropdownConfigUser);
var $displayName = $userAdmin.find('.'+displayNameCls);
var $avatar = $userAdmin.find('.buttonTitle');
var updateButton = function () {
var myData = metadataMgr.getMetadata().users[metadataMgr.getNetfluxId()];
if (!myData) { return; }
var newName = myData.name;
var url = myData.avatar;
$displayName.text(newName || Messages.anonymous);
if (accountName) {
$avatar.html('');
UI.displayAvatar(Common, $avatar, url, newName, function ($img) {
if ($img) {
$userAdmin.find('button').addClass('avatar');
}
});
}
};
metadataMgr.onChange(updateButton);
updateButton();
$userAdmin.find('a.logout').click(function () {
Common.logout(function () {
window.top.location = origin+'/';
});
});
$userAdmin.find('a.settings').click(function () {
if (padType) {
window.open(origin+'/settings/');
} else {
window.top.location = origin+'/settings/';
}
});
$userAdmin.find('a.profile').click(function () {
if (padType) {
window.open(origin+'/profile/');
} else {
window.top.location = origin+'/profile/';
}
});
$userAdmin.find('a.login').click(function () {
Common.setLoginRedirect(function () {
window.top.location = origin+'/login/';
});
});
$userAdmin.find('a.register').click(function () {
Common.setLoginRedirect(function () {
window.top.location = origin+'/register/';
});
});
return $userAdmin;
};
return UI;
});

View File

@@ -0,0 +1,83 @@
define(['jquery'], function ($) {
var module = {};
module.create = function (cfg, onLocal, Common, metadataMgr) {
var exp = {};
exp.defaultTitle = Common.getDefaultTitle();
exp.title = document.title;
cfg = cfg || {};
var getHeadingText = cfg.getHeadingText || function () { return; };
/* var updateLocalTitle = function (newTitle) {
console.error(newTitle);
exp.title = newTitle;
onLocal();
if (typeof cfg.updateLocalTitle === "function") {
cfg.updateLocalTitle(newTitle);
} else {
document.title = newTitle;
}
};*/
var $title;
exp.setToolbar = function (toolbar) {
$title = toolbar && toolbar.title;
};
exp.getTitle = function () { return exp.title; };
var isDefaultTitle = exp.isDefaultTitle = function (){return exp.title === exp.defaultTitle;};
var suggestTitle = exp.suggestTitle = function (fallback) {
if (isDefaultTitle()) {
return getHeadingText() || fallback || "";
} else {
var title = metadataMgr.getMetadata().title;
return title || getHeadingText() || exp.defaultTitle;
}
};
/*var renameCb = function (err, newTitle) {
if (err) { return; }
onLocal();
//updateLocalTitle(newTitle);
};*/
// update title: href is optional; if not specified, we use window.location.href
exp.updateTitle = function (newTitle, cb) {
cb = cb || $.noop;
if (newTitle === exp.title) { return; }
Common.updateTitle(newTitle, cb);
};
// TODO not needed?
/*exp.updateDefaultTitle = function (newDefaultTitle) {
exp.defaultTitle = newDefaultTitle;
if (!$title) { return; }
$title.find('input').attr("placeholder", exp.defaultTitle);
};*/
metadataMgr.onChange(function () {
var md = metadataMgr.getMetadata();
$title.find('span.title').text(md.title || md.defaultTitle);
$title.find('input').val(md.title || md.defaultTitle);
//exp.updateTitle(md.title || md.defaultTitle);
});
exp.getTitleConfig = function () {
return {
updateTitle: exp.updateTitle,
suggestName: suggestTitle,
defaultName: exp.defaultTitle
};
};
return exp;
};
return module;
});

303
www/common/sframe-common.js Normal file
View File

@@ -0,0 +1,303 @@
define([
'jquery',
'/bower_components/nthen/index.js',
'/customize/messages.js',
'/common/sframe-chainpad-netflux-inner.js',
'/common/sframe-channel.js',
'/common/sframe-common-title.js',
'/common/sframe-common-interface.js',
'/common/sframe-common-history.js',
'/common/metadata-manager.js',
'/customize/application_config.js',
'/common/cryptpad-common.js',
'/common/common-realtime.js'
], function ($, nThen, Messages, CpNfInner, SFrameChannel, Title, UI, History, MetadataMgr,
AppConfig, Cryptpad, CommonRealtime) {
// Chainpad Netflux Inner
var funcs = {};
var ctx = {};
funcs.Messages = Messages;
funcs.startRealtime = function (options) {
if (ctx.cpNfInner) { return ctx.cpNfInner; }
options.sframeChan = ctx.sframeChan;
options.metadataMgr = ctx.metadataMgr;
ctx.cpNfInner = CpNfInner.start(options);
ctx.cpNfInner.metadataMgr.onChangeLazy(options.onLocal);
return ctx.cpNfInner;
};
funcs.getMetadataMgr = function () {
return ctx.metadataMgr;
};
funcs.getCryptpadCommon = function () {
return Cryptpad;
};
var isLoggedIn = funcs.isLoggedIn = function () {
if (!ctx.cpNfInner) { throw new Error("cpNfInner is not ready!"); }
return ctx.cpNfInner.metadataMgr.getPrivateData().accountName;
};
var titleUpdated;
funcs.updateTitle = function (title, cb) {
ctx.metadataMgr.updateTitle(title);
titleUpdated = cb;
};
// UI
funcs.createUserAdminMenu = UI.createUserAdminMenu;
funcs.displayAvatar = UI.displayAvatar;
// History
funcs.getHistory = function (config) { return History.create(funcs, config); };
// Title module
funcs.createTitle = Title.create;
funcs.getDefaultTitle = function () {
if (!ctx.cpNfInner) { throw new Error("cpNfInner is not ready!"); }
return ctx.cpNfInner.metadataMgr.getMetadata().defaultTitle;
};
funcs.setDisplayName = function (name, cb) {
ctx.sframeChan.query('Q_SETTINGS_SET_DISPLAY_NAME', name, function (err) {
if (cb) { cb(err); }
});
};
funcs.logout = function (cb) {
ctx.sframeChan.query('Q_LOGOUT', null, function (err) {
if (cb) { cb(err); }
});
};
funcs.setLoginRedirect = function (cb) {
ctx.sframeChan.query('Q_SET_LOGIN_REDIRECT', null, function (err) {
if (cb) { cb(err); }
});
};
funcs.sendAnonRpcMsg = function (msg, content, cb) {
ctx.sframeChan.query('Q_ANON_RPC_MESSAGE', {
msg: msg,
content: content
}, function (err, data) {
if (cb) { cb(data); }
});
};
funcs.isOverPinLimit = function (cb) {
ctx.sframeChan.query('Q_GET_PIN_LIMIT_STATUS', null, function (err, data) {
cb(data.error, data.overLimit, data.limits);
});
};
funcs.getFullHistory = function (realtime, cb) {
ctx.sframeChan.on('EV_RT_HIST_MESSAGE', function (content) {
realtime.message(content);
});
ctx.sframeChan.query('Q_GET_FULL_HISTORY', null, cb);
};
funcs.feedback = function (action, force) {
if (force !== true) {
if (!action) { return; }
try {
if (!ctx.metadataMgr.getPrivateData().feedbackAllowed) { return; }
} catch (e) { return void console.error(e); }
}
var randomToken = Math.random().toString(16).replace(/0./, '');
//var origin = ctx.metadataMgr.getPrivateData().origin;
var href = /*origin +*/ '/common/feedback.html?' + action + '=' + randomToken;
$.ajax({
type: "HEAD",
url: href,
});
};
var prepareFeedback = function (key) {
if (typeof(key) !== 'string') { return $.noop; }
var type = ctx.metadataMgr.getMetadata().type;
return function () {
funcs.feedback((key + (type? '_' + type: '')).toUpperCase());
};
};
// BUTTONS
var isStrongestStored = function () {
var data = ctx.metadataMgr.getPrivateData();
return !data.readOnly || !data.availableHashes.editHash;
};
funcs.createButton = function (type, rightside, data, callback) {
var button;
var size = "17px";
switch (type) {
case 'export':
button = $('<button>', {
'class': 'fa fa-download',
title: Messages.exportButtonTitle,
}).append($('<span>', {'class': 'drawer'}).text(Messages.exportButton));
button.click(prepareFeedback(type));
if (callback) {
button.click(callback);
}
break;
case 'import':
button = $('<button>', {
'class': 'fa fa-upload',
title: Messages.importButtonTitle,
}).append($('<span>', {'class': 'drawer'}).text(Messages.importButton));
if (callback) {
button
.click(prepareFeedback(type))
.click(Cryptpad.importContent('text/plain', function (content, file) {
callback(content, file);
}, {accept: data ? data.accept : undefined}));
}
break;
case 'template':
if (!AppConfig.enableTemplates) { return; }
button = $('<button>', {
title: Messages.saveTemplateButton,
}).append($('<span>', {'class':'fa fa-bookmark', style: 'font:'+size+' FontAwesome'}));
if (data.rt) {
button
.click(function () {
var title = data.getTitle() || document.title;
var todo = function (val) {
if (typeof(val) !== "string") { return; }
var toSave = data.rt.getUserDoc();
if (val.trim()) {
val = val.trim();
title = val;
try {
var parsed = JSON.parse(toSave);
var meta;
if (Array.isArray(parsed) && typeof(parsed[3]) === "object") {
meta = parsed[3].metadata; // pad
} else if (parsed.info) {
meta = parsed.info; // poll
} else {
meta = parsed.metadata;
}
if (typeof(meta) === "object") {
meta.title = val;
meta.defaultTitle = val;
delete meta.users;
}
toSave = JSON.stringify(parsed);
} catch(e) {
console.error("Parse error while setting the title", e);
}
}
ctx.sframeChan.query('Q_SAVE_AS_TEMPLATE', {
title: title,
toSave: toSave
}, function () {
Cryptpad.alert(Messages.templateSaved);
funcs.feedback('TEMPLATE_CREATED');
});
};
Cryptpad.prompt(Messages.saveTemplatePrompt, title, todo);
});
}
break;
case 'forget':
button = $('<button>', {
id: 'cryptpad-forget',
title: Messages.forgetButtonTitle,
'class': "fa fa-trash cryptpad-forget",
style: 'font:'+size+' FontAwesome'
});
if (!isStrongestStored()) {
button.addClass('hidden');
}
if (callback) {
button
.click(prepareFeedback(type))
.click(function() {
var msg = isLoggedIn() ? Messages.forgetPrompt : Messages.fm_removePermanentlyDialog;
Cryptpad.confirm(msg, function (yes) {
if (!yes) { return; }
ctx.sframeChan.query('Q_MOVE_TO_TRASH', null, function (err) {
if (err) { return void callback(err); }
var cMsg = isLoggedIn() ? Messages.movedToTrash : Messages.deleted;
Cryptpad.alert(cMsg, undefined, true);
callback();
return;
});
});
});
}
break;
case 'history':
if (!AppConfig.enableHistory) {
button = $('<span>');
break;
}
button = $('<button>', {
title: Messages.historyButton,
'class': "fa fa-history history",
}).append($('<span>', {'class': 'drawer'}).text(Messages.historyText));
if (data.histConfig) {
button
.click(prepareFeedback(type))
.on('click', function () {
funcs.getHistory(data.histConfig);
});
}
break;
case 'more':
button = $('<button>', {
title: Messages.moreActions || 'TODO',
'class': "drawer-button fa fa-ellipsis-h",
style: 'font:'+size+' FontAwesome'
});
break;
default:
button = $('<button>', {
'class': "fa fa-question",
style: 'font:'+size+' FontAwesome'
})
.click(prepareFeedback(type));
}
if (rightside) {
button.addClass('rightside-button');
}
return button;
};
/* funcs.storeLinkToClipboard = function (readOnly, cb) {
ctx.sframeChan.query('Q_STORE_LINK_TO_CLIPBOARD', readOnly, function (err) {
if (cb) { cb(err); }
});
};
*/
Object.freeze(funcs);
return { create: function (cb) {
nThen(function (waitFor) {
SFrameChannel.create(window.top, waitFor(function (sfc) { ctx.sframeChan = sfc; }));
// CpNfInner.start() should be here....
}).nThen(function () {
ctx.metadataMgr = MetadataMgr.create(ctx.sframeChan);
ctx.metadataMgr.onTitleChange(function (title) {
ctx.sframeChan.query('Q_SET_PAD_TITLE_IN_DRIVE', title, function (err) {
if (err) { return; }
if (titleUpdated) { titleUpdated(undefined, title); }
});
});
ctx.sframeChan.on('EV_RT_CONNECT', function () { CommonRealtime.setConnectionState(true); });
ctx.sframeChan.on('EV_RT_DISCONNECT', function () { CommonRealtime.setConnectionState(false); });
cb(funcs);
});
} };
});

View File

@@ -0,0 +1,77 @@
// This file defines all of the RPC calls which are used between the inner and outer iframe.
// Define *querys* (which expect a response) using Q_<query name>
// Define *events* (which expect no response) using EV_<event name>
// Please document the queries and events you create, and please please avoid making generic
// "do stuff" events/queries which are used for many different things because it makes the
// protocol unclear.
//
// WARNING: At this point, this protocol is still EXPERIMENTAL. This is not it's final form.
// We need to define protocol one piece at a time and then when we are satisfied that we
// fully understand the problem, we will define the *right* protocol and this file will be dynomited.
//
define({
// When the iframe first launches, this query is sent repeatedly by the controller
// to wait for it to awake and give it the requirejs config to use.
'Q_INIT': true,
// When either the outside or inside registers a query handler, this is sent.
'EV_REGISTER_HANDLER': true,
// Realtime events called from the outside.
// When someone joins the pad, argument is a string with their netflux id.
'EV_RT_JOIN': true,
// When someone leaves the pad, argument is a string with their netflux id.
'EV_RT_LEAVE': true,
// When you have been disconnected, no arguments.
'EV_RT_DISCONNECT': true,
// When you have connected, argument is an object with myID: string, members: list, readOnly: boolean.
'EV_RT_CONNECT': true,
// Called after the history is finished synchronizing, no arguments.
'EV_RT_READY': true,
// Called from both outside and inside, argument is a (string) chainpad message.
'Q_RT_MESSAGE': true,
// Called from the outside, this informs the inside whenever the user's data has been changed.
// The argument is the object representing the content of the user profile minus the netfluxID
// which changes per-reconnect.
'EV_METADATA_UPDATE': true,
// Takes one argument only, the title to set for the CURRENT pad which the user is looking at.
// This changes the pad title in drive ONLY, the pad title needs to be changed inside of the
// iframe and synchronized with the other users. This will not trigger a EV_METADATA_UPDATE
// because the metadata contained in EV_METADATA_UPDATE does not contain the pad title.
'Q_SET_PAD_TITLE_IN_DRIVE': true,
// Update the user's display-name which will be shown to contacts and people in the same pads.
'Q_SETTINGS_SET_DISPLAY_NAME': true,
// Log the user out in all the tabs
'Q_LOGOUT': true,
// When moving to the login or register page from a pad, we need to redirect to that pad at the
// end of the login process. This query set the current href to the sessionStorage.
'Q_SET_LOGIN_REDIRECT': true,
// Store the editing or readonly link of the current pad to the clipboard (share button).
'Q_STORE_LINK_TO_CLIPBOARD': true,
// Use anonymous rpc from inside the iframe (for avatars & pin usage).
'Q_ANON_RPC_MESSAGE': true,
// Check the pin limit to determine if we can store the pad in the drive or if we should.
// display a warning
'Q_GET_PIN_LIMIT_STATUS': true,
// Move a pad to the trash when using the forget button.
'Q_MOVE_TO_TRASH': true,
// Request the full history from the server when the users clicks on the history button.
// Callback is called when the FULL_HISTORY_END message is received in the outside.
'Q_GET_FULL_HISTORY': true,
// When a (full) history message is received from the server.
'EV_RT_HIST_MESSAGE': true,
// Save a pad as a template using the toolbar button
'Q_SAVE_AS_TEMPLATE': true,
});

999
www/common/toolbar3.js Normal file
View File

@@ -0,0 +1,999 @@
define([
'jquery',
'/customize/application_config.js',
'/api/config',
], function ($, Config, ApiConfig) {
var Messages = {};
var Cryptpad;
var Common;
var Bar = {
constants: {},
};
var SPINNER_DISAPPEAR_TIME = 1000;
// Toolbar parts
var TOOLBAR_CLS = Bar.constants.toolbar = 'cryptpad-toolbar';
var TOP_CLS = Bar.constants.top = 'cryptpad-toolbar-top';
var LEFTSIDE_CLS = Bar.constants.leftside = 'cryptpad-toolbar-leftside';
var RIGHTSIDE_CLS = Bar.constants.rightside = 'cryptpad-toolbar-rightside';
var DRAWER_CLS = Bar.constants.drawer = 'drawer-content';
var HISTORY_CLS = Bar.constants.history = 'cryptpad-toolbar-history';
// Userlist
var USERLIST_CLS = Bar.constants.userlist = "cryptpad-dropdown-users";
var EDITSHARE_CLS = Bar.constants.editShare = "cryptpad-dropdown-editShare";
var VIEWSHARE_CLS = Bar.constants.viewShare = "cryptpad-dropdown-viewShare";
var SHARE_CLS = Bar.constants.viewShare = "cryptpad-dropdown-share";
// Top parts
var USER_CLS = Bar.constants.userAdmin = "cryptpad-user";
var SPINNER_CLS = Bar.constants.spinner = 'cryptpad-spinner';
var LIMIT_CLS = Bar.constants.lag = 'cryptpad-limit';
var TITLE_CLS = Bar.constants.title = "cryptpad-title";
var NEWPAD_CLS = Bar.constants.newpad = "cryptpad-new";
// User admin menu
var USERADMIN_CLS = Bar.constants.user = 'cryptpad-user-dropdown';
var USERNAME_CLS = Bar.constants.username = 'cryptpad-toolbar-username';
/*var READONLY_CLS = */Bar.constants.readonly = 'cryptpad-readonly';
var USERBUTTON_CLS = Bar.constants.changeUsername = "cryptpad-change-username";
// Create the toolbar element
var uid = function () {
return 'cryptpad-uid-' + String(Math.random()).substring(2);
};
var createRealtimeToolbar = function (config) {
if (!config.$container) { return; }
var $container = config.$container;
var $toolbar = $('<div>', {
'class': TOOLBAR_CLS,
id: uid(),
});
// TODO iframe
// var parsed = Cryptpad.parsePadUrl(window.location.href);
var parsed = { type:'pad' };
if (typeof parsed.type === "string") {
config.$container.parents('body').addClass('app-' + parsed.type);
}
var $topContainer = $('<div>', {'class': TOP_CLS});
$('<span>', {'class': 'filler'}).appendTo($topContainer);
var $userContainer = $('<span>', {
'class': USER_CLS
}).appendTo($topContainer);
$('<span>', {'class': LIMIT_CLS}).hide().appendTo($userContainer);
$('<span>', {'class': NEWPAD_CLS + ' dropdown-bar'}).hide().appendTo($userContainer);
$('<span>', {'class': USERADMIN_CLS + ' dropdown-bar'}).hide().appendTo($userContainer);
$toolbar.append($topContainer)
.append($('<div>', {'class': LEFTSIDE_CLS}))
.append($('<div>', {'class': RIGHTSIDE_CLS}))
.append($('<div>', {'class': HISTORY_CLS}));
var $rightside = $toolbar.find('.'+RIGHTSIDE_CLS);
if (!config.hideDrawer) {
var $drawerContent = $('<div>', {
'class': DRAWER_CLS,// + ' dropdown-bar-content cryptpad-dropdown'
'tabindex': 1
}).appendTo($rightside).hide();
var $drawer = Cryptpad.createButton('more', true).appendTo($rightside);
$drawer.click(function () {
$drawerContent.toggle();
$drawer.removeClass('active');
if ($drawerContent.is(':visible')) {
$drawer.addClass('active');
$drawerContent.focus();
}
});
var onBlur = function (e) {
if (e.relatedTarget) {
if ($(e.relatedTarget).is('.drawer-button')) { return; }
if ($(e.relatedTarget).parents('.'+DRAWER_CLS).length) {
$(e.relatedTarget).blur(onBlur);
return;
}
}
$drawer.removeClass('active');
$drawerContent.hide();
};
$drawerContent.blur(onBlur);
}
// The 'notitle' class removes the line added for the title with a small screen
if (!config.title || typeof config.title !== "object") {
$toolbar.addClass('notitle');
}
$container.prepend($toolbar);
$container.on('drop dragover', function (e) {
e.preventDefault();
e.stopPropagation();
});
return $toolbar;
};
// Userlist elements
var getOtherUsers = function(config) {
//var userList = config.userList.getUserlist();
var userData = config.metadataMgr.getMetadata().users;
var i = 0; // duplicates counter
var list = [];
// Display only one time each user (if he is connected in multiple tabs)
var uids = [];
Object.keys(userData).forEach(function(user) {
//if (user !== userNetfluxId) {
var data = userData[user] || {};
var userId = data.uid;
if (!userId) { return; }
//data.netfluxId = user;
if (uids.indexOf(userId) === -1) {// && (!myUid || userId !== myUid)) {
uids.push(userId);
list.push(data);
} else { i++; }
//}
});
return {
list: list,
duplicates: i
};
};
var avatars = {};
var updateUserList = function (toolbar, config) {
// Make sure the elements are displayed
var $userButtons = toolbar.userlist;
var $userlistContent = toolbar.userlistContent;
var metadataMgr = config.metadataMgr;
var userData = metadataMgr.getMetadata().users;
var viewers = metadataMgr.getViewers();
var origin = config.metadataMgr.getPrivateData().origin;
// If we are using old pads (readonly unavailable), only editing users are in userList.
// With new pads, we also have readonly users in userList, so we have to intersect with
// the userData to have only the editing users. We can't use userData directly since it
// may contain data about users that have already left the channel.
//userList = config.readOnly === -1 ? userList : arrayIntersect(userList, Object.keys(userData));
// Names of editing users
var others = getOtherUsers(config);
var editUsersNames = others.list;
var duplicates = others.duplicates; // Number of duplicates
editUsersNames.sort(function (a, b) {
var na = a.name || Messages.anonymous;
var nb = b.name || Messages.anonymous;
return na.toLowerCase() > nb.toLowerCase();
});
var numberOfEditUsers = Object.keys(userData).length - duplicates;
var numberOfViewUsers = viewers;
// Update the userlist
var $editUsers = $userlistContent.find('.' + USERLIST_CLS).html('');
Cryptpad.clearTooltips();
var $editUsersList = $('<div>', {'class': 'userlist-others'});
// Editors
// TODO iframe enable friends
//var pendingFriends = Cryptpad.getPendingInvites();
editUsersNames.forEach(function (data) {
var name = data.name || Messages.anonymous;
var $span = $('<span>', {'class': 'avatar'});
var $rightCol = $('<span>', {'class': 'right-col'});
$('<span>', {'class': 'name'}).text(name).appendTo($rightCol);
//var proxy = Cryptpad.getProxy();
//var isMe = data.curvePublic === proxy.curvePublic;
/*if (Cryptpad.isLoggedIn() && data.curvePublic) {
if (isMe) {
$span.attr('title', Messages._getKey('userlist_thisIsYou', [
name
]));
$nameSpan.text(name);
} else if (!proxy.friends || !proxy.friends[data.curvePublic]) {
if (pendingFriends.indexOf(data.netfluxId) !== -1) {
$('<span>', {'class': 'friend'}).text(Messages.userlist_pending)
.appendTo($rightCol);
} else {
$('<span>', {
'class': 'fa fa-user-plus friend',
'title': Messages._getKey('userlist_addAsFriendTitle', [
name
])
}).appendTo($rightCol).click(function (e) {
e.stopPropagation();
Cryptpad.inviteFromUserlist(Cryptpad, data.netfluxId);
});
}
}
}*/
if (data.profile) {
$span.addClass('clickable');
$span.click(function () {
window.open(origin+'/profile/#' + data.profile);
});
}
if (data.avatar && avatars[data.avatar]) {
$span.append(avatars[data.avatar]);
$span.append($rightCol);
} else {
Common.displayAvatar(Common, $span, data.avatar, name, function ($img) {
if (data.avatar && $img) {
avatars[data.avatar] = $img[0].outerHTML;
}
$span.append($rightCol);
});
}
$span.data('uid', data.uid);
$editUsersList.append($span);
});
$editUsers.append($editUsersList);
// Viewers
if (numberOfViewUsers > 0) {
var viewText = '<div class="viewer">';
var viewerText = numberOfViewUsers !== 1 ? Messages.viewers : Messages.viewer;
viewText += numberOfViewUsers + ' ' + viewerText + '</div>';
$editUsers.append(viewText);
}
// Update the buttons
var fa_editusers = '<span class="fa fa-users"></span>';
var fa_viewusers = '<span class="fa fa-eye"></span>';
var $spansmall = $('<span>').html(fa_editusers + ' ' + numberOfEditUsers + '&nbsp;&nbsp; ' + fa_viewusers + ' ' + numberOfViewUsers);
$userButtons.find('.buttonTitle').html('').append($spansmall);
};
var initUserList = function (toolbar, config) {
// TODO clean comments
if (config.metadataMgr) { /* && config.userList.list && config.userList.userNetfluxId) {*/
//var userList = config.userList.list;
//userList.change.push
var metadataMgr = config.metadataMgr;
metadataMgr.onChange(function () {
if (metadataMgr.isConnected()) {toolbar.connected = true;}
if (!toolbar.connected) { return; }
//if (config.userList.data) {
updateUserList(toolbar, config);
//}
});
}
};
// Create sub-elements
var createUserList = function (toolbar, config) {
if (!config.metadataMgr) {
throw new Error("You must provide a `metadataMgr` to display the userlist");
}
var $content = $('<div>', {'class': 'userlist-drawer'});
$content.on('drop dragover', function (e) {
e.preventDefault();
e.stopPropagation();
});
var $closeIcon = $('<span>', {"class": "fa fa-window-close close"}).appendTo($content);
$('<h2>').text(Messages.users).appendTo($content);
$('<p>', {'class': USERLIST_CLS}).appendTo($content);
toolbar.userlistContent = $content;
var $container = $('<span>', {id: 'userButtons', title: Messages.userListButton});
var $button = $('<button>').appendTo($container);
$('<span>',{'class': 'buttonTitle'}).appendTo($button);
toolbar.$leftside.prepend($container);
if (config.$contentContainer) {
config.$contentContainer.prepend($content);
}
var $ck = config.$container.find('.cke_toolbox_main');
var mobile = $('body').width() <= 600;
var hide = function () {
$content.hide();
$button.removeClass('active');
$ck.css({
'padding-left': '',
});
};
var show = function () {
$content.show();
if (mobile) {
$ck.hide();
}
$button.addClass('active');
$ck.css({
'padding-left': '175px',
});
var h = $ck.is(':visible') ? -$ck.height() : 0;
$content.css('margin-top', h+'px');
};
$(window).on('cryptpad-ck-toolbar', function () {
if (mobile && $ck.is(':visible')) { return void hide(); }
if ($content.is(':visible')) { return void show(); }
hide();
});
$(window).on('resize', function () {
mobile = $('body').width() <= 600;
var h = $ck.is(':visible') ? -$ck.height() : 0;
$content.css('margin-top', h+'px');
});
$closeIcon.click(function () {
//Cryptpad.setAttribute('userlist-drawer', false); TODO iframe
hide();
});
$button.click(function () {
var visible = $content.is(':visible');
if (visible) { hide(); }
else { show(); }
visible = !visible;
// TODO iframe
//Cryptpad.setAttribute('userlist-drawer', visible);
Common.feedback(visible?'USERLIST_SHOW': 'USERLIST_HIDE');
});
show();
// TODO iframe
/*Cryptpad.getAttribute('userlist-drawer', function (err, val) {
if (val === false || mobile) { return void hide(); }
show();
});*/
return $container;
};
var createShare = function (toolbar, config) {
if (!config.metadataMgr) {
throw new Error("You must provide a `metadataMgr` to display the userlist");
}
var metadataMgr = config.metadataMgr;
var origin = config.metadataMgr.getPrivateData().origin;
var pathname = config.metadataMgr.getPrivateData().pathname;
var hashes = metadataMgr.getPrivateData().availableHashes;
var readOnly = metadataMgr.getPrivateData().readOnly;
var $shareIcon = $('<span>', {'class': 'fa fa-share-alt'});
var options = [];
if (hashes.editHash) {
options.push({
tag: 'a',
attributes: {title: Messages.editShareTitle, 'class': 'editShare'},
content: '<span class="fa fa-users"></span> ' + Messages.editShare
});
if (readOnly) {
// We're in view mode, display the "open editing link" button
options.push({
tag: 'a',
attributes: {
title: Messages.editOpenTitle,
'class': 'editOpen',
href: origin + pathname + '#' + hashes.editHash,
target: '_blank'
},
content: '<span class="fa fa-users"></span> ' + Messages.editOpen
});
}
options.push({tag: 'hr'});
}
if (hashes.viewHash) {
options.push({
tag: 'a',
attributes: {title: Messages.viewShareTitle, 'class': 'viewShare'},
content: '<span class="fa fa-eye"></span> ' + Messages.viewShare
});
if (!readOnly) {
// We're in edit mode, display the "open readonly" button
options.push({
tag: 'a',
attributes: {
title: Messages.viewOpenTitle,
'class': 'viewOpen',
href: origin + pathname + '#' + hashes.viewHash,
target: '_blank'
},
content: '<span class="fa fa-eye"></span> ' + Messages.viewOpen
});
}
}
var dropdownConfigShare = {
text: $('<div>').append($shareIcon).html(),
options: options,
feedback: 'SHARE_MENU',
};
var $shareBlock = Cryptpad.createDropdown(dropdownConfigShare);
$shareBlock.find('.dropdown-bar-content').addClass(SHARE_CLS).addClass(EDITSHARE_CLS).addClass(VIEWSHARE_CLS);
$shareBlock.addClass('shareButton');
$shareBlock.find('button').attr('title', Messages.shareButton);
if (hashes.editHash) {
$shareBlock.find('a.editShare').click(function () {
/*Common.storeLinkToClipboard(false, function (err) {
if (!err) { Cryptpad.log(Messages.shareSuccess); }
});*/
var url = origin + pathname + '#' + hashes.editHash;
var success = Cryptpad.Clipboard.copy(url);
if (success) { Cryptpad.log(Messages.shareSuccess); }
});
}
if (hashes.viewHash) {
$shareBlock.find('a.viewShare').click(function () {
/*Common.storeLinkToClipboard(true, function (err) {
if (!err) { Cryptpad.log(Messages.shareSuccess); }
});*/
var url = origin + pathname + '#' + hashes.viewHash;
var success = Cryptpad.Clipboard.copy(url);
if (success) { Cryptpad.log(Messages.shareSuccess); }
});
}
toolbar.$leftside.append($shareBlock);
toolbar.share = $shareBlock;
return "Loading share button";
};
var createFileShare = function (toolbar) {
if (!window.location.hash) {
throw new Error("Unable to display the share button: hash required in the URL");
}
var $shareIcon = $('<span>', {'class': 'fa fa-share-alt'});
var $button = $('<button>', {'title': Messages.shareButton}).append($shareIcon);
$button.addClass('shareButton');
$button.click(function () {
var url = window.location.href;
var success = Cryptpad.Clipboard.copy(url);
if (success) { Cryptpad.log(Messages.shareSuccess); }
});
toolbar.$leftside.append($button);
return $button;
};
var createTitle = function (toolbar, config) {
var $titleContainer = $('<span>', {
id: 'toolbarTitle',
'class': TITLE_CLS
}).appendTo(toolbar.$top);
var $hoverable = $('<span>', {'class': 'hoverable'}).appendTo($titleContainer);
if (typeof config.title !== "object") {
console.error("config.title", config);
throw new Error("config.title is not an object");
}
var updateTitle = config.title.updateTitle;
var placeholder = config.title.defaultName;
var suggestName = config.title.suggestName;
// Buttons
var $text = $('<span>', {
'class': 'title'
}).appendTo($hoverable);
var $pencilIcon = $('<span>', {
'class': 'pencilIcon',
'title': Messages.clickToEdit
});
var $saveIcon = $('<span>', {
'class': 'saveIcon',
'title': Messages.saveTitle
}).hide();
if (config.readOnly === 1) {
$titleContainer.append($('<span>', {'class': 'readOnly'})
.text('('+Messages.readonly+')'));
}
if (config.readOnly === 1 || typeof(Cryptpad) === "undefined") { return $titleContainer; }
var $input = $('<input>', {
type: 'text',
placeholder: placeholder
}).appendTo($hoverable).hide();
if (config.readOnly !== 1) {
$text.attr("title", Messages.clickToEdit);
$text.addClass("editable");
var $icon = $('<span>', {
'class': 'fa fa-pencil readonly',
style: 'font-family: FontAwesome;'
});
$pencilIcon.append($icon).appendTo($hoverable);
var $icon2 = $('<span>', {
'class': 'fa fa-check readonly',
style: 'font-family: FontAwesome;'
});
$saveIcon.append($icon2).appendTo($hoverable);
}
// Events
$input.on('mousedown', function (e) {
if (!$input.is(":focus")) {
$input.focus();
}
e.stopPropagation();
return true;
});
var save = function () {
var name = $input.val().trim();
if (name === "") {
name = $input.attr('placeholder');
}
updateTitle(name, function (err/*, newtitle*/) {
if (err) { return console.error(err); }
//$text.text(newtitle);
$input.hide();
$text.show();
$pencilIcon.show();
$saveIcon.hide();
});
};
$input.on('keyup', function (e) {
if (e.which === 13 && toolbar.connected === true) {
save();
} else if (e.which === 27) {
$input.hide();
$text.show();
$pencilIcon.show();
$saveIcon.hide();
//$pencilIcon.css('display', '');
} else if (e.which === 32) {
e.stopPropagation();
}
});
$saveIcon.click(save);
var displayInput = function () {
if (toolbar.connected === false) { return; }
$input.width(Math.max($text.width(), 300)+'px');
$text.hide();
//$pencilIcon.css('display', 'none');
var inputVal = suggestName() || "";
$input.val(inputVal);
$input.show();
$input.focus();
$pencilIcon.hide();
$saveIcon.show();
};
$text.on('click', displayInput);
$pencilIcon.on('click', displayInput);
return $titleContainer;
};
var createPageTitle = function (toolbar, config) {
if (config.title || !config.pageTitle) { return; }
var $titleContainer = $('<span>', {
id: 'toolbarTitle',
'class': TITLE_CLS
}).appendTo(toolbar.$top);
toolbar.$top.find('.filler').hide();
var $hoverable = $('<span>', {'class': 'hoverable'}).appendTo($titleContainer);
// Buttons
$('<span>', {
'class': 'title pageTitle'
}).appendTo($hoverable).text(config.pageTitle);
};
var createLinkToMain = function (toolbar) {
var $linkContainer = $('<span>', {
'class': "cryptpad-link"
}).appendTo(toolbar.$top);
// We need to override the "a" tag action here because it is inside the iframe!
var inDrive = /^\/drive/;
var href = inDrive ? '/index.html' : '/drive/';
var buttonTitle = inDrive ? Messages.header_homeTitle : Messages.header_logoTitle;
var $aTag = $('<a>', {
href: href,
title: buttonTitle,
'class': "cryptpad-logo"
}).append($('<img>', {
src: '/customize/images/logo_white.png?' + ApiConfig.requireConf.urlArgs
}));
var onClick = function (e) {
e.preventDefault();
if (e.ctrlKey) {
window.open(href);
return;
}
window.location = href;
};
var onContext = function (e) { e.stopPropagation(); };
$aTag.click(onClick).contextmenu(onContext);
$linkContainer.append($aTag);
return $linkContainer;
};
var typing = -1;
var kickSpinner = function (toolbar, config, local) {
if (!toolbar.spinner) { return; }
var $spin = toolbar.spinner;
if (typing === -1) {
typing = 1;
$spin.text(Messages.typing);
$spin.interval = window.setInterval(function () {
var dots = Array(typing+1).join('.');
$spin.text(Messages.typing + dots);
typing++;
if (typing > 3) { typing = 0; }
}, 500);
}
var onSynced = function () {
if ($spin.timeout) { clearTimeout($spin.timeout); }
$spin.timeout = setTimeout(function () {
window.clearInterval($spin.interval);
typing = -1;
$spin.text(Messages.saved);
}, local ? 0 : SPINNER_DISAPPEAR_TIME);
};
if (Cryptpad) {
Cryptpad.whenRealtimeSyncs(config.realtime, onSynced);
return;
}
onSynced();
};
var ks = function (toolbar, config, local) {
return function () {
if (toolbar.connected) { kickSpinner(toolbar, config, local); }
};
};
var createSpinner = function (toolbar, config) {
var $spin = $('<span>', {'class': SPINNER_CLS}).appendTo(toolbar.$leftside);
$spin.text(Messages.synchronizing);
if (config.realtime) {
config.realtime.onPatch(ks(toolbar, config));
config.realtime.onMessage(ks(toolbar, config, true));
}
// without this, users in read-only mode say 'synchronizing' until they
// receive a patch.
if (Cryptpad) {
typing = 0;
Cryptpad.whenRealtimeSyncs(config.realtime, function () {
kickSpinner(toolbar, config);
});
}
return $spin;
};
var createLimit = function (toolbar) {
if (!Config.enablePinning) { return; }
var $limitIcon = $('<span>', {'class': 'fa fa-exclamation-triangle'});
var $limit = toolbar.$userAdmin.find('.'+LIMIT_CLS).attr({
'title': Messages.pinLimitReached
}).append($limitIcon).hide();
var todo = function (e, overLimit) {
if (e) { return void console.error("Unable to get the pinned usage"); }
if (overLimit) {
var key = 'pinLimitReachedAlert';
if (ApiConfig.noSubscriptionButton === true) {
key = 'pinLimitReachedAlertNoAccounts';
}
$limit.show().click(function () {
Cryptpad.alert(Messages._getKey(key, [encodeURIComponent(window.location.hostname)]), null, true);
});
}
};
Common.isOverPinLimit(todo);
return $limit;
};
var createNewPad = function (toolbar, config) {
var $newPad = toolbar.$top.find('.'+NEWPAD_CLS).show();
var origin = config.metadataMgr.getPrivateData().origin;
var pads_options = [];
Config.availablePadTypes.forEach(function (p) {
if (p === 'drive') { return; }
if (!Cryptpad.isLoggedIn() && Config.registeredOnlyTypes &&
Config.registeredOnlyTypes.indexOf(p) !== -1) { return; }
pads_options.push({
tag: 'a',
attributes: {
'target': '_blank',
'href': origin + '/' + p + '/',
},
content: $('<div>').append(Cryptpad.getIcon(p)).html() + Messages.type[p]
});
});
var dropdownConfig = {
text: '', // Button initial text
options: pads_options, // Entries displayed in the menu
container: $newPad,
left: true,
feedback: /drive/.test(window.location.pathname)?
'DRIVE_NEWPAD': 'NEWPAD',
};
var $newPadBlock = Cryptpad.createDropdown(dropdownConfig);
$newPadBlock.find('button').attr('title', Messages.newButtonTitle);
$newPadBlock.find('button').addClass('fa fa-th');
return $newPadBlock;
};
var createUserAdmin = function (toolbar, config) {
if (!config.metadataMgr) {
throw new Error("You must provide a `metadataMgr` to display the user menu");
}
var metadataMgr = config.metadataMgr;
var $userAdmin = toolbar.$userAdmin.find('.'+USERADMIN_CLS).show();
var userMenuCfg = {
$initBlock: $userAdmin,
metadataMgr: metadataMgr,
Common: Common
};
if (!config.hideDisplayName) {
$.extend(true, userMenuCfg, {
displayNameCls: USERNAME_CLS,
changeNameButtonCls: USERBUTTON_CLS,
});
}
if (config.readOnly !== 1) {
userMenuCfg.displayName = 1;
userMenuCfg.displayChangeName = 1;
}
Common.createUserAdminMenu(userMenuCfg);
$userAdmin.find('button').attr('title', Messages.userAccountButton);
var $userButton = toolbar.$userNameButton = $userAdmin.find('a.' + USERBUTTON_CLS);
$userButton.click(function (e) {
e.preventDefault();
e.stopPropagation();
var myData = metadataMgr.getMetadata().users[metadataMgr.getNetfluxId()];
var lastName = myData.name;
Cryptpad.prompt(Messages.changeNamePrompt, lastName || '', function (newName) {
if (newName === null && typeof(lastName) === "string") { return; }
if (newName === null) { newName = ''; }
else { Common.feedback('NAME_CHANGED'); }
Common.setDisplayName(newName, function (err) {
if (err) {
console.log("Couldn't set username");
console.error(err);
return;
}
//Cryptpad.changeDisplayName(newName, true); Already done?
});
});
});
return $userAdmin;
};
// Events
var initClickEvents = function (toolbar, config) {
var removeDropdowns = function () {
window.setTimeout(function () {
toolbar.$toolbar.find('.cryptpad-dropdown').hide();
});
};
var cancelEditTitle = function (e) {
// Now we want to apply the title even if we click somewhere else
if ($(e.target).parents('.' + TITLE_CLS).length || !toolbar.title) {
return;
}
var $title = toolbar.title;
if (!$title.find('input').is(':visible')) { return; }
// Press enter
var ev = $.Event("keyup");
ev.which = 13;
$title.find('input').trigger(ev);
};
// Click in the main window
var w = config.ifrw || window;
$(w).on('click', removeDropdowns);
$(w).on('click', cancelEditTitle);
// Click in iframes
try {
if (w.$ && w.$('iframe').length) {
config.ifrw.$('iframe').each(function (i, el) {
$(el.contentWindow).on('click', removeDropdowns);
$(el.contentWindow).on('click', cancelEditTitle);
});
}
} catch (e) {
// empty try catch in case this iframe is problematic
}
};
// Notifications
var initNotifications = function (toolbar, config) {
// Display notifications when users are joining/leaving the session
var oldUserData;
if (!config.metadataMgr) { return; }
var metadataMgr = config.metadataMgr;
var userNetfluxId = metadataMgr.getNetfluxId();
if (typeof Cryptpad !== "undefined") {
var notify = function(type, name, oldname) {
// type : 1 (+1 user), 0 (rename existing user), -1 (-1 user)
if (typeof name === "undefined") { return; }
name = name || Messages.anonymous;
switch(type) {
case 1:
Cryptpad.log(Messages._getKey("notifyJoined", [name]));
break;
case 0:
oldname = (!oldname) ? Messages.anonymous : oldname;
Cryptpad.log(Messages._getKey("notifyRenamed", [oldname, name]));
break;
case -1:
Cryptpad.log(Messages._getKey("notifyLeft", [name]));
break;
default:
console.log("Invalid type of notification");
break;
}
};
var userPresent = function (id, user, data) {
if (!(user && user.uid)) {
console.log('no uid');
return 0;
}
if (!data) {
console.log('no data');
return 0;
}
var count = 0;
Object.keys(data).forEach(function (k) {
if (data[k] && data[k].uid === user.uid) { count++; }
});
return count;
};
metadataMgr.onChange(function () {
var newdata = metadataMgr.getMetadata().users;
var netfluxIds = Object.keys(newdata);
// Notify for disconnected users
if (typeof oldUserData !== "undefined") {
for (var u in oldUserData) {
// if a user's uid is still present after having left, don't notify
if (netfluxIds.indexOf(u) === -1) {
var temp = JSON.parse(JSON.stringify(oldUserData[u]));
delete oldUserData[u];
if (temp && newdata[userNetfluxId] && temp.uid === newdata[userNetfluxId].uid) { return; }
if (userPresent(u, temp, newdata || oldUserData) < 1) {
notify(-1, temp.name);
}
}
}
}
// Update the "oldUserData" object and notify for new users and names changed
if (typeof newdata === "undefined") { return; }
if (typeof oldUserData === "undefined") {
oldUserData = JSON.parse(JSON.stringify(newdata));
return;
}
if (config.readOnly === 0 && !oldUserData[userNetfluxId]) {
oldUserData = JSON.parse(JSON.stringify(newdata));
return;
}
for (var k in newdata) {
if (k !== userNetfluxId && netfluxIds.indexOf(k) !== -1) {
if (typeof oldUserData[k] === "undefined") {
// if the same uid is already present in the userdata, don't notify
if (!userPresent(k, newdata[k], oldUserData)) {
notify(1, newdata[k].name);
}
} else if (oldUserData[k].name !== newdata[k].name) {
notify(0, newdata[k].name, oldUserData[k].name);
}
}
}
oldUserData = JSON.parse(JSON.stringify(newdata));
});
}
};
// Main
Bar.create = function (cfg) {
var config = cfg || {};
Cryptpad = config.common;
Common = config.sfCommon;
Messages = Cryptpad.Messages;
config.readOnly = (typeof config.readOnly !== "undefined") ? (config.readOnly ? 1 : 0) : -1;
config.displayed = config.displayed || [];
var toolbar = {};
toolbar.connected = false;
toolbar.firstConnection = true;
var $toolbar = toolbar.$toolbar = createRealtimeToolbar(config);
toolbar.$leftside = $toolbar.find('.'+Bar.constants.leftside);
toolbar.$rightside = $toolbar.find('.'+Bar.constants.rightside);
toolbar.$drawer = $toolbar.find('.'+Bar.constants.drawer);
toolbar.$top = $toolbar.find('.'+Bar.constants.top);
toolbar.$history = $toolbar.find('.'+Bar.constants.history);
toolbar.$userAdmin = $toolbar.find('.'+Bar.constants.userAdmin);
// Create the subelements
var tb = {};
tb['userlist'] = createUserList;
tb['share'] = createShare;
tb['fileshare'] = createFileShare;//TODO
tb['title'] = createTitle;
tb['pageTitle'] = createPageTitle;//TODO
tb['lag'] = $.noop;
tb['spinner'] = createSpinner;
tb['state'] = $.noop;
tb['limit'] = createLimit; // TODO
tb['upgrade'] = $.noop;
tb['newpad'] = createNewPad;
tb['useradmin'] = createUserAdmin;
var addElement = toolbar.addElement = function (arr, additionnalCfg, init) {
if (typeof additionnalCfg === "object") { $.extend(true, config, additionnalCfg); }
arr.forEach(function (el) {
if (typeof el !== "string" || !el.trim()) { return; }
if (typeof tb[el] === "function") {
if (!init && config.displayed.indexOf(el) !== -1) { return; } // Already done
toolbar[el] = tb[el](toolbar, config);
if (!init) { config.displayed.push(el); }
}
});
};
addElement(config.displayed, {}, true);
initUserList(toolbar, config);
toolbar['linkToMain'] = createLinkToMain(toolbar, config);
if (!config.realtime) { toolbar.connected = true; }
initClickEvents(toolbar, config);
initNotifications(toolbar, config);
var failed = toolbar.failed = function () {
toolbar.connected = false;
if (toolbar.spinner) {
toolbar.spinner.text(Messages.disconnected);
}
//checkLag(toolbar, config);
};
toolbar.reconnecting = function (/*userId*/) {
//if (config.metadataMgr) { config.userList.userNetfluxId = userId; } TODO
toolbar.connected = false;
if (toolbar.spinner) {
toolbar.spinner.text(Messages.reconnecting);
}
//checkLag(toolbar, config);
};
// On log out, remove permanently the realtime elements of the toolbar
Cryptpad.onLogout(function () {
failed();
if (toolbar.useradmin) { toolbar.useradmin.hide(); }
if (toolbar.userlist) { toolbar.userlist.hide(); }
});
return toolbar;
};
return Bar;
});

View File

@@ -413,6 +413,15 @@ define([
});
return ret;
};
exp.getRecentPads = function () {
var allFiles = files[FILES_DATA];
var sorted = Object.keys(allFiles)
.sort(function (a,b) {
return allFiles[a].atime < allFiles[b].atime;
})
.map(function (str) { return Number(str); });
return sorted;
};
/**
* OPERATIONS