Merge branch 'staging' into cba

pull/1/head
yflory 5 years ago
commit dd814713dd

@ -89,6 +89,14 @@ module.exports = {
*/
//httpSafePort: 3001,
/* CryptPad will launch a child process for every core available
* in order to perform CPU-intensive tasks in parallel.
* Some host environments may have a very large number of cores available
* or you may want to limit how much computing power CryptPad can take.
* If so, set 'maxWorkers' to a positive integer.
*/
// maxWorkers: 4,
/* =====================
* Admin
* ===================== */

@ -2,7 +2,7 @@ html, body {
margin: 0px;
padding: 0px;
}
#sbox-iframe, #sbox-share-iframe, #sbox-filePicker-iframe {
#sbox-iframe, #sbox-secure-iframe {
position: fixed;
top:0; left:0;
bottom:0; right:0;

@ -54,6 +54,7 @@ server {
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options nosniff;
add_header Access-Control-Allow-Origin "*";
# add_header X-Frame-Options "SAMEORIGIN";
# Insert the path to your CryptPad repository root here

@ -1,4 +1,5 @@
/*jshint esversion: 6 */
/* globals process */
const nThen = require("nthen");
const getFolderSize = require("get-folder-size");
const Util = require("../common-util");
@ -50,6 +51,7 @@ var getCacheStats = function (env, server, cb) {
metaSize: metaSize,
channel: channelCount,
channelSize: channelSize,
memoryUsage: process.memoryUsage(),
});
};

