You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cryptpad/www/common/sframe-common-outer.js

1507 lines
66 KiB
JavaScript

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

// 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',
], function (nThen, ApiConfig, $) {
var common = {};
common.start = function (cfg) {
cfg = cfg || {};
var realtime = !cfg.noRealtime;
var secret;
var hashes;
var isNewFile;
var CpNfOuter;
var Cryptpad;
var Crypto;
var Cryptget;
var SFrameChannel;
var sframeChan;
var FilePicker;
var Share;
var Messaging;
var Notifier;
var Utils = {
nThen: nThen
};
var AppConfig;
var Test;
var password;
var initialPathInDrive;
var currentPad = window.CryptPad_location = {
href: cfg.href || window.location.href,
hash: cfg.hash || window.location.hash
};
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/outer/worker-channel.js',
'/filepicker/main.js',
'/share/main.js',
'/common/common-messaging.js',
'/common/common-notifier.js',
'/common/common-hash.js',
'/common/common-util.js',
'/common/common-realtime.js',
'/common/common-constants.js',
'/common/common-feedback.js',
'/common/outer/local-store.js',
'/customize/application_config.js',
'/common/test.js',
'/common/userObject.js',
], waitFor(function (_CpNfOuter, _Cryptpad, _Crypto, _Cryptget, _SFrameChannel,
_FilePicker, _Share, _Messaging, _Notifier, _Hash, _Util, _Realtime,
_Constants, _Feedback, _LocalStore, _AppConfig, _Test, _UserObject) {
CpNfOuter = _CpNfOuter;
Cryptpad = _Cryptpad;
Crypto = Utils.Crypto = _Crypto;
Cryptget = _Cryptget;
SFrameChannel = _SFrameChannel;
FilePicker = _FilePicker;
Share = _Share;
Messaging = _Messaging;
Notifier = _Notifier;
Utils.Hash = _Hash;
Utils.Util = _Util;
Utils.Realtime = _Realtime;
Utils.Constants = _Constants;
Utils.Feedback = _Feedback;
Utils.LocalStore = _LocalStore;
Utils.UserObject = _UserObject;
AppConfig = _AppConfig;
Test = _Test;
if (localStorage.CRYPTPAD_URLARGS !== ApiConfig.requireConf.urlArgs) {
console.log("New version, flushing cache");
Object.keys(localStorage).forEach(function (k) {
if (k.indexOf('CRYPTPAD_CACHE|') !== 0) { return; }
delete localStorage[k];
});
localStorage.CRYPTPAD_URLARGS = ApiConfig.requireConf.urlArgs;
}
var cache = window.cpCache = {};
var localStore = window.localStore = {};
Object.keys(localStorage).forEach(function (k) {
if (k.indexOf('CRYPTPAD_CACHE|') === 0) {
cache[k.slice(('CRYPTPAD_CACHE|').length)] = localStorage[k];
return;
}
if (k.indexOf('CRYPTPAD_STORE|') === 0) {
localStore[k.slice(('CRYPTPAD_STORE|').length)] = localStorage[k];
return;
}
});
// The inner iframe tries to get some data from us every ms (cache, store...).
// It will send a "READY" message and wait for our answer with the correct txid.
// First, we have to answer to this message, otherwise we're going to block
// sframe-boot.js. Then we can start the channel.
var msgEv = _Util.mkEvent();
var iframe = $('#sbox-iframe')[0].contentWindow;
var postMsg = function (data) {
iframe.postMessage(data, '*');
};
var whenReady = waitFor(function (msg) {
if (msg.source !== iframe) { return; }
var data = JSON.parse(msg.data);
if (!data.txid) { return; }
// Remove the listener once we've received the READY message
window.removeEventListener('message', whenReady);
// Answer with the requested data
postMsg(JSON.stringify({ txid: data.txid, cache: cache, localStore: localStore, language: Cryptpad.getLanguage() }));
// Then start the channel
window.addEventListener('message', function (msg) {
if (msg.source !== iframe) { return; }
msgEv.fire(msg);
});
SFrameChannel.create(msgEv, postMsg, waitFor(function (sfc) {
Utils.sframeChan = sframeChan = sfc;
}));
});
window.addEventListener('message', whenReady);
Cryptpad.loading.onDriveEvent.reg(function (data) {
if (sframeChan) { sframeChan.event('EV_LOADING_INFO', data); }
});
Cryptpad.ready(waitFor(function () {
if (sframeChan) {
sframeChan.event('EV_LOADING_INFO', {
state: -1
});
}
}), {
driveEvents: cfg.driveEvents,
currentPad: currentPad
});
if (window.history && window.history.replaceState && currentPad.hash) {
var nHash = currentPad.hash;
if (!/^#/.test(nHash)) { nHash = '#' + nHash; }
window.history.replaceState({}, window.document.title, nHash);
}
}));
}).nThen(function (waitFor) {
if (!Utils.Hash.isValidHref(window.location.href)) {
waitFor.abort();
return void sframeChan.event('EV_LOADING_ERROR', 'INVALID_HASH');
}
$('#sbox-iframe').focus();
sframeChan.on('EV_CACHE_PUT', function (x) {
Object.keys(x).forEach(function (k) {
localStorage['CRYPTPAD_CACHE|' + k] = x[k];
});
});
sframeChan.on('EV_LOCALSTORE_PUT', function (x) {
Object.keys(x).forEach(function (k) {
if (typeof(x[k]) === "undefined") {
delete localStorage['CRYPTPAD_STORE|' + k];
return;
}
localStorage['CRYPTPAD_STORE|' + k] = x[k];
});
});
if (cfg.getSecrets) {
var w = waitFor();
// No password for drive, profile and todo
cfg.getSecrets(Cryptpad, Utils, waitFor(function (err, s) {
secret = Utils.secret = s;
Cryptpad.getShareHashes(secret, function (err, h) {
hashes = h;
w();
});
}));
} else {
var parsed = Utils.Hash.parsePadUrl(currentPad.href);
var todo = function () {
secret = Utils.secret = Utils.Hash.getSecrets(parsed.type, parsed.hash, password);
Cryptpad.getShareHashes(secret, waitFor(function (err, h) {
hashes = h;
/* XXX this won't happen again: we don't need to update the rendered hash
if (password && !parsed.hashData.password) {
var ohc = window.onhashchange;
window.onhashchange = function () {};
window.location.hash = h.fileHash || h.editHash || h.viewHash || window.location.hash;
window.onhashchange = ohc;
ohc({reset: true});
}
*/
}));
};
if (!parsed.hashData) { // No hash, no need to check for a password
return void todo();
}
// We now need to check if there is a password and if we know the correct password.
// We'll use getFileSize and isNewChannel to detect incorrect passwords.
// First we'll get the password value from our drive (getPadAttribute), and we'll check
// if the channel is valid. If the pad is not stored in our drive, we'll test with an
// empty password instead.
// If this initial check returns a valid channel, open the pad.
// If the channel is invalid:
// Option 1: this is a password-protected pad not stored in our drive --> password prompt
// Option 2: this is a pad stored in our drive
// 2a: 'edit' pad or file --> password-prompt
// 2b: 'view' pad no '/p/' --> the seed is incorrect
// 2c: 'view' pad and '/p/' and a wrong password stored --> the seed is incorrect
// 2d: 'view' pad and '/p/' and password never stored (security feature) --> password-prompt
var askPassword = function (wrongPasswordStored, cfg) {
// Ask for the password and check if the pad exists
// If the pad doesn't exist, it means the password isn't correct
// or the pad has been deleted
var correctPassword = waitFor();
sframeChan.on('Q_PAD_PASSWORD_VALUE', function (data, cb) {
password = data;
var next = function (e, isNew) {
if (Boolean(isNew)) {
// Ask again in the inner iframe
// We should receive a new Q_PAD_PASSWORD_VALUE
cb(false);
} else {
todo();
if (wrongPasswordStored) {
// Store the correct password
nThen(function (w) {
Cryptpad.setPadAttribute('password', password, w(), parsed.getUrl());
Cryptpad.setPadAttribute('channel', secret.channel, w(), parsed.getUrl());
if (parsed.hashData.mode === 'edit') {
var href = window.location.pathname + '#' + Utils.Hash.getEditHashFromKeys(secret);
Cryptpad.setPadAttribute('href', href, w(), parsed.getUrl());
var roHref = window.location.pathname + '#' + Utils.Hash.getViewHashFromKeys(secret);
Cryptpad.setPadAttribute('roHref', roHref, w(), parsed.getUrl());
}
}).nThen(correctPassword);
} else {
correctPassword();
}
cb(true);
}
};
if (parsed.type === "file") {
// `isNewChannel` doesn't work for files (not a channel)
// `getFileSize` is not adapted to channels because of metadata
Cryptpad.getFileSize(currentPad.href, password, function (e, size) {
next(e, size === 0);
});
return;
}
// Not a file, so we can use `isNewChannel`
Cryptpad.isNewChannel(currentPad.href, password, next);
});
sframeChan.event("EV_PAD_PASSWORD", cfg);
};
var done = waitFor();
var stored = false;
var passwordCfg = {
value: ''
};
// Hidden hash: can't find the channel in our drives: abort
var noPadData = function (err) {
console.error(err);
// XXX Display error screen in inner
};
// Hidden hash: can't find requestd edit URL in our drives: ask
var badPadData = function (cb) {
// If we requested edit but we only know view: ???
setTimeout(function () {
cb(true);
}); // XXX ask in inner?
};
var newHref;
nThen(function (w) {
if (!parsed.hashData.key && parsed.hashData.channel) {
var edit = parsed.hashData.mode === 'edit';
Cryptpad.getPadDataFromChannel({
channel: parsed.hashData.channel,
edit: edit,
file: parsed.hashData.type === 'file'
}, w(function (err, res) {
// Error while getting data? abort
if (err || !res || res.error) {
w.abort();
return void noPadData(err || (!res ? 'EINVAL' : res.error));
}
// No data found? abort
if (!Object.keys(res).length) {
w.abort();
return void noPadData('NO_RESULT');
}
// Data found but weaker? warn
if (edit && !res.href) {
return void badPadData(w(function (load) {
if (!load) {
w.abort();
return;
}
newHref = res.roHref;
}));
}
// We have good data, keep the hash in memory
newHref = edit ? res.href : (res.roHref || res.href);
}));
}
}).nThen(function (w) {
if (newHref) {
// Get the options (embed, present, etc.) of the hidden hash
// Use the same options in the full hash
var opts = parsed.getOptions();
parsed = Utils.Hash.parsePadUrl(newHref);
currentPad.href = parsed.getUrl(opts);
currentPad.hash = parsed.hashData && parsed.hashData.getHash(opts);
}
Cryptpad.getPadAttribute('title', w(function (err, data) {
stored = (!err && typeof (data) === "string");
}));
Cryptpad.getPadAttribute('password', w(function (err, val) {
password = val;
}), parsed.getUrl());
}).nThen(function (w) {
if (!password && !stored && sessionStorage.newPadPassword) {
passwordCfg.value = sessionStorage.newPadPassword;
delete sessionStorage.newPadPassword;
}
if (parsed.type === "file") {
// `isNewChannel` doesn't work for files (not a channel)
// `getFileSize` is not adapted to channels because of metadata
Cryptpad.getFileSize(currentPad.href, password, w(function (e, size) {
if (size !== 0) { return void todo(); }
// Wrong password or deleted file?
askPassword(true, passwordCfg);
}));
return;
}
// Not a file, so we can use `isNewChannel`
Cryptpad.isNewChannel(currentPad.href, password, w(function(e, isNew) {
if (!isNew) { return void todo(); }
if (parsed.hashData.mode === 'view' && (password || !parsed.hashData.password)) {
// Error, wrong password stored, the view seed has changed with the password
// password will never work
sframeChan.event("EV_PAD_PASSWORD_ERROR");
waitFor.abort();
return;
}
if (!stored && !parsed.hashData.password) {
// We've received a link without /p/ and it doesn't work without a password: abort
return void todo();
}
// Wrong password or deleted file?
askPassword(true, passwordCfg);
}));
}).nThen(done);
}
}).nThen(function (waitFor) {
if (cfg.afterSecrets) {
cfg.afterSecrets(Cryptpad, Utils, secret, waitFor());
}
}).nThen(function (waitFor) {
// Check if the pad exists on server
if (!currentPad.hash) { isNewFile = true; return; }
if (realtime) {
// TODO we probably don't need to check again for password-protected pads
// (we use isNewChannel to test the password...)
Cryptpad.isNewChannel(currentPad.href, password, waitFor(function (e, isNew) {
if (e) { return console.error(e); }
isNewFile = Boolean(isNew);
}));
}
}).nThen(function () {
var readOnly = secret.keys && !secret.keys.editKeyStr;
var isNewHash = true;
if (!secret.keys) {
isNewHash = false;
secret.keys = secret.key;
readOnly = false;
}
Utils.crypto = Utils.Crypto.createEncryptor(Utils.secret.keys);
var parsed = Utils.Hash.parsePadUrl(currentPad.href);
var burnAfterReading = parsed && parsed.hashData && parsed.hashData.ownerKey;
if (!parsed.type) { throw new Error(); }
var defaultTitle = Utils.UserObject.getDefaultName(parsed);
var edPublic, curvePublic, notifications, isTemplate;
var forceCreationScreen = cfg.useCreationScreen &&
sessionStorage[Utils.Constants.displayPadCreationScreen];
delete sessionStorage[Utils.Constants.displayPadCreationScreen];
var updateMeta = function () {
//console.log('EV_METADATA_UPDATE');
var metaObj;
nThen(function (waitFor) {
Cryptpad.getMetadata(waitFor(function (err, m) {
if (err) { console.log(err); }
metaObj = m;
edPublic = metaObj.priv.edPublic; // needed to create an owned pad
curvePublic = metaObj.user.curvePublic;
notifications = metaObj.user.notifications;
}));
if (typeof(isTemplate) === "undefined") {
Cryptpad.isTemplate(currentPad.href, waitFor(function (err, t) {
if (err) { console.log(err); }
isTemplate = t;
}));
}
}).nThen(function (/*waitFor*/) {
metaObj.doc = {
defaultTitle: defaultTitle,
type: cfg.type || parsed.type
};
var additionalPriv = {
app: parsed.type,
accountName: Utils.LocalStore.getAccountName(),
origin: window.location.origin,
pathname: window.location.pathname,
fileHost: ApiConfig.fileHost,
readOnly: readOnly,
isTemplate: isTemplate,
feedbackAllowed: Utils.Feedback.state,
isPresent: parsed.hashData && parsed.hashData.present,
isEmbed: parsed.hashData && parsed.hashData.embed,
accounts: {
donateURL: Cryptpad.donateURL,
upgradeURL: Cryptpad.upgradeURL
},
isNewFile: isNewFile,
isDeleted: isNewFile && currentPad.hash.length > 0,
forceCreationScreen: forceCreationScreen,
password: password,
channel: secret.channel,
enableSF: localStorage.CryptPad_SF === "1", // TODO to remove when enabled by default
devMode: localStorage.CryptPad_dev === "1",
fromFileData: Cryptpad.fromFileData ? {
title: Cryptpad.fromFileData.title
} : undefined,
burnAfterReading: burnAfterReading,
storeInTeam: Cryptpad.initialTeam || (Cryptpad.initialPath ? -1 : undefined)
};
if (window.CryptPad_newSharedFolder) {
additionalPriv.newSharedFolder = window.CryptPad_newSharedFolder;
}
if (Utils.Constants.criticalApps.indexOf(parsed.type) === -1 &&
AppConfig.availablePadTypes.indexOf(parsed.type) === -1) {
additionalPriv.disabledApp = true;
}
if (!Utils.LocalStore.isLoggedIn() &&
AppConfig.registeredOnlyTypes.indexOf(parsed.type) !== -1 &&
parsed.type !== "file") {
additionalPriv.registeredOnly = true;
}
if (['debug', 'profile'].indexOf(parsed.type) !== -1) {
additionalPriv.hashes = hashes;
}
for (var k in additionalPriv) { metaObj.priv[k] = additionalPriv[k]; }
if (cfg.addData) {
cfg.addData(metaObj.priv, Cryptpad, metaObj.user);
}
sframeChan.event('EV_METADATA_UPDATE', metaObj);
});
};
Cryptpad.onMetadataChanged(updateMeta);
sframeChan.onReg('EV_METADATA_UPDATE', updateMeta);
Utils.LocalStore.onLogout(function () {
sframeChan.event('EV_LOGOUT');
});
Test.registerOuter(sframeChan);
Cryptpad.onNewVersionReconnect.reg(function () {
sframeChan.event("EV_NEW_VERSION");
});
// Put in the following function the RPC queries that should also work in filepicker
var addCommonRpc = function (sframeChan) {
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_GET_PINNED_USAGE', function (data, cb) {
Cryptpad.getPinnedUsage({}, function (e, used) {
cb({
error: e,
quota: used
});
});
});
sframeChan.on('Q_GET_PIN_LIMIT_STATUS', function (data, cb) {
Cryptpad.isOverPinLimit(null, function (e, overLimit, limits) {
cb({
error: e,
overLimit: overLimit,
limits: limits
});
});
});
sframeChan.on('Q_THUMBNAIL_GET', function (data, cb) {
Utils.LocalStore.getThumbnail(data.key, function (e, data) {
cb({
error: e,
data: data
});
});
});
sframeChan.on('Q_THUMBNAIL_SET', function (data, cb) {
Utils.LocalStore.setThumbnail(data.key, data.value, function (e) {
cb({error:e});
});
});
sframeChan.on('Q_GET_ATTRIBUTE', function (data, cb) {
Cryptpad.getAttribute(data.key, function (e, data) {
cb({
error: e,
data: data
});
});
});
sframeChan.on('Q_SET_ATTRIBUTE', function (data, cb) {
Cryptpad.setAttribute(data.key, data.value, function (e) {
cb({error:e});
});
});
Cryptpad.mailbox.onEvent.reg(function (data, cb) {
sframeChan.query('EV_MAILBOX_EVENT', data, function (err, obj) {
if (!cb) { return; }
if (err) { return void cb({error: err}); }
cb(obj);
});
});
sframeChan.on('Q_MAILBOX_COMMAND', function (data, cb) {
Cryptpad.mailbox.execCommand(data, cb);
});
sframeChan.on('Q_SET_LOGIN_REDIRECT', function (data, cb) {
sessionStorage.redirectTo = currentPad.href;
cb();
});
sframeChan.on('Q_STORE_IN_TEAM', function (data, cb) {
Cryptpad.storeInTeam(data, cb);
});
sframeChan.on('EV_GOTO_URL', function (url) {
if (url) {
window.location.href = url;
} else {
window.location.reload();
}
});
sframeChan.on('EV_OPEN_URL', function (url) {
if (url) {
window.open(url);
}
});
sframeChan.on('Q_GET_PAD_METADATA', function (data, cb) {
if (!data || !data.channel) {
data = {
channel: secret.channel
};
}
Cryptpad.getPadMetadata(data, cb);
});
sframeChan.on('Q_SET_PAD_METADATA', function (data, cb) {
Cryptpad.setPadMetadata(data, cb);
});
sframeChan.on('Q_GET_PAD_ATTRIBUTE', function (data, cb) {
var href;
if (readOnly && hashes.editHash) {
// If we have a stronger hash, use it for pad attributes
href = window.location.pathname + '#' + hashes.editHash;
}
if (data.href) { href = data.href; }
Cryptpad.getPadAttribute(data.key, function (e, data) {
cb({
error: e,
data: data
});
}, href);
});
sframeChan.on('Q_SET_PAD_ATTRIBUTE', function (data, cb) {
var href;
if (readOnly && hashes.editHash) {
// If we have a stronger hash, use it for pad attributes
href = window.location.pathname + '#' + hashes.editHash;
}
if (data.href) { href = data.href; }
Cryptpad.setPadAttribute(data.key, data.value, function (e) {
cb({error:e});
}, href);
});
};
addCommonRpc(sframeChan);
var currentTitle;
var currentTabTitle;
var setDocumentTitle = function () {
if (!currentTabTitle) {
document.title = currentTitle || 'CryptPad';
return;
}
var title = currentTabTitle.replace(/\{title\}/g, currentTitle || 'CryptPad');
document.title = title;
};
sframeChan.on('Q_SET_PAD_TITLE_IN_DRIVE', function (newData, cb) {
var newTitle = newData.title || newData.defaultTitle;
currentTitle = newTitle;
setDocumentTitle();
var data = {
password: password,
title: newTitle,
channel: secret.channel,
path: initialPathInDrive // Where to store the pad if we don't have it in our drive
};
Cryptpad.setPadTitle(data, function (err, obj) {
if (!err && !(obj && obj.notStored)) {
// No error and the pad was correctly stored
// hide the hash
var opts = parsed.getOptions();
var hash = Utils.Hash.getHiddenHashFromKeys(parsed.type, secret, opts);
if (window.history && window.history.replaceState) {
if (!/^#/.test(hash)) { hash = '#' + hash; }
window.history.replaceState({}, window.document.title, hash);
}
}
cb({error: err});
});
});
sframeChan.on('EV_SET_TAB_TITLE', function (newTabTitle) {
currentTabTitle = newTabTitle;
setDocumentTitle();
});
sframeChan.on('EV_SET_HASH', function (hash) {
// In this case, we want to set the hash for the next page reload
// This hash is a category for the sidebar layout apps
// No need to store it in memory
window.location.hash = hash;
});
Cryptpad.autoStore.onStoreRequest.reg(function (data) {
sframeChan.event("EV_AUTOSTORE_DISPLAY_POPUP", data);
});
sframeChan.on('Q_AUTOSTORE_STORE', function (obj, cb) {
var data = {
password: password,
title: currentTitle,
channel: secret.channel,
path: initialPathInDrive, // Where to store the pad if we don't have it in our drive
forceSave: true
};
Cryptpad.setPadTitle(data, function (err) {
if (!err && !(obj && obj.notStored)) {
// No error and the pad was correctly stored
// hide the hash
var opts = parsed.getOptions();
var hash = Utils.Hash.getHiddenHashFromKeys(parsed.type, secret, opts);
if (window.history && window.history.replaceState) {
if (!/^#/.test(hash)) { hash = '#' + hash; }
window.history.replaceState({}, window.document.title, hash);
}
}
cb({error: err});
});
});
sframeChan.on('Q_IS_PAD_STORED', function (data, cb) {
Cryptpad.getPadAttribute('title', function (err, data) {
cb (!err && typeof (data) === "string");
});
});
sframeChan.on('Q_ACCEPT_OWNERSHIP', function (data, cb) {
var parsed = Utils.Hash.parsePadUrl(data.href);
if (parsed.type === 'drive') {
// Shared folder
var secret = Utils.Hash.getSecrets(parsed.type, parsed.hash, data.password);
Cryptpad.addSharedFolder(null, secret, cb);
} else {
var _data = {
password: data.password,
href: data.href,
channel: data.channel,
title: data.title,
owners: data.metadata.owners,
expire: data.metadata.expire,
forceSave: true
};
Cryptpad.setPadTitle(_data, function (err) {
cb({error: err});
});
}
// Also add your mailbox to the metadata object
var padParsed = Utils.Hash.parsePadUrl(data.href);
var padSecret = Utils.Hash.getSecrets(padParsed.type, padParsed.hash, data.password);
var padCrypto = Utils.Crypto.createEncryptor(padSecret.keys);
try {
var value = {};
value[edPublic] = padCrypto.encrypt(JSON.stringify({
notifications: notifications,
curvePublic: curvePublic
}));
var msg = {
channel: data.channel,
command: 'ADD_MAILBOX',
value: value
};
Cryptpad.setPadMetadata(msg, function (res) {
if (res.error) { console.error(res.error); }
});
} catch (err) {
return void console.error(err);
}
});
sframeChan.on('Q_IMPORT_MEDIATAG', function (obj, cb) {
var key = obj.key;
var channel = obj.channel;
var hash = Utils.Hash.getFileHashFromKeys({
version: 1,
channel: channel,
keys: {
fileKeyStr: key
}
});
var href = '/file/#' + hash;
var data = {
title: obj.name,
href: href,
channel: channel,
owners: obj.owners,
forceSave: true,
};
Cryptpad.setPadTitle(data, function (err) {
Cryptpad.setPadAttribute('fileType', obj.type, null, href);
cb(err);
});
});
sframeChan.on('Q_SETTINGS_SET_DISPLAY_NAME', function (newName, cb) {
Cryptpad.setDisplayName(newName, function (err) {
if (err) {
console.log("Couldn't set username");
console.error(err);
cb('ERROR');
return;
}
Cryptpad.changeMetadata();
cb();
});
});
sframeChan.on('Q_LOGOUT', function (data, cb) {
Utils.LocalStore.logout(cb);
});
sframeChan.on('EV_NOTIFY', function (data) {
Notifier.notify(data);
});
sframeChan.on('Q_MOVE_TO_TRASH', function (data, cb) {
cb = cb || $.noop;
if (readOnly && hashes.editHash) {
var appPath = window.location.pathname;
Cryptpad.moveToTrash(cb, appPath + '#' + hashes.editHash);
return;
}
Cryptpad.moveToTrash(cb);
});
sframeChan.on('Q_SAVE_AS_TEMPLATE', function (data, cb) {
Cryptpad.saveAsTemplate(Cryptget.put, data, cb);
});
// Messaging
sframeChan.on('Q_SEND_FRIEND_REQUEST', function (data, cb) {
Cryptpad.messaging.sendFriendRequest(data, cb);
});
sframeChan.on('Q_ANSWER_FRIEND_REQUEST', function (data, cb) {
Cryptpad.messaging.answerFriendRequest(data, cb);
});
sframeChan.on('Q_ANON_GET_PREVIEW_CONTENT', function (data, cb) {
Cryptpad.anonGetPreviewContent(data, cb);
});
// History
sframeChan.on('Q_GET_FULL_HISTORY', function (data, cb) {
var crypto = Crypto.createEncryptor(secret.keys);
Cryptpad.getFullHistory({
channel: secret.channel,
validateKey: secret.keys.validateKey
}, function (encryptedMsgs) {
var nt = nThen;
var decryptedMsgs = [];
var total = encryptedMsgs.length;
encryptedMsgs.forEach(function (msg, i) {
nt = nt(function (waitFor) {
// The 3rd parameter "true" means we're going to skip signature validation.
// We don't need it since the message is already validated serverside by hk
decryptedMsgs.push(crypto.decrypt(msg, true, true));
setTimeout(waitFor(function () {
sframeChan.event('EV_FULL_HISTORY_STATUS', (i+1)/total);
}));
}).nThen;
});
nt(function () {
cb(decryptedMsgs);
});
});
});
sframeChan.on('Q_GET_HISTORY_RANGE', function (data, cb) {
var nSecret = secret;
if (cfg.isDrive) {
// Shared folder or user hash or fs hash
var hash = Utils.LocalStore.getUserHash() || Utils.LocalStore.getFSHash();
if (data.sharedFolder) { hash = data.sharedFolder.hash; }
if (hash) {
var password = (data.sharedFolder && data.sharedFolder.password) || undefined;
nSecret = Utils.Hash.getSecrets('drive', hash, password);
}
}
var channel = nSecret.channel;
var validate = nSecret.keys.validateKey;
var crypto = Crypto.createEncryptor(nSecret.keys);
Cryptpad.getHistoryRange({
channel: channel,
validateKey: validate,
lastKnownHash: data.lastKnownHash
}, function (data) {
cb({
isFull: data.isFull,
messages: data.messages.map(function (msg) {
// The 3rd parameter "true" means we're going to skip signature validation.
// We don't need it since the message is already validated serverside by hk
return crypto.decrypt(msg, true, true);
}),
lastKnownHash: data.lastKnownHash
});
});
});
// Store
sframeChan.on('Q_DRIVE_GETDELETED', function (data, cb) {
Cryptpad.getDeletedPads(data, function (err, obj) {
if (err) { return void console.error(err); }
cb(obj);
});
});
sframeChan.on('Q_SESSIONSTORAGE_PUT', function (data, cb) {
if (typeof (data.value) === "undefined") {
delete sessionStorage[data.key];
} else {
sessionStorage[data.key] = data.value;
}
cb();
});
sframeChan.on('Q_IS_ONLY_IN_SHARED_FOLDER', function (data, cb) {
Cryptpad.isOnlyInSharedFolder(secret.channel, function (err, t) {
if (err) { return void cb({error: err}); }
cb(t);
});
});
// Present mode URL
sframeChan.on('Q_PRESENT_URL_GET_VALUE', function (data, cb) {
var parsed = Utils.Hash.parsePadUrl(currentPad.href);
cb(parsed.hashData && parsed.hashData.present);
});
sframeChan.on('EV_PRESENT_URL_SET_VALUE', function (data) {
// Update the rendered hash and the full hash with the "present" settings
var opts = parsed.getOptions();
opts.present = data;
// Full hash
currentPad.href = parsed.getUrl(opts);
if (parsed.hashData) { currentPad.hash = parsed.hashData.getHash(opts); }
// Rendered (maybe hidden) hash
var hiddenParsed = Utils.Hash.parsePadUrl(window.location.href);
window.location.href = hiddenParsed.getUrl(opts);
});
// File upload
var onFileUpload = function (sframeChan, data, cb) {
require(['/common/outer/upload.js'], function (Files) {
var sendEvent = function (data) {
sframeChan.event("EV_FILE_UPLOAD_STATE", data);
};
var updateProgress = function (progressValue) {
sendEvent({
uid: data.uid,
progress: progressValue
});
};
var onComplete = function (href) {
sendEvent({
complete: true,
uid: data.uid,
href: href
});
};
var onError = function (e) {
sendEvent({
uid: data.uid,
error: e
});
};
var onPending = function (cb) {
sframeChan.query('Q_CANCEL_PENDING_FILE_UPLOAD', {
uid: data.uid
}, function (err, data) {
if (data) {
cb();
}
});
};
data.blob = Crypto.Nacl.util.decodeBase64(data.blob);
Files.upload(data, data.noStore, Cryptpad, updateProgress, onComplete, onError, onPending);
cb();
});
};
sframeChan.on('Q_UPLOAD_FILE', function (data, cb) {
onFileUpload(sframeChan, data, cb);
});
// File picker
var FP = {};
var initFilePicker = function (cfg) {
// cfg.hidden means pre-loading the filepicker while keeping it hidden.
// if cfg.hidden is true and the iframe already exists, do nothing
if (!FP.$iframe) {
var config = {};
config.onFilePicked = function (data) {
sframeChan.event('EV_FILE_PICKED', data);
};
config.onClose = function () {
FP.$iframe.hide();
};
config.onFileUpload = onFileUpload;
config.types = cfg;
config.addCommonRpc = addCommonRpc;
config.modules = {
Cryptpad: Cryptpad,
SFrameChannel: SFrameChannel,
Utils: Utils
};
FP.$iframe = $('<iframe>', {id: 'sbox-filePicker-iframe'}).appendTo($('body'));
FP.picker = FilePicker.create(config);
} else if (!cfg.hidden) {
FP.$iframe.show();
FP.picker.refresh(cfg);
}
if (cfg.hidden) {
FP.$iframe.hide();
return;
}
FP.$iframe.focus();
};
sframeChan.on('EV_FILE_PICKER_OPEN', function (data) {
initFilePicker(data);
});
// Share modal
var ShareModal = {};
var initShareModal = function (cfg) {
cfg.hashes = hashes;
cfg.password = password;
cfg.isTemplate = isTemplate;
// cfg.hidden means pre-loading the filepicker while keeping it hidden.
// if cfg.hidden is true and the iframe already exists, do nothing
if (!ShareModal.$iframe) {
var config = {};
config.onShareAction = function (data) {
sframeChan.event('EV_SHARE_ACTION', data);
};
config.onClose = function () {
ShareModal.$iframe.hide();
};
config.data = cfg;
config.addCommonRpc = addCommonRpc;
config.modules = {
Cryptpad: Cryptpad,
SFrameChannel: SFrameChannel,
Utils: Utils
};
ShareModal.$iframe = $('<iframe>', {id: 'sbox-share-iframe'}).appendTo($('body'));
ShareModal.modal = Share.create(config);
} else if (!cfg.hidden) {
ShareModal.modal.refresh(cfg, function () {
ShareModal.$iframe.show();
});
}
if (cfg.hidden) {
ShareModal.$iframe.hide();
return;
}
ShareModal.$iframe.focus();
};
sframeChan.on('EV_SHARE_OPEN', function (data) {
initShareModal(data || {});
});
sframeChan.on('Q_TEMPLATE_USE', function (data, cb) {
Cryptpad.useTemplate(data, Cryptget, cb);
});
sframeChan.on('Q_TEMPLATE_EXIST', function (type, cb) {
Cryptpad.listTemplates(type, function (err, templates) {
cb(templates.length > 0);
});
});
var getKey = function (href, channel) {
var parsed = Utils.Hash.parsePadUrl(href);
return 'thumbnail-' + parsed.type + '-' + channel;
};
sframeChan.on('Q_CREATE_TEMPLATES', function (type, cb) {
Cryptpad.getSecureFilesList({
types: [type],
where: ['template']
}, function (err, data) {
// NOTE: Never return data directly!
if (err) { return void cb({error: err}); }
var res = [];
nThen(function (waitFor) {
Object.keys(data).map(function (el) {
var k = getKey(data[el].href, data[el].channel);
Utils.LocalStore.getThumbnail(k, waitFor(function (e, thumb) {
res.push({
id: el,
name: data[el].filename || data[el].title || '?',
thumbnail: thumb,
used: data[el].used || 0
});
}));
});
}).nThen(function () {
cb({data: res});
});
});
});
sframeChan.on('Q_GET_FILE_THUMBNAIL', function (data, cb) {
if (!Cryptpad.fromFileData || !Cryptpad.fromFileData.href) {
return void cb({
error: "EINVAL",
});
}
var key = getKey(Cryptpad.fromFileData.href, Cryptpad.fromFileData.channel);
Utils.LocalStore.getThumbnail(key, function (e, data) {
if (data === "EMPTY") { data = null; }
cb({
error: e,
data: data
});
});
});
sframeChan.on('Q_PIN_GET_USAGE', function (teamId, cb) {
Cryptpad.isOverPinLimit(teamId, function (err, overLimit, data) {
cb({
error: err,
data: data
});
});
});
sframeChan.on('Q_LANGUAGE_SET', function (data, cb) {
Cryptpad.setLanguage(data, cb);
});
sframeChan.on('Q_GET_ALL_TAGS', function (data, cb) {
Cryptpad.listAllTags(function (err, tags) {
cb({
error: err,
tags: tags
});
});
});
sframeChan.on('Q_BLOB_PASSWORD_CHANGE', function (data, cb) {
data.href = data.href || currentPad.href;
var onPending = function (cb) {
sframeChan.query('Q_BLOB_PASSWORD_CHANGE_PENDING', null, function (err, obj) {
if (obj && obj.cancel) { cb(); }
});
};
var updateProgress = function (p) {
sframeChan.event('EV_BLOB_PASSWORD_CHANGE_PROGRESS', p);
};
Cryptpad.changeBlobPassword(data, {
onPending: onPending,
updateProgress: updateProgress
}, cb);
});
sframeChan.on('Q_OO_PASSWORD_CHANGE', function (data, cb) {
data.href = data.href || currentPad.href;
Cryptpad.changeOOPassword(data, cb);
});
sframeChan.on('Q_PAD_PASSWORD_CHANGE', function (data, cb) {
data.href = data.href || currentPad.href;
Cryptpad.changePadPassword(Cryptget, Crypto, data, cb);
});
sframeChan.on('Q_CHANGE_USER_PASSWORD', function (data, cb) {
Cryptpad.changeUserPassword(Cryptget, edPublic, data, cb);
});
sframeChan.on('Q_WRITE_LOGIN_BLOCK', function (data, cb) {
Cryptpad.writeLoginBlock(data, cb);
});
sframeChan.on('Q_REMOVE_LOGIN_BLOCK', function (data, cb) {
Cryptpad.removeLoginBlock(data, cb);
});
// It seems we have performance issues when we open and close a lot of channels over
// the same network, maybe a memory leak. To fix this, we kill and create a new
// network every 30 cryptget calls (1 call = 1 channel)
var cgNetwork;
var whenCGReady = function (cb) {
if (cgNetwork && cgNetwork !== true) { console.log(cgNetwork); return void cb(); }
setTimeout(function () {
whenCGReady(cb);
}, 500);
};
var i = 0;
sframeChan.on('Q_CRYPTGET', function (data, cb) {
var todo = function () {
data.opts.network = cgNetwork;
Cryptget.get(data.hash, function (err, val) {
cb({
error: err,
data: val
});
}, data.opts, function (progress) {
sframeChan.event("EV_CRYPTGET_PROGRESS", {
hash: data.hash,
progress: progress,
});
});
};
//return void todo();
if (i > 30) {
i = 0;
cgNetwork = undefined;
}
i++;
if (!cgNetwork) {
cgNetwork = true;
return void Cryptpad.makeNetwork(function (err, nw) {
console.log(nw);
cgNetwork = nw;
todo();
});
} else if (cgNetwork === true) {
return void whenCGReady(todo);
}
todo();
});
sframeChan.on('EV_CRYPTGET_DISCONNECT', function () {
if (!cgNetwork) { return; }
cgNetwork.disconnect();
cgNetwork = undefined;
});
if (cfg.addRpc) {
cfg.addRpc(sframeChan, Cryptpad, Utils);
}
sframeChan.on('Q_CURSOR_OPENCHANNEL', function (data, cb) {
Cryptpad.cursor.execCommand({
cmd: 'INIT_CURSOR',
data: {
channel: data,
secret: secret
}
}, cb);
});
Cryptpad.cursor.onEvent.reg(function (data) {
sframeChan.event('EV_CURSOR_EVENT', data);
});
sframeChan.on('Q_CURSOR_COMMAND', function (data, cb) {
Cryptpad.cursor.execCommand(data, cb);
});
Cryptpad.universal.onEvent.reg(function (data) {
sframeChan.event('EV_UNIVERSAL_EVENT', data);
});
sframeChan.on('Q_UNIVERSAL_COMMAND', function (data, cb) {
Cryptpad.universal.execCommand(data, cb);
});
Cryptpad.onTimeoutEvent.reg(function () {
sframeChan.event('EV_WORKER_TIMEOUT');
});
sframeChan.on('EV_GIVE_ACCESS', function (data, cb) {
Cryptpad.padRpc.giveAccess(data, cb);
});
// REQUEST_ACCESS is used both to check IF we can contact an owner (send === false)
// AND also to send the request if we want (send === true)
sframeChan.on('Q_REQUEST_ACCESS', function (send, cb) {
if (readOnly && hashes.editHash) {
return void cb({error: 'ALREADYKNOWN'});
}
var owner, owners;
var crypto = Crypto.createEncryptor(secret.keys);
nThen(function (waitFor) {
// Try to get the owner's mailbox from the pad metadata first.
// If it's is an older owned pad, check if the owner is a friend
// or an acquaintance (from async-store directly in requestAccess)
Cryptpad.getPadMetadata({
channel: secret.channel
}, waitFor(function (obj) {
obj = obj || {};
if (obj.error) { return; }
owners = obj.owners;
var mailbox;
// Get the first available mailbox (the field can be an string or an object)
// TODO maybe we should send the request to all the owners?
if (typeof (obj.mailbox) === "string") {
mailbox = obj.mailbox;
} else if (obj.mailbox && obj.owners && obj.owners.length) {
mailbox = obj.mailbox[obj.owners[0]];
}
if (mailbox) {
try {
var dataStr = crypto.decrypt(mailbox, true, true);
var data = JSON.parse(dataStr);
if (!data.notifications || !data.curvePublic) { return; }
owner = data;
} catch (e) { console.error(e); }
}
}));
}).nThen(function () {
// If we are just checking (send === false) and there is a mailbox field, cb state true
// If there is no mailbox, we'll have to check if an owner is a friend in the worker
if (owner && !send) {
return void cb({state: true});
}
Cryptpad.padRpc.requestAccess({
send: send,
channel: secret.channel,
owner: owner,
owners: owners
}, cb);
});
});
sframeChan.on('EV_BURN_PAD', function (channel) {
if (!burnAfterReading) { return; }
Cryptpad.burnPad({
channel: channel,
ownerKey: burnAfterReading
});
});
if (cfg.messaging) {
Notifier.getPermission();
sframeChan.on('Q_CHAT_OPENPADCHAT', function (data, cb) {
Cryptpad.universal.execCommand({
type: 'messenger',
data: {
cmd: 'OPEN_PAD_CHAT',
data: {
channel: data,
secret: secret
}
}
}, cb);
});
}
// Chrome 68 on Mac contains a bug resulting in the page turning white after a few seconds
try {
if (navigator.platform.toUpperCase().indexOf('MAC') >= 0 &&
!localStorage.CryptPad_chrome68) {
var isChrome = !!window.chrome && !!window.chrome.webstore;
var getChromeVersion = function () {
var raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
return raw ? parseInt(raw[2], 10) : false;
};
if (isChrome && getChromeVersion() === 68) {
sframeChan.whenReg('EV_CHROME_68', function () {
sframeChan.event("EV_CHROME_68");
localStorage.CryptPad_chrome68 = "1";
});
}
}
} catch (e) {}
// Join the netflux channel
var rtStarted = false;
var startRealtime = function (rtConfig) {
rtConfig = rtConfig || {};
rtStarted = true;
var replaceHash = function (hash) {
// XXX Always put the full hash here.
// The pad has just been created but is not stored yet. We'll switch
// to hidden hash once the pad is stored
if (window.history && window.history.replaceState) {
if (!/^#/.test(hash)) { hash = '#' + hash; }
window.history.replaceState({}, window.document.title, hash);
if (typeof(window.onhashchange) === 'function') {
window.onhashchange();
}
return;
}
window.location.hash = hash;
};
if (burnAfterReading) {
Cryptpad.padRpc.onReadyEvent.reg(function () {
Cryptpad.burnPad({
password: password,
href: currentPad.href,
channel: secret.channel,
ownerKey: burnAfterReading
});
});
}
var cpNfCfg = {
sframeChan: sframeChan,
channel: secret.channel,
padRpc: Cryptpad.padRpc,
validateKey: secret.keys.validateKey || undefined,
isNewHash: isNewHash,
readOnly: readOnly,
crypto: Crypto.createEncryptor(secret.keys),
onConnect: function () {
if (currentPad.hash && currentPad.hash !== '#') {
/*window.location = parsed.getUrl({
present: parsed.hashData.present,
embed: parsed.hashData.embed
});*/
return;
}
if (readOnly || cfg.noHash) { return; }
replaceHash(Utils.Hash.getEditHashFromKeys(secret));
}
};
nThen(function (waitFor) {
if (isNewFile && cfg.owned && !currentPad.hash) {
Cryptpad.getMetadata(waitFor(function (err, m) {
cpNfCfg.owners = [m.priv.edPublic];
}));
} else if (isNewFile && !cfg.useCreationScreen && currentPad.hash) {
console.log("new file with hash in the address bar in an app without pcs and which requires owners");
sframeChan.onReady(function () {
sframeChan.query("EV_LOADING_ERROR", "DELETED");
});
waitFor.abort();
}
}).nThen(function () {
Object.keys(rtConfig).forEach(function (k) {
cpNfCfg[k] = rtConfig[k];
});
CpNfOuter.start(cpNfCfg);
});
};
sframeChan.on('Q_CREATE_PAD', function (data, cb) {
if (!isNewFile || rtStarted) { return; }
// Create a new hash
password = data.password;
var newHash = Utils.Hash.createRandomHash(parsed.type, password);
secret = Utils.secret = Utils.Hash.getSecrets(parsed.type, newHash, password);
Utils.crypto = Utils.Crypto.createEncryptor(Utils.secret.keys);
// Update the hash in the address bar
var ohc = window.onhashchange;
window.onhashchange = function () {};
window.location.hash = newHash;
currentPad.hash = newHash;
currentPad.href = '/' + parsed.type + '/#' + newHash;
window.onhashchange = ohc;
ohc({reset: true});
// Update metadata values and send new metadata inside
parsed = Utils.Hash.parsePadUrl(currentPad.href);
defaultTitle = Utils.UserObject.getDefaultName(parsed);
hashes = Utils.Hash.getHashes(secret);
readOnly = false;
updateMeta();
var rtConfig = {
metadata: {}
};
if (data.team) {
Cryptpad.initialTeam = data.team.id;
}
if (data.owned && data.team && data.team.edPublic) {
rtConfig.metadata.owners = [data.team.edPublic];
} else if (data.owned) {
rtConfig.metadata.owners = [edPublic];
rtConfig.metadata.mailbox = {};
rtConfig.metadata.mailbox[edPublic] = Utils.crypto.encrypt(JSON.stringify({
notifications: notifications,
curvePublic: curvePublic
}));
}
if (data.expire) {
rtConfig.metadata.expire = data.expire;
}
rtConfig.metadata.validateKey = (secret.keys && secret.keys.validateKey) || undefined;
Utils.rtConfig = rtConfig;
nThen(function(waitFor) {
if (data.templateId) {
if (data.templateId === -1) {
initialPathInDrive = ['template'];
return;
}
Cryptpad.getPadData(data.templateId, waitFor(function (err, d) {
data.template = d.href;
}));
}
}).nThen(function () {
var cryptputCfg = $.extend(true, {}, rtConfig, {password: password});
if (data.template) {
// Pass rtConfig to useTemplate because Cryptput will create the file and
// we need to have the owners and expiration time in the first line on the
// server
Cryptpad.useTemplate({
href: data.template
}, Cryptget, function () {
startRealtime();
cb();
}, cryptputCfg);
return;
}
// if we open a new code from a file
if (Cryptpad.fromFileData) {
Cryptpad.useFile(Cryptget, function () {
startRealtime();
cb();
}, cryptputCfg);
return;
}
// Start realtime outside the iframe and callback
startRealtime(rtConfig);
cb();
});
});
sframeChan.on('EV_BURN_AFTER_READING', function () {
startRealtime();
});
sframeChan.ready();
Utils.Feedback.reportAppUsage();
if (!realtime && !Test.testing) { return; }
if (isNewFile && cfg.useCreationScreen && !Test.testing) { return; }
if (burnAfterReading) { return; }
//if (isNewFile && Utils.LocalStore.isLoggedIn()
// && AppConfig.displayCreationScreen && cfg.useCreationScreen) { return; }
startRealtime();
});
};
return common;
});