diff --git a/config/config.example.js b/config/config.example.js index 273c196d2..3826a7291 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -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 * ===================== */ diff --git a/customize.dist/src/outer.css b/customize.dist/src/outer.css index b36fb2c10..75f449e61 100644 --- a/customize.dist/src/outer.css +++ b/customize.dist/src/outer.css @@ -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; diff --git a/docs/example.nginx.conf b/docs/example.nginx.conf index ea8224c14..f6e163910 100644 --- a/docs/example.nginx.conf +++ b/docs/example.nginx.conf @@ -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 diff --git a/lib/commands/admin-rpc.js b/lib/commands/admin-rpc.js index ed1d5b8a4..c60cdcfbc 100644 --- a/lib/commands/admin-rpc.js +++ b/lib/commands/admin-rpc.js @@ -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(), }); }; diff --git a/lib/commands/channel.js b/lib/commands/channel.js index d35e15241..fce5b048c 100644 --- a/lib/commands/channel.js +++ b/lib/commands/channel.js @@ -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'); diff --git a/lib/commands/pin-rpc.js b/lib/commands/pin-rpc.js index f3ade0489..a6f1642c6 100644 --- a/lib/commands/pin-rpc.js +++ b/lib/commands/pin-rpc.js @@ -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) { - return Object.keys(O).filter(function (k) { - return O[k]; - }); + try { + return Object.keys(O).filter(function (k) { + return O[k]; + }); + } catch (err) { + return []; + } }; var getChannelList = Pinning.getChannelList = function (Env, safeKey, _cb) { diff --git a/lib/historyKeeper.js b/lib/historyKeeper.js index f70dba006..ff0327751 100644 --- a/lib/historyKeeper.js +++ b/lib/historyKeeper.js @@ -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 || {}; diff --git a/lib/hk-util.js b/lib/hk-util.js index 0bb96a52b..0e79c0691 100644 --- a/lib/hk-util.js +++ b/lib/hk-util.js @@ -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); diff --git a/lib/workers/crypto-worker.js b/lib/workers/crypto-worker.js deleted file mode 100644 index 5ed58ac7c..000000000 --- a/lib/workers/crypto-worker.js +++ /dev/null @@ -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); -}); diff --git a/lib/workers/db-worker.js b/lib/workers/db-worker.js index 0b9de4f53..0f3d3b87f 100644 --- a/lib/workers/db-worker.js +++ b/lib/workers/db-worker.js @@ -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) { diff --git a/lib/workers/index.js b/lib/workers/index.js index 3190741d3..cc11d538b 100644 --- a/lib/workers/index.js +++ b/lib/workers/index.js @@ -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 state = workers[workerIndex]; + 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 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); -}; + diff --git a/www/code/inner.js b/www/code/inner.js index 408e78c45..09158d952 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -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 { diff --git a/www/common/common-interface.js b/www/common/common-interface.js index 4c4518434..b2cd042a8 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -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); diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index a9c682afc..75ee78c15 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -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,40 +206,28 @@ define([ delete friends[curve]; }); - var friendsList = UIElements.getUserGrid(Messages.share_linkFriends, { - common: common, - data: friends, - noFilter: false, - large: true - }, refreshButtons); - var friendDiv = friendsList.div; - $div.append(friendDiv); - var others = friendsList.icons; + var others = []; + if (Object.keys(friends).length) { + var friendsList = UIElements.getUserGrid(Messages.share_linkFriends, { + common: common, + data: friends, + noFilter: false, + large: true + }, refreshButtons); + var friendDiv = friendsList.div; + $div.append(friendDiv); + 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 - }; - }); - var teamsList = UIElements.getUserGrid(Messages.share_linkTeam, { - common: common, - noFilter: true, - large: true, - data: teams - }, refreshButtons); - $div.append(teamsList.div); + if (Object.keys(teams).length) { + var teamsList = UIElements.getUserGrid(Messages.share_linkTeam, { + common: common, + noFilter: true, + large: true, + 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,29 +2663,25 @@ 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) { - if (data.type === type && first) { - UI.addLoadingScreen({hideTips: true}); - var chatChan = common.getPadChat(); - var cursorChan = common.getCursorChannel(); - sframeChan.query('Q_TEMPLATE_USE', { - href: data.href, - chat: chatChan, - cursor: cursorChan - }, function () { - first = false; - UI.removeLoadingScreen(); - Feedback.send('TEMPLATE_USED'); - }); - if (focus) { focus.focus(); } - return; - } + common.openFilePicker(pickerCfg, function (data) { + if (data.type === type && first) { + UI.addLoadingScreen({hideTips: true}); + var chatChan = common.getPadChat(); + var cursorChan = common.getCursorChannel(); + sframeChan.query('Q_TEMPLATE_USE', { + href: data.href, + chat: chatChan, + cursor: cursorChan + }, function () { + first = false; + UI.removeLoadingScreen(); + Feedback.send('TEMPLATE_USED'); + }); + if (focus) { focus.focus(); } + return; } - }; - common.initFilePicker(fileDialogCfg); + }); }; sframeChan.query("Q_TEMPLATE_EXIST", type, function (err, data) { diff --git a/www/common/diffMarked.js b/www/common/diffMarked.js index f53828e17..8fc8aab6e 100644 --- a/www/common/diffMarked.js +++ b/www/common/diffMarked.js @@ -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) { diff --git a/www/common/inner/common-modal.js b/www/common/inner/common-modal.js index ee475c028..33ac14d9f 100644 --- a/www/common/inner/common-modal.js +++ b/www/common/inner/common-modal.js @@ -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, diff --git a/www/common/onlyoffice/inner.js b/www/common/onlyoffice/inner.js index 5ac3da269..20125d82c 100644 --- a/www/common/onlyoffice/inner.js +++ b/www/common/onlyoffice/inner.js @@ -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() { diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 604c799f3..c8de4f787 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -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]) || {}; diff --git a/www/common/outer/team.js b/www/common/outer/team.js index a2dbf7c05..c151de004 100644 --- a/www/common/outer/team.js +++ b/www/common/outer/team.js @@ -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 diff --git a/www/common/sframe-app-framework.js b/www/common/sframe-app-framework.js index e3adfd6c7..24e7d0abd 100644 --- a/www/common/sframe-app-framework.js +++ b/www/common/sframe-app-framework.js @@ -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($(''), 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) { diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index b781ac95e..0971300b6 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -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(); - }; - config.onFileUpload = onFileUpload; - config.types = cfg; - config.addCommonRpc = addCommonRpc; - config.modules = { - Cryptpad: Cryptpad, - SFrameChannel: SFrameChannel, - Utils: Utils + SecureModal.$iframe.hide(); }; - FP.$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 = { + hashes: hashes, + password: password, + isTemplate: isTemplate }; - config.data = cfg; config.addCommonRpc = addCommonRpc; config.modules = { Cryptpad: Cryptpad, SFrameChannel: SFrameChannel, Utils: Utils }; - ShareModal.$iframe = $('', {id: 'sbox-share-iframe'}).appendTo($('body')); - ShareModal.modal = Share.create(config); + SecureModal.$iframe = $('', {id: 'sbox-secure-iframe'}).appendTo($('body')); + SecureModal.modal = SecureIframe.create(config); } else if (!cfg.hidden) { - ShareModal.modal.refresh(cfg, function () { - ShareModal.$iframe.show(); + SecureModal.modal.refresh(cfg, function () { + SecureModal.$iframe.show(); }); } if (cfg.hidden) { - ShareModal.$iframe.hide(); + SecureModal.$iframe.hide(); return; } - ShareModal.$iframe.focus(); + SecureModal.$iframe.focus(); }; + + sframeChan.on('Q_FILE_PICKER_OPEN', function (data, cb) { + initSecureModal('filepicker', data || {}, cb); + }); + + sframeChan.on('EV_PROPERTIES_OPEN', function (data) { + initSecureModal('properties', data || {}, null); + }); + + 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 diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index 3fa511c58..ad8d6f77e 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -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); diff --git a/www/common/translations/messages.ca.json b/www/common/translations/messages.ca.json index aca3b8068..a10a9fffd 100644 --- a/www/common/translations/messages.ca.json +++ b/www/common/translations/messages.ca.json @@ -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 (saber-ne més). Es desen al vostre navegador, per tant, si netegeu el vostre historial podríeu perdre'ls.Registreu-vos o Inicieu la sessió per mantenir-los accessibles.", @@ -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 XWiki SAS, 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 XWiki SAS, 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": "Col·laboreu amb Confiança\nFeu créixer les vostres idees conjuntament amb documents compartits mentre la tecnologia Coneixement Zero assegura la vostra privacitat; fins i tot per nosaltres.", + "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" } diff --git a/www/filepicker/main.js b/www/filepicker/main.js deleted file mode 100644 index e63ab457c..000000000 --- a/www/filepicker/main.js +++ /dev/null @@ -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 - }; -}); diff --git a/www/poll/inner.js b/www/poll/inner.js index 5c9f9f182..204835555 100644 --- a/www/poll/inner.js +++ b/www/poll/inner.js @@ -1206,22 +1206,18 @@ define([ updatePublishButton(); if (common.isLoggedIn()) { - var fileDialogCfg = { - onSelect: function (data) { - if (data.type === 'file' && APP.editor) { - var mt = ''; - 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 = ''; + APP.editor.replaceSelection(mt); + return; + } + }); }).appendTo($rightside); var $tags = common.createButton('hashtag', true); diff --git a/www/filepicker/app-filepicker.less b/www/secureiframe/app-secure.less similarity index 84% rename from www/filepicker/app-filepicker.less rename to www/secureiframe/app-secure.less index c0b9e7789..bece9d4c0 100644 --- a/www/filepicker/app-filepicker.less +++ b/www/secureiframe/app-secure.less @@ -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; diff --git a/www/filepicker/inner.html b/www/secureiframe/inner.html similarity index 73% rename from www/filepicker/inner.html rename to www/secureiframe/inner.html index 8090d7abc..29c3cf797 100644 --- a/www/filepicker/inner.html +++ b/www/secureiframe/inner.html @@ -2,7 +2,7 @@ - + - + diff --git a/www/filepicker/inner.js b/www/secureiframe/inner.js similarity index 56% rename from www/filepicker/inner.js rename to www/secureiframe/inner.js index 205a0b8db..dc8f5af3f 100644 --- a/www/filepicker/inner.js +++ b/www/secureiframe/inner.js @@ -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; + }; + + // 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; + }); + }); + }; - var hideFileDialog = function () { - sframeChan.event('EV_FILE_PICKER_CLOSE'); + // 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 data = { - FM: APP.FM - }; - var updateContainer; - var createFileDialog = function () { + var filters = _filters; var types = filters.types || []; + var data = { + FM: APP.FM + }; + // 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 = $('').text(text); - $block.append($description); + $block.append(h('p', text)); - var $filter = $('', {'class': 'cp-modal-form'}).appendTo($block); + // Add filter input + var $filter = $(h('p.cp-modal-form')).appendTo($block); var to; - $('', { + var $input = $('', { type: 'text', 'class': 'cp-filepicker-filter', 'placeholder': Messages.filePicker_filter @@ -111,7 +176,7 @@ define([ to = window.setTimeout(updateContainer, 300); }); - //If file, display the upload button + // If file, display the upload button if (types.indexOf('file') !== -1 && common.isLoggedIn()) { var f = (filters && filters.filter) || {}; delete data.accept; @@ -126,15 +191,16 @@ define([ $filter.append(common.createButton('upload', false, data)); } - var $container = $('', {'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([ $('', {'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); }); }); }; diff --git a/www/share/main.js b/www/secureiframe/main.js similarity index 85% rename from www/share/main.js rename to www/secureiframe/main.js index f1f9a3a3a..ced68bbc6 100644 --- a/www/share/main.js +++ b/www/secureiframe/main.js @@ -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 { diff --git a/www/share/app-share.less b/www/share/app-share.less deleted file mode 100644 index 8efdd17f3..000000000 --- a/www/share/app-share.less +++ /dev/null @@ -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(); -} diff --git a/www/share/index.html b/www/share/index.html deleted file mode 100644 index 8be90cfb5..000000000 --- a/www/share/index.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - CryptPad - - - - - - - - \ No newline at end of file diff --git a/www/share/inner.html b/www/share/inner.html deleted file mode 100644 index 6cf7179ae..000000000 --- a/www/share/inner.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - diff --git a/www/share/inner.js b/www/share/inner.js deleted file mode 100644 index f02af4b8b..000000000 --- a/www/share/inner.js +++ /dev/null @@ -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(); -});
').text(text); - $block.append($description); + $block.append(h('p', text)); - var $filter = $('
', {'class': 'cp-modal-form'}).appendTo($block); + // Add filter input + var $filter = $(h('p.cp-modal-form')).appendTo($block); var to; - $('', { + var $input = $('', { type: 'text', 'class': 'cp-filepicker-filter', 'placeholder': Messages.filePicker_filter @@ -111,7 +176,7 @@ define([ to = window.setTimeout(updateContainer, 300); }); - //If file, display the upload button + // If file, display the upload button if (types.indexOf('file') !== -1 && common.isLoggedIn()) { var f = (filters && filters.filter) || {}; delete data.accept; @@ -126,15 +191,16 @@ define([ $filter.append(common.createButton('upload', false, data)); } - var $container = $('', {'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([ $('', {'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); }); }); }; diff --git a/www/share/main.js b/www/secureiframe/main.js similarity index 85% rename from www/share/main.js rename to www/secureiframe/main.js index f1f9a3a3a..ced68bbc6 100644 --- a/www/share/main.js +++ b/www/secureiframe/main.js @@ -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 { diff --git a/www/share/app-share.less b/www/share/app-share.less deleted file mode 100644 index 8efdd17f3..000000000 --- a/www/share/app-share.less +++ /dev/null @@ -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(); -} diff --git a/www/share/index.html b/www/share/index.html deleted file mode 100644 index 8be90cfb5..000000000 --- a/www/share/index.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - CryptPad - - - - - - - - \ No newline at end of file diff --git a/www/share/inner.html b/www/share/inner.html deleted file mode 100644 index 6cf7179ae..000000000 --- a/www/share/inner.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - diff --git a/www/share/inner.js b/www/share/inner.js deleted file mode 100644 index f02af4b8b..000000000 --- a/www/share/inner.js +++ /dev/null @@ -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(); -});