@ -54,16 +54,8 @@ Channel.clearOwnedChannel = function (Env, safeKey, channelId, cb, Server) {
});
};
Channel.removeOwnedChannel = function (Env, safeKey, channelId, cb, Server) {
if (typeof(channelId) !== 'string' || !Core.isValidId(channelId)) {
return cb('INVALID_ARGUMENTS');
}
var archiveOwnedChannel = function (Env, safeKey, channelId, cb, Server) {
var unsafeKey = Util.unescapeKeyCharacters(safeKey);
if (Env.blobStore.isFileId(channelId)) {
return void Env.removeOwnedBlob(channelId, safeKey, cb);
}
Metadata.getMetadata(Env, channelId, function (err, metadata) {
if (err) { return void cb(err); }
if (!Core.hasOwners(metadata)) { return void cb('E_NO_OWNERS'); }
@ -124,6 +116,24 @@ Channel.removeOwnedChannel = function (Env, safeKey, channelId, cb, Server) {
});
};
Channel.removeOwnedChannel = function (Env, safeKey, channelId, __cb, Server) {
var _cb = Util.once(Util.mkAsync(__cb));
if (typeof(channelId) !== 'string' || !Core.isValidId(channelId)) {
return _cb('INVALID_ARGUMENTS');
}
// archiving large channels or files can be expensive, so do it one at a time
// for any given user to ensure that nobody can use too much of the server's resources
Env.queueDeletes(safeKey, function (next) {
var cb = Util.both(_cb, next);
if (Env.blobStore.isFileId(channelId)) {
return void Env.removeOwnedBlob(channelId, safeKey, cb);
}
archiveOwnedChannel(Env, safeKey, channelId, cb, Server);
});
};
Channel.trimHistory = function (Env, safeKey, data, cb) {
if (!(data && typeof(data.channel) === 'string' && typeof(data.hash) === 'string' && data.hash.length === 64)) {
return void cb('INVALID_ARGS');

@ -49,16 +49,19 @@ var loadUserPins = function (Env, safeKey, cb) {
// only put this into the cache if it completes
session.channels = value;
}
session.channels = value;
done(value);
});
});
};
var truthyKeys = function (O) {
try {
return Object.keys(O).filter(function (k) {
return O[k];
});
} catch (err) {
return [];
}
};
var getChannelList = Pinning.getChannelList = function (Env, safeKey, _cb) {

@ -31,13 +31,13 @@ module.exports.create = function (config, cb) {
// and more easily share state between historyKeeper and rpc
const Env = {
Log: Log,
// tasks
// store
id: Crypto.randomBytes(8).toString('hex'),
metadata_cache: {},
channel_cache: {},
queueStorage: WriteQueue(),
queueDeletes: WriteQueue(),
batchIndexReads: BatchRead("HK_GET_INDEX"),
batchMetadata: BatchRead('GET_METADATA'),
@ -98,7 +98,7 @@ module.exports.create = function (config, cb) {
paths.staging = keyOrDefaultString('blobStagingPath', './blobstage');
paths.blob = keyOrDefaultString('blobPath', './blob');
Env.defaultStorageLimit = typeof(config.defaultStorageLimit) === 'number' && config.defaultStorageLimit > 0?
Env.defaultStorageLimit = typeof(config.defaultStorageLimit) === 'number' && config.defaultStorageLimit >= 0?
config.defaultStorageLimit:
Core.DEFAULT_LIMIT;
@ -252,17 +252,14 @@ module.exports.create = function (config, cb) {
channelExpirationMs: config.channelExpirationMs,
verbose: config.verbose,
openFileLimit: config.openFileLimit,
maxWorkers: config.maxWorkers,
}, w(function (err) {
if (err) {
throw new Error(err);
}
}));
}).nThen(function (w) {
// create a task store (for scheduling tasks)
require("./storage/tasks").create(config, w(function (e, tasks) {
if (e) { throw e; }
Env.tasks = tasks;
}));
if (config.disableIntegratedTasks) { return; }
config.intervals = config.intervals || {};

@ -529,7 +529,7 @@ const handleFirstMessage = function (Env, channelName, metadata) {
if(metadata.expire && typeof(metadata.expire) === 'number') {
// the fun part...
// the user has said they want this pad to expire at some point
Env.tasks.write(metadata.expire, "EXPIRE", [ channelName ], function (err) {
Env.writeTask(metadata.expire, "EXPIRE", [ channelName ], function (err) {
if (err) {
// if there is an error, we don't want to crash the whole server...
// just log it, and if there's a problem you'll be able to fix it
@ -621,7 +621,10 @@ const handleGetHistory = function (Env, Server, seq, userId, parsed) {
Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, JSON.stringify(msg)], readMore);
}, (err) => {
if (err && err.code !== 'ENOENT') {
if (err.message !== 'EINVAL') { Log.error("HK_GET_HISTORY", err); }
if (err.message !== 'EINVAL') { Log.error("HK_GET_HISTORY", {
err: err && err.message,
stack: err && err.stack,
}); }
const parsedMsg = {error:err.message, channel: channelName, txid: txid};
Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, JSON.stringify(parsedMsg)]);
return;
@ -669,6 +672,7 @@ const handleGetHistoryRange = function (Env, Server, seq, userId, parsed) {
if (!Array.isArray(messages)) { messages = []; }
// FIXME this reduction could be done in the worker instead of the main process
var toSend = [];
if (typeof (desiredMessages) === "number") {
toSend = messages.slice(-desiredMessages);

@ -1,113 +0,0 @@
/* jshint esversion: 6 */
/* global process */
const Nacl = require('tweetnacl/nacl-fast');
const COMMANDS = {};
COMMANDS.INLINE = function (data, cb) {
var signedMsg;
try {
signedMsg = Nacl.util.decodeBase64(data.msg);
} catch (e) {
return void cb('E_BAD_MESSAGE');
}
var validateKey;
try {
validateKey = Nacl.util.decodeBase64(data.key);
} catch (e) {
return void cb("E_BADKEY");
}
// validate the message
const validated = Nacl.sign.open(signedMsg, validateKey);
if (!validated) {
return void cb("FAILED");
}
cb();
};
const checkDetachedSignature = function (signedMsg, signature, publicKey) {
if (!(signedMsg && publicKey)) { return false; }
var signedBuffer;
var pubBuffer;
var signatureBuffer;
try {
signedBuffer = Nacl.util.decodeUTF8(signedMsg);
} catch (e) {
throw new Error("INVALID_SIGNED_BUFFER");
}
try {
pubBuffer = Nacl.util.decodeBase64(publicKey);
} catch (e) {
throw new Error("INVALID_PUBLIC_KEY");
}
try {
signatureBuffer = Nacl.util.decodeBase64(signature);
} catch (e) {
throw new Error("INVALID_SIGNATURE");
}
if (pubBuffer.length !== 32) {
throw new Error("INVALID_PUBLIC_KEY_LENGTH");
}
if (signatureBuffer.length !== 64) {
throw new Error("INVALID_SIGNATURE_LENGTH");
}
if (Nacl.sign.detached.verify(signedBuffer, signatureBuffer, pubBuffer) !== true) {
throw new Error("FAILED");
}
};
COMMANDS.DETACHED = function (data, cb) {
try {
checkDetachedSignature(data.msg, data.sig, data.key);
} catch (err) {
return void cb(err && err.message);
}
cb();
};
COMMANDS.HASH_CHANNEL_LIST = function (data, cb) {
var channels = data.channels;
if (!Array.isArray(channels)) { return void cb('INVALID_CHANNEL_LIST'); }
var uniques = [];
channels.forEach(function (a) {
if (uniques.indexOf(a) === -1) { uniques.push(a); }
});
uniques.sort();
var hash = Nacl.util.encodeBase64(Nacl.hash(Nacl
.util.decodeUTF8(JSON.stringify(uniques))));
cb(void 0, hash);
};
process.on('message', function (data) {
if (!data || !data.txid) {
return void process.send({
error:'E_INVAL'
});
}
const cb = function (err, value) {
process.send({
txid: data.txid,
error: err,
value: value,
});
};
const command = COMMANDS[data.command];
if (typeof(command) !== 'function') {
return void cb("E_BAD_COMMAND");
}
command(data, cb);
});

@ -12,6 +12,7 @@ const Core = require("../commands/core");
const Saferphore = require("saferphore");
const Logger = require("../log");
const Tasks = require("../storage/tasks");
const Nacl = require('tweetnacl/nacl-fast');
const Env = {
Log: {},
@ -418,6 +419,10 @@ const runTasks = function (data, cb) {
Env.tasks.runAll(cb);
};
const writeTask = function (data, cb) {
Env.tasks.write(data.time, data.task_command, data.args, cb);
};
const COMMANDS = {
COMPUTE_INDEX: computeIndex,
COMPUTE_METADATA: computeMetadata,
@ -430,6 +435,92 @@ const COMMANDS = {
GET_HASH_OFFSET: getHashOffset,
REMOVE_OWNED_BLOB: removeOwnedBlob,
RUN_TASKS: runTasks,
WRITE_TASK: writeTask,
};
COMMANDS.INLINE = function (data, cb) {
var signedMsg;
try {
signedMsg = Nacl.util.decodeBase64(data.msg);
} catch (e) {
return void cb('E_BAD_MESSAGE');
}
var validateKey;
try {
validateKey = Nacl.util.decodeBase64(data.key);
} catch (e) {
return void cb("E_BADKEY");
}
// validate the message
const validated = Nacl.sign.open(signedMsg, validateKey);
if (!validated) {
return void cb("FAILED");
}
cb();
};
const checkDetachedSignature = function (signedMsg, signature, publicKey) {
if (!(signedMsg && publicKey)) { return false; }
var signedBuffer;
var pubBuffer;
var signatureBuffer;
try {
signedBuffer = Nacl.util.decodeUTF8(signedMsg);
} catch (e) {
throw new Error("INVALID_SIGNED_BUFFER");
}
try {
pubBuffer = Nacl.util.decodeBase64(publicKey);
} catch (e) {
throw new Error("INVALID_PUBLIC_KEY");
}
try {
signatureBuffer = Nacl.util.decodeBase64(signature);
} catch (e) {
throw new Error("INVALID_SIGNATURE");
}
if (pubBuffer.length !== 32) {
throw new Error("INVALID_PUBLIC_KEY_LENGTH");
}
if (signatureBuffer.length !== 64) {
throw new Error("INVALID_SIGNATURE_LENGTH");
}
if (Nacl.sign.detached.verify(signedBuffer, signatureBuffer, pubBuffer) !== true) {
throw new Error("FAILED");
}
};
COMMANDS.DETACHED = function (data, cb) {
try {
checkDetachedSignature(data.msg, data.sig, data.key);
} catch (err) {
return void cb(err && err.message);
}
cb();
};
COMMANDS.HASH_CHANNEL_LIST = function (data, cb) {
var channels = data.channels;
if (!Array.isArray(channels)) { return void cb('INVALID_CHANNEL_LIST'); }
var uniques = [];
channels.forEach(function (a) {
if (uniques.indexOf(a) === -1) { uniques.push(a); }
});
uniques.sort();
var hash = Nacl.util.encodeBase64(Nacl.hash(Nacl
.util.decodeUTF8(JSON.stringify(uniques))));
cb(void 0, hash);
};
process.on('message', function (data) {

@ -3,103 +3,14 @@
const Util = require("../common-util");
const nThen = require('nthen');
const OS = require("os");
const numCPUs = OS.cpus().length;
const { fork } = require('child_process');
const Workers = module.exports;
const PID = process.pid;
const CRYPTO_PATH = 'lib/workers/crypto-worker';
const DB_PATH = 'lib/workers/db-worker';
const MAX_JOBS = 16;
Workers.initializeValidationWorkers = function (Env) {
if (typeof(Env.validateMessage) !== 'undefined') {
return void console.error("validation workers are already initialized");
}
// Create our workers
const workers = [];
for (let i = 0; i < numCPUs; i++) {
workers.push(fork(CRYPTO_PATH));
}
const response = Util.response(function (errLabel, info) {
Env.Log.error('HK_VALIDATE_WORKER__' + errLabel, info);
});
var initWorker = function (worker) {
worker.on('message', function (res) {
if (!res || !res.txid) { return; }
response.handle(res.txid, [res.error, res.value]);
});
var substituteWorker = Util.once( function () {
Env.Log.info("SUBSTITUTE_VALIDATION_WORKER", '');
var idx = workers.indexOf(worker);
if (idx !== -1) {
workers.splice(idx, 1);
}
// Spawn a new one
var w = fork(CRYPTO_PATH);
workers.push(w);
initWorker(w);
});
// Spawn a new process in one ends
worker.on('exit', substituteWorker);
worker.on('close', substituteWorker);
worker.on('error', function (err) {
substituteWorker();
Env.Log.error('VALIDATION_WORKER_ERROR', {
error: err,
});
});
};
workers.forEach(initWorker);
var nextWorker = 0;
const send = function (msg, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
// let's be paranoid about asynchrony and only calling back once..
nextWorker = (nextWorker + 1) % workers.length;
if (workers.length === 0 || typeof(workers[nextWorker].send) !== 'function') {
return void cb("INVALID_WORKERS");
}
var txid = msg.txid = Util.uid();
// expect a response within 45s
response.expect(txid, cb, 60000);
// Send the request
workers[nextWorker].send(msg);
};
Env.validateMessage = function (signedMsg, key, cb) {
send({
msg: signedMsg,
key: key,
command: 'INLINE',
}, cb);
};
Env.checkSignature = function (signedMsg, signature, publicKey, cb) {
send({
command: 'DETACHED',
sig: signature,
msg: signedMsg,
key: publicKey,
}, cb);
};
Env.hashChannelList = function (channels, cb) {
send({
command: 'HASH_CHANNEL_LIST',
channels: channels,
}, cb);
};
};
Workers.initializeIndexWorkers = function (Env, config, _cb) {
Workers.initialize = function (Env, config, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
const workers = [];
@ -124,16 +35,60 @@ Workers.initializeIndexWorkers = function (Env, config, _cb) {
return response.expected(id)? guid(): id;
};
var workerIndex = 0;
var sendCommand = function (msg, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
var workerOffset = -1;
var queue = [];
var getAvailableWorkerIndex = function () {
// If there is already a backlog of tasks you can avoid some work
// by going to the end of the line
if (queue.length) { return -1; }
var L = workers.length;
if (L === 0) {
Log.error('NO_WORKERS_AVAILABLE', {
queue: queue.length,
});
return -1;
}
workerIndex = (workerIndex + 1) % workers.length;
if (!isWorker(workers[workerIndex])) {
return void cb("NO_WORKERS");
// cycle through the workers once
// start from a different offset each time
// return -1 if none are available
workerOffset = (workerOffset + 1) % L;
var temp;
for (let i = 0; i < L; i++) {
temp = (workerOffset + i) % L;
/* I'd like for this condition to be more efficient
(`Object.keys` is sub-optimal) but I found some bugs in my initial
implementation stemming from a task counter variable going out-of-sync
with reality when a worker crashed and its tasks were re-assigned to
its substitute. I'm sure it can be done correctly and efficiently,
but this is a relatively easy way to make sure it's always up to date.
We'll see how it performs in practice before optimizing.
*/
if (workers[temp] && Object.keys(workers[temp]).length < MAX_JOBS) {
return temp;
}
}
return -1;
};
var sendCommand = function (msg, _cb) {
var index = getAvailableWorkerIndex();
var state = workers[index];
// if there is no worker available:
if (!isWorker(state)) {
// queue the message for when one becomes available
queue.push({
msg: msg,
cb: _cb,
});
return;
}
var state = workers[workerIndex];
var cb = Util.once(Util.mkAsync(_cb));
const txid = guid();
msg.txid = txid;
@ -141,14 +96,42 @@ Workers.initializeIndexWorkers = function (Env, config, _cb) {
// track which worker is doing which jobs
state.tasks[txid] = msg;
response.expect(txid, function (err, value) {
// clean up when you get a response
delete state[txid];
cb(err, value);
}, 60000);
response.expect(txid, cb, 60000);
state.worker.send(msg);
};
var handleResponse = function (state, res) {
if (!res) { return; }
// handle log messages before checking if it was addressed to your PID
// it might still be useful to know what happened inside an orphaned worker
if (res.log) {
return void handleLog(res.log, res.label, res.info);
}
// but don't bother handling things addressed to other processes
// since it's basically guaranteed not to work
if (res.pid !== PID) {
return void Log.error("WRONG_PID", res);
}
if (!res.txid) { return; }
response.handle(res.txid, [res.error, res.value]);
delete state.tasks[res.txid];
if (!queue.length) { return; }
var nextMsg = queue.shift();
/* `nextMsg` was at the top of the queue.
We know that a job just finished and all of this code
is synchronous, so calling `sendCommand` should take the worker
which was just freed up. This is somewhat fragile though, so
be careful if you want to modify this block. The risk is that
we take something that was at the top of the queue and push it
to the back because the following msg took its place. OR, in an
even worse scenario, we cycle through the queue but don't run anything.
*/
sendCommand(nextMsg.msg, nextMsg.cb);
};
const initWorker = function (worker, cb) {
const txid = guid();
@ -170,19 +153,7 @@ Workers.initializeIndexWorkers = function (Env, config, _cb) {
});
worker.on('message', function (res) {
if (!res) { return; }
// handle log messages before checking if it was addressed to your PID
// it might still be useful to know what happened inside an orphaned worker
if (res.log) {
return void handleLog(res.log, res.label, res.info);
}
// but don't bother handling things addressed to other processes
// since it's basically guaranteed not to work
if (res.pid !== PID) {
return void Log.error("WRONG_PID", res);
}
response.handle(res.txid, [res.error, res.value]);
handleResponse(state, res);
});
var substituteWorker = Util.once(function () {
@ -222,7 +193,32 @@ Workers.initializeIndexWorkers = function (Env, config, _cb) {
};
nThen(function (w) {
OS.cpus().forEach(function () {
const max = config.maxWorkers;
var limit;
if (typeof(max) !== 'undefined') {
// the admin provided a limit on the number of workers
if (typeof(max) === 'number' && !isNaN(max)) {
if (max < 1) {
Log.info("INSUFFICIENT_MAX_WORKERS", max);
limit = 1;
}
} else {
Log.error("INVALID_MAX_WORKERS", '[' + max + ']');
}
}
var logged;
OS.cpus().forEach(function (cpu, index) {
if (limit && index >= limit) {
if (!logged) {
logged = true;
Log.info('WORKER_LIMIT', "(Opting not to use available CPUs beyond " + index + ')');
}
return;
}
initWorker(fork(DB_PATH), w(function (err) {
if (!err) { return; }
w.abort();
@ -327,11 +323,42 @@ Workers.initializeIndexWorkers = function (Env, config, _cb) {
}, cb);
};
Env.writeTask = function (time, command, args, cb) {
sendCommand({
command: 'WRITE_TASK',
time: time,
task_command: command,
args: args,
}, cb);
};
// Synchronous crypto functions
Env.validateMessage = function (signedMsg, key, cb) {
sendCommand({
msg: signedMsg,
key: key,
command: 'INLINE',
}, cb);
};
Env.checkSignature = function (signedMsg, signature, publicKey, cb) {
sendCommand({
command: 'DETACHED',
sig: signature,
msg: signedMsg,
key: publicKey,
}, cb);
};
Env.hashChannelList = function (channels, cb) {
sendCommand({
command: 'HASH_CHANNEL_LIST',
channels: channels,
}, cb);
};
cb(void 0);
});
};
Workers.initialize = function (Env, config, cb) {
Workers.initializeValidationWorkers(Env);
Workers.initializeIndexWorkers(Env, config, cb);
};

@ -281,6 +281,7 @@ define([
if (MEDIA_TAG_MODES.indexOf(mode) !== -1) {
// Embedding is endabled
framework.setMediaTagEmbedder(function (mt) {
editor.focus();
editor.replaceSelection($(mt)[0].outerHTML);
});
} else {

@ -196,6 +196,7 @@ define([
]);
var $frame = $(frame);
frame.closeModal = function (cb) {
frame.closeModal = function () {}; // Prevent further calls
$frame.fadeOut(150, function () {
$frame.detach();
if (typeof(cb) === "function") { cb(); }
@ -464,16 +465,23 @@ define([
UI.createModal = function (cfg) {
var $body = cfg.$body || $('body');
var $blockContainer = $body.find('#'+cfg.id);
if (!$blockContainer.length) {
$blockContainer = $(h('div.cp-modal-container#'+cfg.id, {
var $blockContainer = cfg.id && $body.find('#'+cfg.id);
if (!$blockContainer || !$blockContainer.length) {
var id = '';
if (cfg.id) { id = '#'+cfg.id; }
$blockContainer = $(h('div.cp-modal-container'+id, {
tabindex: 1
}));
}
var deleted = false;
var hide = function () {
if (cfg.onClose) { return void cfg.onClose(); }
if (deleted) { return; }
$blockContainer.hide();
if (cfg.onClosed) { cfg.onClosed(); }
if (!cfg.id) {
deleted = true;
$blockContainer.remove();
}
if (cfg.onClose) { cfg.onClose(); }
};
$blockContainer.html('').appendTo($body);
var $block = $(h('div.cp-modal')).appendTo($blockContainer);

@ -175,9 +175,9 @@ define([
var common = config.common;
var sframeChan = common.getSframeChannel();
var title = config.title;
var friends = config.friends;
var friends = config.friends || {};
var teams = config.teams || {};
var myName = common.getMetadataMgr().getUserData().name;
if (!friends) { return; }
var order = [];
var smallCurves = Object.keys(friends).map(function (c) {
@ -206,6 +206,8 @@ define([
delete friends[curve];
});
var others = [];
if (Object.keys(friends).length) {
var friendsList = UIElements.getUserGrid(Messages.share_linkFriends, {
common: common,
data: friends,
@ -214,25 +216,10 @@ define([
}, refreshButtons);
var friendDiv = friendsList.div;
$div.append(friendDiv);
var others = friendsList.icons;
others = friendsList.icons;
}
var privateData = common.getMetadataMgr().getPrivateData();
var teamsData = Util.tryParse(JSON.stringify(privateData.teams)) || {};
var teams = {};
Object.keys(teamsData).forEach(function (id) {
// config.teamId only exists when we're trying to share a pad from a team drive
// In this case, we don't want to share the pad with the current team
if (config.teamId && config.teamId === id) { return; }
if (!teamsData[id].hasSecondaryKey) { return; }
var t = teamsData[id];
teams[t.edPublic] = {
notifications: true,
displayName: t.name,
edPublic: t.edPublic,
avatar: t.avatar,
id: id
};
});
if (Object.keys(teams).length) {
var teamsList = UIElements.getUserGrid(Messages.share_linkTeam, {
common: common,
noFilter: true,
@ -240,6 +227,7 @@ define([
data: teams
}, refreshButtons);
$div.append(teamsList.div);
}
var shareButton = {
className: 'primary cp-share-with-friends',
@ -395,6 +383,26 @@ define([
}
};
var getEditableTeams = function (common, config) {
var privateData = common.getMetadataMgr().getPrivateData();
var teamsData = Util.tryParse(JSON.stringify(privateData.teams)) || {};
var teams = {};
Object.keys(teamsData).forEach(function (id) {
// config.teamId only exists when we're trying to share a pad from a team drive
// In this case, we don't want to share the pad with the current team
if (config.teamId && config.teamId === id) { return; }
if (!teamsData[id].hasSecondaryKey) { return; }
var t = teamsData[id];
teams[t.edPublic] = {
notifications: true,
displayName: t.name,
edPublic: t.edPublic,
avatar: t.avatar,
id: id
};
});
return teams;
};
var makeBurnAfterReadingUrl = function (common, href, channel, cb) {
var keyPair = Hash.generateSignPair();
var parsed = Hash.parsePadUrl(href);
@ -643,7 +651,10 @@ define([
// Share with contacts tab
var hasFriends = Object.keys(config.friends || {}).length !== 0;
var teams = getEditableTeams(common, config);
config.teams = teams;
var hasFriends = Object.keys(config.friends || {}).length ||
Object.keys(teams).length;
var onFriendShare = Util.mkEvent();
@ -926,7 +937,10 @@ define([
});
// share with contacts tab
var hasFriends = Object.keys(config.friends || {}).length !== 0;
var teams = getEditableTeams(common, config);
config.teams = teams;
var hasFriends = Object.keys(config.friends || {}).length ||
Object.keys(teams).length;
var friendsObject = hasFriends ? createShareWithFriends(config, null, getLinkValue) : noContactsMessage(common);
var friendsList = friendsObject.content;
@ -1592,11 +1606,7 @@ define([
.text(Messages.accessButton))
.click(common.prepareFeedback(type))
.click(function () {
require(['/common/inner/access.js'], function (Access) {
Access.getAccessModal(common, {}, function (e) {
if (e) { console.error(e); }
});
});
sframeChan.event('EV_ACCESS_OPEN');
});
break;
case 'properties':
@ -1611,11 +1621,7 @@ define([
if (!data) {
return void UI.alert(Messages.autostore_notAvailable);
}
require(['/common/inner/properties.js'], function (Properties) {
Properties.getPropertiesModal(common, {}, function (e) {
if (e) { console.error(e); }
});
});
sframeChan.event('EV_PROPERTIES_OPEN');
});
});
break;
@ -2628,17 +2634,13 @@ define([
});
};
UIElements.initFilePicker = function (common, cfg) {
var onSelect = cfg.onSelect || $.noop;
UIElements.openFilePicker = function (common, types, cb) {
var sframeChan = common.getSframeChannel();
sframeChan.on("EV_FILE_PICKED", function (data) {
onSelect(data);
sframeChan.query("Q_FILE_PICKER_OPEN", types, function (err, data) {
if (err) { return; }
cb(data);
});
};
UIElements.openFilePicker = function (common, types) {
var sframeChan = common.getSframeChannel();
sframeChan.event("EV_FILE_PICKER_OPEN", types);
};
UIElements.openTemplatePicker = function (common, force) {
var metadataMgr = common.getMetadataMgr();
@ -2661,10 +2663,8 @@ define([
return;
}
delete pickerCfg.hidden;
common.openFilePicker(pickerCfg);
var first = true; // We can only pick a template once (for a new document)
var fileDialogCfg = {
onSelect: function (data) {
common.openFilePicker(pickerCfg, function (data) {
if (data.type === type && first) {
UI.addLoadingScreen({hideTips: true});
var chatChan = common.getPadChat();
@ -2681,9 +2681,7 @@ define([
if (focus) { focus.focus(); }
return;
}
}
};
common.initFilePicker(fileDialogCfg);
});
};
sframeChan.query("Q_TEMPLATE_EXIST", type, function (err, data) {

@ -23,8 +23,17 @@ define([
init: function () {}
};
var mermaidThemeCSS = ".node rect { fill: #DDD; stroke: #AAA; } " +
"rect.task, rect.task0, rect.task2 { stroke-width: 1 !important; rx: 0 !important; } " +
"g.grid g.tick line { opacity: 0.25; }" +
"g.today line { stroke: red; stroke-width: 1; stroke-dasharray: 3; opacity: 0.5; }";
require(['mermaid', 'css!/code/mermaid-new.css'], function (_Mermaid) {
Mermaid = _Mermaid;
Mermaid.initialize({
gantt: { axisFormat: '%m-%d', },
"themeCSS": mermaidThemeCSS,
});
});
var highlighter = function () {
@ -351,6 +360,12 @@ define([
// retrieve the attached source code which it was drawn
var src = el.getAttribute('mermaid-source');
/* The new source might have syntax errors that will prevent rendering.
It might be preferable to keep the existing state instead of removing it
if you don't have something better to display. Ideally we should display
the cause of the syntax error so that the user knows what to correct. */
//if (!Mermaid.parse(src)) { } // TODO
// check if that source exists in the set of charts which are about to be rendered
if (mermaid_source.indexOf(src) === -1) {
// if it's not, then you can remove it
@ -418,7 +433,7 @@ define([
throw new Error(patch);
} else {
DD.apply($content[0], patch);
var $mts = $content.find('media-tag:not(:has(*))');
var $mts = $content.find('media-tag');
$mts.each(function (i, el) {
var $mt = $(el).contextmenu(function (e) {
e.preventDefault();
@ -426,6 +441,16 @@ define([
$(contextMenu.menu).find('li').show();
contextMenu.show(e);
});
if ($mt.children().length) {
$mt.off('dblclick preview');
$mt.on('preview', onPreview($mt));
if ($mt.find('img').length) {
$mt.on('dblclick', function () {
$mt.trigger('preview');
});
}
return;
}
MediaTag(el);
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {

@ -59,7 +59,7 @@ define([
}), opts.href);
// If this is a file, don't try to look for metadata
if (opts.channel && opts.channel.length > 34) { return; }
if (opts.channel && opts.channel.length > 32) { return; }
if (opts.channel) { data.channel = opts.channel; }
Modal.loadMetadata(Env, data, waitFor);
}).nThen(function () {
@ -129,7 +129,10 @@ define([
tabs[i] = {
content: c && UI.dialog.customModal(node, {
buttons: obj.buttons || button,
onClose: function () { blocked = false; }
onClose: function () {
blocked = false;
if (typeof(opts.onClose) === "function") { opts.onClose(); }
}
}),
disabled: !c,
title: obj.title,

@ -132,6 +132,12 @@ define([
APP.onLocal();
};
var isRegisteredUserOnline = function () {
var users = metadataMgr.getMetadata().users || {};
return Object.keys(users).some(function (id) {
return users[id] && users[id].curvePublic;
});
};
var isUserOnline = function (ooid) {
// Remove ids for users that have left the channel
deleteOffline();
@ -348,16 +354,44 @@ define([
fixSheets();
APP.FM.handleFile(blob, data);
};
Messages.oo_login = 'Log in...'; // XXX
var noLogin = false;
var makeCheckpoint = function (force) {
if (!common.isLoggedIn()) { return; }
var locked = content.saveLock;
var lastCp = getLastCp();
var needCp = force || ooChannel.cpIndex % CHECKPOINT_INTERVAL === 0 ||
(ooChannel.cpIndex - lastCp.index) > CHECKPOINT_INTERVAL;
(ooChannel.cpIndex - (lastCp.index || 0)) > CHECKPOINT_INTERVAL;
if (!needCp) { return; }
if (!locked || !isUserOnline(locked) || force) {
if (!common.isLoggedIn() && !isRegisteredUserOnline() && !noLogin) {
var login = h('button.cp-corner-primary', Messages.login_login);
var register = h('button.cp-corner-primary', Messages.login_register);
var cancel = h('button.cp-corner-cancel', Messages.cancel);
var actions = h('div', [cancel, register, login]);
var modal = UI.cornerPopup(Messages.oo_login, actions, '', {alt: true});
$(register).click(function () {
common.setLoginRedirect(function () {
common.gotoURL('/register/');
});
modal.delete();
});
$(login).click(function () {
common.setLoginRedirect(function () {
common.gotoURL('/login/');
});
modal.delete();
});
$(cancel).click(function () {
modal.delete();
noLogin = true;
});
return;
}
if (!common.isLoggedIn()) { return; }
content.saveLock = myOOId;
APP.onLocal();
APP.realtime.onSettle(function () {
@ -907,8 +941,16 @@ define([
if (ifr) { ifr.remove(); }
};
common.initFilePicker({
onSelect: function (data) {
APP.AddImage = function(cb1, cb2) {
APP.AddImageSuccessCallback = cb1;
APP.AddImageErrorCallback = cb2;
common.openFilePicker({
types: ['file'],
where: ['root'],
filter: {
fileType: ['image/']
}
}, function (data) {
if (data.type !== 'file') {
debug("Unexpected data type picked " + data.type);
return;
@ -929,19 +971,6 @@ define([
});
});
});
}
});
APP.AddImage = function(cb1, cb2) {
APP.AddImageSuccessCallback = cb1;
APP.AddImageErrorCallback = cb2;
common.openFilePicker({
types: ['file'],
where: ['root'],
filter: {
fileType: ['image/']
}
});
};
@ -1091,12 +1120,12 @@ define([
var x2tSaveAndConvertData = function(data, filename, extension, finalFilename) {
// Perform the x2t conversion
require(['/common/onlyoffice/x2t/x2t.js'], function() {
require(['/common/onlyoffice/x2t/x2t.js'], function() { // XXX why does this fail without an access-control-allow-origin header?
var x2t = window.Module;
x2t.run();
if (x2tInitialized) {
debug("x2t runtime already initialized");
x2tSaveAndConvertDataInternal(x2t, data, filename, extension, finalFilename);
x2tSaveAndConvertDataInternal(x2t, data, filename, extension, finalFilename); // XXX shouldn't this return ?
}
x2t.onRuntimeInitialized = function() {

@ -675,6 +675,8 @@ define([
sem.take(function (give) {
var otherOwners = false;
nThen(function (_w) {
// Don't check server metadata for blobs
if (c.length !== 32) { return; }
Store.anonRpcMsg(null, {
msg: 'GET_METADATA',
data: c
@ -1807,6 +1809,7 @@ define([
var cb = Util.once(Util.mkAsync(_cb));
if (!data.channel) { return void cb({ error: 'ENOTFOUND'}); }
if (data.channel.length !== 32) { return void cb({ error: 'EINVAL'}); }
store.anon_rpc.send('GET_METADATA', data.channel, function (err, obj) {
if (err) { return void cb({error: err}); }
var metadata = (obj && obj[0]) || {};

@ -678,6 +678,8 @@ define([
sem.take(function (give) {
var otherOwners = false;
nThen(function (_w) {
// Don't check server metadata for blobs
if (c.length !== 32) { return; }
ctx.Store.anonRpcMsg(null, {
msg: 'GET_METADATA',
data: c

@ -503,8 +503,13 @@ define([
var createFilePicker = function () {
if (!common.isLoggedIn()) { return; }
common.initFilePicker({
onSelect: function (data) {
$embedButton = common.createButton('mediatag', true).click(function () {
var cfg = {
types: ['file'],
where: ['root']
};
if ($embedButton.data('filter')) { cfg.filter = $embedButton.data('filter'); }
common.openFilePicker(cfg, function (data) {
if (data.type !== 'file') {
console.log("Unexpected data type picked " + data.type);
return;
@ -516,17 +521,7 @@ define([
var src = data.src = data.src.slice(0,1) === '/' ? origin + data.src : data.src;
mediaTagEmbedder($('<media-tag src="' + src +
'" data-crypto-key="cryptpad:' + data.key + '"></media-tag>'), data);
}
});
$embedButton = common.createButton('mediatag', true).click(function () {
var cfg = {
types: ['file'],
where: ['root']
};
if ($embedButton.data('filter')) {
cfg.filter = $embedButton.data('filter');
}
common.openFilePicker(cfg);
}).appendTo(toolbar.$rightside).hide();
};
var setMediaTagEmbedder = function (mte, filter) {

@ -18,8 +18,7 @@ define([
var Cryptget;
var SFrameChannel;
var sframeChan;
var FilePicker;
var Share;
var SecureIframe;
var Messaging;
var Notifier;
var Utils = {
@ -44,8 +43,7 @@ define([
'/bower_components/chainpad-crypto/crypto.js',
'/common/cryptget.js',
'/common/outer/worker-channel.js',
'/filepicker/main.js',
'/share/main.js',
'/secureiframe/main.js',
'/common/common-messaging.js',
'/common/common-notifier.js',
'/common/common-hash.js',
@ -58,15 +56,14 @@ define([
'/common/test.js',
'/common/userObject.js',
], waitFor(function (_CpNfOuter, _Cryptpad, _Crypto, _Cryptget, _SFrameChannel,
_FilePicker, _Share, _Messaging, _Notifier, _Hash, _Util, _Realtime,
_SecureIframe, _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;
SecureIframe = _SecureIframe;
Messaging = _Messaging;
Notifier = _Notifier;
Utils.Hash = _Hash;
@ -491,7 +488,7 @@ define([
// Put in the following function the RPC queries that should also work in filepicker
var addCommonRpc = function (sframeChan) {
var addCommonRpc = function (sframeChan, safe) {
sframeChan.on('Q_ANON_RPC_MESSAGE', function (data, cb) {
Cryptpad.anonRpcMsg(data.msg, data.content, function (err, response) {
cb({error: err, response: response});
@ -598,6 +595,12 @@ define([
}
if (data.href) { href = data.href; }
Cryptpad.getPadAttribute(data.key, function (e, data) {
if (!safe && data) {
// Remove unsafe data for the unsafe iframe
delete data.href;
delete data.roHref;
delete data.password;
}
cb({
error: e,
data: data
@ -985,81 +988,63 @@ define([
onFileUpload(sframeChan, data, cb);
});
// File picker
var FP = {};
var initFilePicker = function (cfg) {
// cfg.hidden means pre-loading the filepicker while keeping it hidden.
// Secure modal
var SecureModal = {};
// Create or display the iframe and modal
var initSecureModal = function (type, cfg, cb) {
cfg.modal = type;
SecureModal.cb = cb;
// cfg.hidden means pre-loading the iframe while keeping it hidden.
// if cfg.hidden is true and the iframe already exists, do nothing
if (!FP.$iframe) {
if (!SecureModal.$iframe) {
var config = {};
config.onFilePicked = function (data) {
sframeChan.event('EV_FILE_PICKED', data);
config.onAction = function (data) {
if (typeof(SecureModal.cb) !== "function") { return; }
SecureModal.cb(data);
SecureModal.$iframe.hide();
};
config.onClose = function () {
FP.$iframe.hide();
SecureModal.$iframe.hide();
};
config.data = {
hashes: hashes,
password: password,
isTemplate: isTemplate
};
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);
SecureModal.$iframe = $('<iframe>', {id: 'sbox-secure-iframe'}).appendTo($('body'));
SecureModal.modal = SecureIframe.create(config);
} else if (!cfg.hidden) {
FP.$iframe.show();
FP.picker.refresh(cfg);
SecureModal.modal.refresh(cfg, function () {
SecureModal.$iframe.show();
});
}
if (cfg.hidden) {
FP.$iframe.hide();
SecureModal.$iframe.hide();
return;
}
FP.$iframe.focus();
SecureModal.$iframe.focus();
};
sframeChan.on('EV_FILE_PICKER_OPEN', function (data) {
initFilePicker(data);
sframeChan.on('Q_FILE_PICKER_OPEN', function (data, cb) {
initSecureModal('filepicker', data || {}, cb);
});
// 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();
sframeChan.on('EV_PROPERTIES_OPEN', function (data) {
initSecureModal('properties', data || {}, null);
});
}
if (cfg.hidden) {
ShareModal.$iframe.hide();
return;
}
ShareModal.$iframe.focus();
};
sframeChan.on('EV_ACCESS_OPEN', function (data) {
initSecureModal('access', data || {}, null);
});
sframeChan.on('EV_SHARE_OPEN', function (data) {
initShareModal(data || {});
initSecureModal('share', data || {}, null);
});
sframeChan.on('Q_TEMPLATE_USE', function (data, cb) {
@ -1275,6 +1260,9 @@ define([
var _parsed = Utils.Hash.parsePadUrl(metadata.roHref);
_secret = Utils.Hash.getSecrets(_parsed.type, _parsed.hash, metadata.password);
}
if (_secret.channel.length !== 32) {
return void cb({error: 'EINVAL'});
}
var crypto = Crypto.createEncryptor(_secret.keys);
nThen(function (waitFor) {
// Try to get the owner's mailbox from the pad metadata first.
@ -1334,6 +1322,9 @@ define([
var _parsed = Utils.Hash.parsePadUrl(metadata.href || metadata.roHref);
_secret = Utils.Hash.getSecrets(_parsed.type, _parsed.hash, metadata.password);
}
if (_secret.channel.length !== 32) {
return void cb({error: 'EINVAL'});
}
var crypto = Crypto.createEncryptor(_secret.keys);
nThen(function (waitFor) {
// If we already have metadata, use it, otherwise, try to get it

@ -89,7 +89,6 @@ define([
window.CryptPad_UIElements = UIElements;
window.CryptPad_common = funcs;
funcs.createUserAdminMenu = callWithCommon(UIElements.createUserAdminMenu);
funcs.initFilePicker = callWithCommon(UIElements.initFilePicker);
funcs.openFilePicker = callWithCommon(UIElements.openFilePicker);
funcs.openTemplatePicker = callWithCommon(UIElements.openTemplatePicker);
funcs.displayMediatagImage = callWithCommon(MT.displayMediatagImage);

@ -350,7 +350,7 @@
"fm_info_root": "Creeu aquí tantes carpetes imbrincades com vulgueu per ordenar els vostres fitxers.",
"fm_info_unsorted": "Conté tots els fitxers que heu visitat i que encara no estan ordenats, a \"Documents\" o desplaçats a la \"Paperera\".",
"fm_info_template": "Conté tots els documents desats com plantilles i que podeu reutilitzar quan vulgueu crear un nou document.",
"fm_info_recent": "Llista els documents modificats o oberts recentment.",
"fm_info_recent": "Aquests documents s'han modificat o obert darrerament, per vós o per alguna persona col·laboradora.",
"fm_info_trash": "Buideu la paperera per alliberar espai al vostre CryptDrive.",
"fm_info_allFiles": "Conté tots els fitxers de \"Documents\", \"Desordenats\" i \"Paperera\". No podeu desplaçar o suprimir fitxers des d'aquí.",
"fm_info_anonymous": "No heu iniciat la sessió, per tant, els vostres documents caducaran d'aquí a 3 mesos (<a href=\"https://blog.cryptpad.fr/2017/05/17/You-gotta-log-in/\" target=\"_blank\">saber-ne més</a>). Es desen al vostre navegador, per tant, si netegeu el vostre historial podríeu perdre'ls.<br><a href=\"/register/\">Registreu-vos</a> o <a href=\"/login/\">Inicieu la sessió</a> per mantenir-los accessibles.<br>",
@ -515,7 +515,7 @@
"settings_pinningError": "Alguna cosa no ha funcionat correctament",
"settings_usageAmount": "Els vostres documents fixats ocupen {0} MB",
"settings_logoutEverywhereButton": "Tanca la sessió",
"settings_logoutEverywhereTitle": "Tanca la sessió arreu",
"settings_logoutEverywhereTitle": "Tanca les sessions remotes",
"settings_logoutEverywhere": "Tanca totes les altres sessions",
"settings_logoutEverywhereConfirm": "De debò? Haureu de tornar a iniciar la vostra sessió a tots els dispositius.",
"settings_driveDuplicateTitle": "Documents propis duplicats",
@ -573,7 +573,7 @@
"upload_success": "El fitxer ({0}) ha estat carregat correctament i afegit al vostre CryptDrive.",
"upload_notEnoughSpace": "No hi ha prou espai al CryptDrive per aquest fitxer.",
"upload_notEnoughSpaceBrief": "No hi ha prou espai",
"upload_tooLarge": "Aquest fitxer supera la mida màxima permesa.",
"upload_tooLarge": "Aquest fitxer supera la mida màxima permesa pel vostre compte",
"upload_tooLargeBrief": "El fitxer és massa gran",
"upload_choose": "Trieu un fitxer",
"upload_pending": "Pendent",
@ -619,5 +619,20 @@
"download_resourceNotAvailable": "El recurs sol·licitat no estava disponible... Premeu Esc per continuar.",
"about_contributors": "Col·laboracions clau",
"about_core": "Desenvolupament principal",
"about_intro": "CryptPad s'ha creat dins l'Equip de Recerca de <a href=\"http://xwiki.com\">XWiki SAS</a>, una petita empresa de París, França i Iasi, Romania. Hi ha 3 membres de l'equip central treballant amb CryptPad més una quantitat de persones col·laboradores, dins i fora d'XWiki SAS."
"about_intro": "CryptPad s'ha creat dins l'Equip de Recerca de <a href=\"http://xwiki.com\">XWiki SAS</a>, una petita empresa de París, França i Iasi, Romania. Hi ha 3 membres de l'equip central treballant amb CryptPad més una quantitat de persones col·laboradores, dins i fora d'XWiki SAS.",
"main_catch_phrase": "El Núvol Coneixement Zero",
"main_footerText": "Amb Cryptpad, podeu crear documents col·laboratius per prendre notes i posar en ordre idees de forma conjunta de forma ràpida.",
"footer_applications": "Aplicacions",
"footer_contact": "Contacte",
"footer_aboutUs": "Sobre nosaltres",
"about": "Sobre",
"contact": "Contacte",
"blog": "Bloc",
"topbar_whatIsCryptpad": "Què és CryptPad",
"whatis_collaboration": "Col·laboració fàcil i ràpida",
"whatis_title": "Què és CryptPad",
"terms": "Condicions d'ús",
"main_info": "<h2>Col·laboreu amb Confiança</h2>\nFeu créixer les vostres idees conjuntament amb documents compartits mentre la tecnologia <strong>Coneixement Zero</strong> assegura la vostra privacitat; <strong>fins i tot per nosaltres</strong>.",
"whatis_collaboration_p1": "Amb CryptPad, podeu crear de forma ràpida, documents col·laboratius per prendre notes i posar en ordre idees conjuntament. Quan us registreu i inicieu la vostra sessió, teniu la capacitat de carregar fitxers i un CryptDrive on podeu organitzar tots els vostres documents. Com a persona registrada disposeu de 50MB d'espai gratuït.",
"privacy": "Privacitat"
}

@ -1,134 +0,0 @@
// Load #1, load as little as possible because we are in a race to get the loading screen up.
define([
'/bower_components/nthen/index.js',
'/api/config',
'jquery',
'/common/requireconfig.js',
], function (nThen, ApiConfig, $, RequireConfig) {
var requireConfig = RequireConfig();
var create = function (config) {
// Loaded in load #2
var sframeChan;
nThen(function (waitFor) {
$(waitFor());
}).nThen(function (waitFor) {
var req = {
cfg: requireConfig,
req: [ '/common/loading.js' ],
pfx: window.location.origin
};
window.rc = requireConfig;
window.apiconf = ApiConfig;
$('#sbox-filePicker-iframe').attr('src',
ApiConfig.httpSafeOrigin + '/filepicker/inner.html?' + requireConfig.urlArgs +
'#' + encodeURIComponent(JSON.stringify(req)));
// This is a cheap trick to avoid loading sframe-channel in parallel with the
// loading screen setup.
var done = waitFor();
var onMsg = function (msg) {
var data = JSON.parse(msg.data);
if (data.q !== 'READY') { return; }
window.removeEventListener('message', onMsg);
var _done = done;
done = function () { };
_done();
};
window.addEventListener('message', onMsg);
}).nThen(function (/*waitFor*/) {
var Cryptpad = config.modules.Cryptpad;
var Utils = config.modules.Utils;
nThen(function (waitFor) {
// 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 = Utils.Util.mkEvent();
var iframe = $('#sbox-filePicker-iframe')[0].contentWindow;
var postMsg = function (data) {
iframe.postMessage(data, '*');
};
var w = waitFor();
var whenReady = 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, language: Cryptpad.getLanguage(), localStore: window.localStore, cache: window.cpCache }));
// Then start the channel
window.addEventListener('message', function (msg) {
if (msg.source !== iframe) { return; }
msgEv.fire(msg);
});
config.modules.SFrameChannel.create(msgEv, postMsg, waitFor(function (sfc) {
sframeChan = sfc;
}));
w();
};
window.addEventListener('message', whenReady);
}).nThen(function () {
var updateMeta = function () {
//console.log('EV_METADATA_UPDATE');
var metaObj;
nThen(function (waitFor) {
Cryptpad.getMetadata(waitFor(function (err, n) {
if (err) { console.log(err); }
metaObj = n;
}));
}).nThen(function (/*waitFor*/) {
metaObj.doc = {};
var additionalPriv = {
fileHost: ApiConfig.fileHost,
loggedIn: Utils.LocalStore.isLoggedIn(),
origin: window.location.origin,
pathname: window.location.pathname,
feedbackAllowed: Utils.Feedback.state,
types: config.types
};
for (var k in additionalPriv) { metaObj.priv[k] = additionalPriv[k]; }
sframeChan.event('EV_METADATA_UPDATE', metaObj);
});
};
Cryptpad.onMetadataChanged(updateMeta);
sframeChan.onReg('EV_METADATA_UPDATE', updateMeta);
config.addCommonRpc(sframeChan);
sframeChan.on('Q_GET_FILES_LIST', function (types, cb) {
Cryptpad.getSecureFilesList(types, function (err, data) {
cb({
error: err,
data: data
});
});
});
sframeChan.on('EV_FILE_PICKER_CLOSE', function () {
config.onClose();
});
sframeChan.on('EV_FILE_PICKED', function (data) {
config.onFilePicked(data);
});
sframeChan.on('Q_UPLOAD_FILE', function (data, cb) {
config.onFileUpload(sframeChan, data, cb);
});
});
});
var refresh = function (types) {
if (!sframeChan) { return; }
sframeChan.event('EV_FILE_PICKER_REFRESH', types);
};
return {
refresh: refresh
};
};
return {
create: create
};
});

@ -1206,22 +1206,18 @@ define([
updatePublishButton();
if (common.isLoggedIn()) {
var fileDialogCfg = {
onSelect: function (data) {
if (data.type === 'file' && APP.editor) {
var mt = '<media-tag src="' + data.src + '" data-crypto-key="cryptpad:' + data.key + '"></media-tag>';
APP.editor.replaceSelection(mt);
return;
}
}
};
common.initFilePicker(fileDialogCfg);
APP.$mediaTagButton = common.createButton('mediatag', true).click(function () {
var pickerCfg = {
types: ['file'],
where: ['root']
};
common.openFilePicker(pickerCfg);
common.openFilePicker(pickerCfg, function (data) {
if (data.type === 'file' && APP.editor) {
var mt = '<media-tag src="' + data.src + '" data-crypto-key="cryptpad:' + data.key + '"></media-tag>';
APP.editor.replaceSelection(mt);
return;
}
});
}).appendTo($rightside);
var $tags = common.createButton('hashtag', true);

@ -6,8 +6,11 @@
@import (reference) '../../customize/src/less2/include/tippy.less';
@import (reference) '../../customize/src/less2/include/checkmark.less';
@import (reference) '../../customize/src/less2/include/password-input.less';
@import (reference) '../../customize/src/less2/include/modals-ui-elements.less';
@import (reference) '../../customize/src/less2/include/usergrid.less';
&.cp-app-filepicker {
&.cp-app-secureiframe {
.modals-ui-elements_main();
.iconColors_main();
.fileupload_main();
.alertify_main();
@ -15,6 +18,7 @@
.checkmark_main(20px);
.password_main();
.modal_main();
.usergrid_main();
#cp-filepicker-dialog {
display: none;
@ -26,6 +30,14 @@
overflow-y: auto;
}
.cp-loading-spinner-container {
height: 100px;
.cp-spinner {
border-color: @colortheme_logo-2;
border-top-color: transparent;
}
}
.cp-filepicker-content-element {
width: 125px;
//min-width: 200px;

@ -2,7 +2,7 @@
<html style="height: 100%; background: transparent;">
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<script async data-bootload="/filepicker/inner.js" data-main="/common/sframe-boot.js?ver=1.6" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<script async data-bootload="/secureiframe/inner.js" data-main="/common/sframe-boot.js?ver=1.6" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<style>
.loading-hidden { display: none; }
body #cp-loading {
@ -22,7 +22,7 @@
}
</style>
</head>
<body class="cp-app-filepicker" style="background: transparent;">
<body class="cp-app-secureiframe" style="background: transparent;">
</body>
</html>

@ -7,12 +7,13 @@ define([
'/common/common-ui-elements.js',
'/common/common-util.js',
'/common/common-hash.js',
'/common/hyperscript.js',
'json.sortify',
'/customize/messages.js',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
'less!/filepicker/app-filepicker.less',
'less!/secureiframe/app-secure.less',
], function (
$,
Crypto,
@ -22,6 +23,7 @@ define([
UIElements,
Util,
Hash,
h,
Sortify,
Messages)
{
@ -29,23 +31,88 @@ define([
var andThen = function (common) {
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
var $body = $('body');
var sframeChan = common.getSframeChannel();
var filters = metadataMgr.getPrivateData().types;
var $body = $('body');
var hideIframe = function () {
sframeChan.event('EV_SECURE_IFRAME_CLOSE');
};
var displayed;
var create = {};
// Share modal
create['share'] = function (data) {
var priv = metadataMgr.getPrivateData();
var f = (data && data.file) ? UIElements.createFileShareModal
: UIElements.createShareModal;
var friends = common.getFriends();
var _modal;
var modal = f({
origin: priv.origin,
pathname: priv.pathname,
password: priv.password,
isTemplate: priv.isTemplate,
hashes: priv.hashes,
common: common,
title: data.title,
friends: friends,
onClose: function () {
if (_modal && _modal.close) { _modal.close(); }
hideIframe();
},
fileData: {
hash: priv.hashes.fileHash,
password: priv.password
}
});
$('button.cancel').click(); // Close any existing alertify
_modal = UI.openCustomModal(modal);
displayed = modal;
};
var hideFileDialog = function () {
sframeChan.event('EV_FILE_PICKER_CLOSE');
// Properties modal
create['properties'] = function () {
require(['/common/inner/properties.js'], function (Properties) {
Properties.getPropertiesModal(common, {
onClose: function () {
hideIframe();
}
}, function (e, modal) {
if (e) { console.error(e); }
displayed = modal;
});
});
};
// Access modal
create['access'] = function () {
require(['/common/inner/access.js'], function (Access) {
Access.getAccessModal(common, {
onClose: function () {
hideIframe();
}
}, function (e, modal) {
if (e) { console.error(e); }
displayed = modal;
});
});
};
// File uploader
var onFilePicked = function (data) {
var privateData = metadataMgr.getPrivateData();
var parsed = Hash.parsePadUrl(data.url);
hideFileDialog();
if (displayed && displayed.hide) { displayed.hide(); }
hideIframe();
if (parsed.type === 'file') {
var secret = Hash.getSecrets('file', parsed.hash, data.password);
var fileHost = privateData.fileHost || privateData.origin;
var src = fileHost + Hash.getBlobPathFromHex(secret.channel);
var key = Hash.encodeBase64(secret.keys.cryptKey);
sframeChan.event("EV_FILE_PICKED", {
sframeChan.event("EV_SECURE_ACTION", {
type: parsed.type,
src: src,
name: data.name,
@ -53,14 +120,12 @@ define([
});
return;
}
sframeChan.event("EV_FILE_PICKED", {
sframeChan.event("EV_SECURE_ACTION", {
type: parsed.type,
href: data.url,
name: data.name
});
};
// File uploader
var fmConfig = {
body: $('body'),
noHandlers: true,
@ -69,40 +134,40 @@ define([
}
};
APP.FM = common.createFileManager(fmConfig);
create['filepicker'] = function (_filters) {
var updateContainer = function () {};
// Create file picker
var onSelect = function (url, name, password) {
onFilePicked({url: url, name: name, password: password});
};
var filters = _filters;
var types = filters.types || [];
var data = {
FM: APP.FM
};
var updateContainer;
var createFileDialog = function () {
var types = filters.types || [];
// Create modal
var modal = UI.createModal({
id: 'cp-filepicker-dialog',
$body: $body,
onClose: hideFileDialog
onClose: function () {
hideIframe();
}
});
displayed = modal;
modal.show();
var $blockContainer = modal.$modal;
// Set the fixed content
var $block = $blockContainer.find('.cp-modal');
modal.$modal.attr('id', 'cp-filepicker-dialog');
var $block = modal.$modal.find('.cp-modal');
// Description
var text = Messages.filePicker_description;
if (types && types.length === 1 && types[0] !== 'file') {
// Should be Templates
text = Messages.selectTemplate;
}
var $description = $('<p>').text(text);
$block.append($description);
$block.append(h('p', text));
var $filter = $('<p>', {'class': 'cp-modal-form'}).appendTo($block);
// Add filter input
var $filter = $(h('p.cp-modal-form')).appendTo($block);
var to;
$('<input>', {
var $input = $('<input>', {
type: 'text',
'class': 'cp-filepicker-filter',
'placeholder': Messages.filePicker_filter
@ -126,15 +191,16 @@ define([
$filter.append(common.createButton('upload', false, data));
}
var $container = $('<span>', {'class': 'cp-filepicker-content'}).appendTo($block);
var $container = $(h('span.cp-filepicker-content', [
h('div.cp-loading-spinner-container', h('span.cp-spinner'))
])).appendTo($block);
// Update the files list when needed
updateContainer = function () {
$container.html('');
var $input = $filter.find('.cp-filepicker-filter');
var filter = $input.val().trim();
var todo = function (err, list) {
if (err) { return void console.error(err); }
$container.html('');
Object.keys(list).forEach(function (id) {
var data = list[id];
var name = data.filename || data.title || '?';
@ -149,8 +215,8 @@ define([
$('<span>', {'class': 'cp-filepicker-content-element-name'}).text(name)
.appendTo($span);
$span.click(function () {
if (typeof onSelect === "function") {
onSelect(data.href, name, data.password);
if (typeof onFilePicked === "function") {
onFilePicked({url: data.href, name: name, password: data.password});
}
});
@ -163,21 +229,23 @@ define([
};
updateContainer();
};
sframeChan.on('EV_FILE_PICKER_REFRESH', function (newFilters) {
if (Sortify(filters) !== Sortify(newFilters)) {
$body.html('');
filters = newFilters;
return void createFileDialog();
}
updateContainer();
sframeChan.on('EV_REFRESH', function (data) {
if (!data) { return; }
var type = data.modal;
if (!create[type]) { return; }
if (displayed && displayed.close) { displayed.close(); }
else if (displayed && displayed.hide) { displayed.hide(); }
displayed = undefined;
create[type](data);
});
createFileDialog();
UI.removeLoadingScreen();
};
var main = function () {
var common;
var _andThen = Util.once(andThen);
nThen(function (waitFor) {
$(waitFor(function () {
@ -187,11 +255,11 @@ define([
}).nThen(function (/*waitFor*/) {
var metadataMgr = common.getMetadataMgr();
if (metadataMgr.getMetadataLazy() !== 'uninitialized') {
andThen(common);
_andThen(common);
return;
}
metadataMgr.onChange(function () {
andThen(common);
_andThen(common);
});
});
};

@ -22,8 +22,8 @@ define([
};
window.rc = requireConfig;
window.apiconf = ApiConfig;
$('#sbox-share-iframe').attr('src',
ApiConfig.httpSafeOrigin + '/share/inner.html?' + requireConfig.urlArgs +
$('#sbox-secure-iframe').attr('src',
ApiConfig.httpSafeOrigin + '/secureiframe/inner.html?' + requireConfig.urlArgs +
'#' + encodeURIComponent(JSON.stringify(req)));
// This is a cheap trick to avoid loading sframe-channel in parallel with the
@ -48,7 +48,7 @@ define([
// 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 = Utils.Util.mkEvent();
var iframe = $('#sbox-share-iframe')[0].contentWindow;
var iframe = $('#sbox-secure-iframe')[0].contentWindow;
var postMsg = function (data) {
iframe.postMessage(data, '*');
};
@ -106,7 +106,11 @@ define([
Cryptpad.onMetadataChanged(updateMeta);
sframeChan.onReg('EV_METADATA_UPDATE', updateMeta);
config.addCommonRpc(sframeChan);
config.addCommonRpc(sframeChan, true);
Cryptpad.padRpc.onMetadataEvent.reg(function (data) {
sframeChan.event('EV_RT_METADATA', data);
});
sframeChan.on('EV_CACHE_PUT', function (x) {
Object.keys(x).forEach(function (k) {
@ -132,11 +136,24 @@ define([
});
});
sframeChan.on('EV_SHARE_CLOSE', function () {
config.onClose();
sframeChan.on('EV_SECURE_ACTION', function (data) {
config.onAction(data);
});
sframeChan.on('Q_UPLOAD_FILE', function (data, cb) {
config.onFileUpload(sframeChan, data, cb);
});
sframeChan.on('EV_SHARE_ACTION', function (data) {
config.onShareAction(data);
sframeChan.on('Q_GET_FILES_LIST', function (types, cb) {
Cryptpad.getSecureFilesList(types, function (err, data) {
cb({
error: err,
data: data
});
});
});
sframeChan.on('EV_SECURE_IFRAME_CLOSE', function () {
config.onClose();
});
sframeChan.onReady(function () {
@ -155,7 +172,7 @@ define([
};
return;
}
sframeChan.event('EV_SHARE_REFRESH', data);
sframeChan.event('EV_REFRESH', data);
cb();
};
return {

@ -1,17 +0,0 @@
@import (reference) '../../customize/src/less2/include/tippy.less';
@import (reference) '../../customize/src/less2/include/modal.less';
@import (reference) '../../customize/src/less2/include/alertify.less';
@import (reference) '../../customize/src/less2/include/checkmark.less';
@import (reference) '../../customize/src/less2/include/password-input.less';
@import (reference) '../../customize/src/less2/include/usergrid.less';
@import (reference) '../../customize/src/less2/include/modals-ui-elements.less';
&.cp-app-share {
.tippy_main();
.alertify_main();
.modals-ui-elements_main();
.checkmark_main(20px);
.password_main();
.modal_main();
.usergrid_main();
}

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

@ -1,29 +0,0 @@
<!DOCTYPE html>
<html style="height: 100%; background: transparent;">
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<script async data-bootload="/share/inner.js" data-main="/common/sframe-boot.js?ver=1.6" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<style>
.loading-hidden { display: none; }
body #cp-loading {
display: none;
position: absolute;
top: 15vh;
bottom: 15vh;
left: 10vw;
right: 10vw;
z-index: 200000;
overflow: hidden;
}
body #cp-loading .cp-loading-container {
margin-top: 35vh;
}
body #cp-loading .cp-loading-cryptofist {
display: none;
}
</style>
</head>
<body class="cp-app-share" style="background: transparent;">
</body>
</html>

@ -1,87 +0,0 @@
define([
'jquery',
'/bower_components/nthen/index.js',
'/common/sframe-common.js',
'/common/common-ui-elements.js',
'/common/common-interface.js',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
'less!/share/app-share.less',
], function (
$,
nThen,
SFCommon,
UIElements,
UI)
{
var APP = window.APP = {};
var init = false;
var andThen = function (common) {
if (init) { return; }
init = true;
var metadataMgr = common.getMetadataMgr();
var sframeChan = common.getSframeChannel();
var hideShareDialog = function () {
sframeChan.event('EV_SHARE_CLOSE');
};
var createShareDialog = function (data) {
var priv = metadataMgr.getPrivateData();
var hashes = priv.hashes;
var origin = priv.origin;
var pathname = priv.pathname;
var f = (data && data.file) ? UIElements.createFileShareModal
: UIElements.createShareModal;
var friends = common.getFriends();
var modal = f({
origin: origin,
pathname: pathname,
password: priv.password,
isTemplate: priv.isTemplate,
hashes: hashes,
common: common,
title: data.title,
friends: friends,
onClose: function () {
hideShareDialog();
},
fileData: {
hash: hashes.fileHash,
password: priv.password
}
});
$('button.cancel').click(); // Close any existing alertify
UI.openCustomModal(modal);
};
sframeChan.on('EV_SHARE_REFRESH', function (data) {
createShareDialog(data);
});
};
var main = function () {
var common;
nThen(function (waitFor) {
$(waitFor(function () {
UI.removeLoadingScreen();
}));
SFCommon.create(waitFor(function (c) { APP.common = common = c; }));
}).nThen(function (/*waitFor*/) {
var metadataMgr = common.getMetadataMgr();
if (metadataMgr.getMetadataLazy() !== 'uninitialized') {
andThen(common);
return;
}
metadataMgr.onChange(function () {
andThen(common);
});
});
};
main();
});
Loading…
Cancel
Save