From 17636769e42805656e41fcde531fd9ce295d379e Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 5 Dec 2017 16:40:04 +0100 Subject: [PATCH] Remove the second network created for the drive --- www/common/cryptpad-common.js | 35 +- www/common/outer/async-store.js | 68 ++- www/common/outer/chainpad-netflux-worker.js | 248 ++++++++ www/common/outer/store-rpc.js | 7 + www/common/outer/userObject.js | 582 ++++++++++++++++++ www/common/sframe-chainpad-netflux-outer.js | 3 - www/common/sframe-common-outer.js | 39 +- www/common/sframe-protocol.js | 10 + www/common/userObject.js | 619 ++------------------ www/drive/inner.js | 189 +++--- www/drive/main.js | 18 + 11 files changed, 1136 insertions(+), 682 deletions(-) create mode 100644 www/common/outer/chainpad-netflux-worker.js create mode 100644 www/common/outer/userObject.js diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 7cdc169ec..555030b74 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -59,11 +59,6 @@ define([ // RESTRICTED // Settings only - common.getUserObject = function (cb) { - postMessage("GET", [], function (obj) { - cb(obj); - }); - }; common.resetDrive = function (cb) { postMessage("RESET_DRIVE", null, function (obj) { if (obj.error) { return void cb(obj.error); } @@ -81,6 +76,12 @@ define([ cb(); }); }; + // Settings and drive + common.getUserObject = function (cb) { + postMessage("GET", [], function (obj) { + cb(obj); + }); + }; // Settings and auth common.getUserObject = function (cb) { postMessage("GET", [], function (obj) { @@ -94,6 +95,11 @@ define([ }; postMessage("MIGRATE_ANON_DRIVE", data, cb); }; + // Drive + common.userObjectCommand = function (data, cb) { + postMessage("DRIVE_USEROBJECT", data, cb); + }; + common.onDriveLog = Util.mkEvent(); // Profile common.getProfileEditUrl = function (cb) { postMessage("GET", ['profile', 'edit'], function (obj) { @@ -450,6 +456,10 @@ define([ }); }; + // Network + common.onNetworkDisconnect = Util.mkEvent(); + common.onNetworkReconnect = Util.mkEvent(); + // Messenger var messenger = common.messenger = {}; messenger.getFriendList = function (cb) { @@ -500,6 +510,10 @@ define([ pad.onLeaveEvent = Util.mkEvent(); pad.onDisconnectEvent = Util.mkEvent(); + common.getFullHistory = function (data, cb) { + postMessage("GET_FULL_HISTORY", data, cb); + }; + common.getShareHashes = function (secret, cb) { var hashes; if (!window.location.hash) { @@ -591,6 +605,13 @@ define([ common.onFriendComplete(data); break; } + // Network + case 'NETWORK_DISCONNECT': { + common.onNetworkDisconnect.fire(); break; + } + case 'NETWORK_RECONNECT': { + common.onNetworkReconnect.fire(data); break; + } // Messenger case 'CONTACTS_MESSAGE': { common.messenger.onMessageEvent.fire(data); break; @@ -626,6 +647,10 @@ define([ case 'PAD_DISCONNECT': { common.padRpc.onDisconnectEvent.fire(data); break; } + // Drive + case 'DRIVE_LOG': { + common.onDriveLog.fire(data); + } } }; diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index eb222d1a2..c8627dbea 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -767,10 +767,66 @@ define([ // TODO // GET_FULL_HISTORY from sframe-common-outer + Store.getFullHistory = function (data, cb) { + var network = store.network; + var hkn = network.historyKeeper; + //var crypto = Crypto.createEncryptor(data.keys); + // Get the history messages and send them to the iframe + var parse = function (msg) { + try { + return JSON.parse(msg); + } catch (e) { + return null; + } + }; + var msgs = []; + var onMsg = function (msg) { + var parsed = parse(msg); + if (parsed[0] === 'FULL_HISTORY_END') { + cb(msgs); + return; + } + if (parsed[0] !== 'FULL_HISTORY') { return; } + if (parsed[1] && parsed[1].validateKey) { // First message + return; + } + if (parsed[1][3] !== data.channel) { return; } + msg = parsed[1][4]; + if (msg) { + msg = msg.replace(/^cp\|/, ''); + //var decryptedMsg = crypto.decrypt(msg, true); + msgs.push(msg); + } + }; + network.on('message', onMsg); + network.sendto(hkn, JSON.stringify(['GET_FULL_HISTORY', data.channel, data.validateKey])); + }; // TODO with sharedworker // when the tab is closed, leave the pad + // Drive + Store.userObjectCommand = function (cmdData, cb) { + if (!cmdData || !cmdData.cmd) { return; } + var data = cmdData.data; + switch (cmdData.cmd) { + case 'move': + store.userObject.move(data.paths, data.newPath, cb); break; + case 'restore': + store.userObject.restore(data.path, cb); break; + case 'addFolder': + store.userObject.addFolder(data.path, data.name, cb); break; + case 'delete': + store.userObject.delete(data.paths, cb, data.nocheck); break; + case 'emptyTrash': + store.userObject.emptyTrash(cb); break; + case 'rename': + store.userObject.rename(data.path, data.newName, cb); break; + default: + cb(); + } + }; + ////////////////////////////////////////////////////////////////// /////////////////////// Init ///////////////////////////////////// ////////////////////////////////////////////////////////////////// @@ -779,7 +835,10 @@ define([ var proxy = store.proxy; var userObject = store.userObject = UserObject.init(proxy.drive, { pinPads: Store.pinPads, - loggedIn: store.loggedIn + loggedIn: store.loggedIn, + log: function (msg) { + postMessage("DRIVE_LOG", msg); + } }); var todo = function () { userObject.fixFiles(); @@ -902,6 +961,13 @@ define([ rt.realtime.abort(); } }); + + rt.proxy.on('disconnect', function () { + postMessage('NETWORK_DISCONNECT'); + }); + rt.proxy.on('reconnect', function (info) { + postMessage('NETWORK_RECONNECT', {myId: info.myId}); + }); }; /** diff --git a/www/common/outer/chainpad-netflux-worker.js b/www/common/outer/chainpad-netflux-worker.js new file mode 100644 index 000000000..7ca8a4104 --- /dev/null +++ b/www/common/outer/chainpad-netflux-worker.js @@ -0,0 +1,248 @@ +/* + * Copyright 2014 XWiki SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +define([], function () { + var USE_HISTORY = true; + + var verbose = function (x) { console.log(x); }; + verbose = function () {}; // comment out to enable verbose logging + + var unBencode = function (str) { return str.replace(/^\d+:/, ''); }; + + var start = function (conf) { + var channel = conf.channel; + var validateKey = conf.validateKey; + var readOnly = conf.readOnly || false; + var network = conf.network; + var onConnect = conf.onConnect || function () { }; + var onMessage = conf.onMessage; + var onJoin = conf.onJoin; + var onLeave = conf.onLeave; + var onReady = conf.onReady; + var onDisconnect = conf.onDisconnect; + conf = undefined; + + var initializing = true; + var lastKnownHash; + + var messageFromOuter = function () {}; + + var onRdy = function () { + // Trigger onReady only if not ready yet. This is important because the history keeper sends a direct + // message through "network" when it is synced, and it triggers onReady for each channel joined. + if (!initializing) { return; } + onReady(); + //sframeChan.event('EV_RT_READY', null); + // we're fully synced + initializing = false; + }; + + // shim between chainpad and netflux + var msgIn = function (peerId, msg) { + return msg.replace(/^cp\|/, ''); + + /*try { + var decryptedMsg = Crypto.decrypt(msg, validateKey); + return decryptedMsg; + } catch (err) { + console.error(err); + return msg; + }*/ + }; + + var msgOut = function (msg) { + if (readOnly) { return; } + return msg; + /*try { + var cmsg = Crypto.encrypt(msg); + if (msg.indexOf('[4') === 0) { cmsg = 'cp|' + cmsg; } + return cmsg; + } catch (err) { + console.log(msg); + throw err; + }*/ + }; + + var onMsg = function(peer, msg, wc, network, direct) { + // unpack the history keeper from the webchannel + var hk = network.historyKeeper; + + if (direct && peer !== hk) { + return; + } + if (direct) { + var parsed = JSON.parse(msg); + if (parsed.validateKey && parsed.channel) { + if (parsed.channel === wc.id && !validateKey) { + validateKey = parsed.validateKey; + } + // We have to return even if it is not the current channel: + // we don't want to continue with other channels messages here + return; + } + if (parsed.state && parsed.state === 1 && parsed.channel) { + if (parsed.channel === wc.id) { + onRdy(); + } + // We have to return even if it is not the current channel: + // we don't want to continue with other channels messages here + return; + } + } + if (peer === hk) { + // if the peer is the 'history keeper', extract their message + var parsed1 = JSON.parse(msg); + msg = parsed1[4]; + // Check that this is a message for our channel + if (parsed1[3] !== wc.id) { return; } + } + + lastKnownHash = msg.slice(0,64); + var message = msgIn(peer, msg); + + verbose(message); + + // slice off the bencoded header + // Why are we getting bencoded stuff to begin with? + // FIXME this shouldn't be necessary + message = unBencode(message);//.slice(message.indexOf(':[') + 1); + + // pass the message into Chainpad + onMessage(message); + //sframeChan.query('Q_RT_MESSAGE', message, function () { }); + }; + + // We use an object to store the webchannel so that we don't have to push new handlers to chainpad + // and remove the old ones when reconnecting and keeping the same 'realtime' object + // See realtime.onMessage below: we call wc.bcast(...) but wc may change + var wcObject = {}; + var onOpen = function(wc, network, firstConnection) { + wcObject.wc = wc; + channel = wc.id; + + // Add the existing peers in the userList + //TODO sframeChan.event('EV_RT_CONNECT', { myID: wc.myID, members: wc.members, readOnly: readOnly }); + + // Add the handlers to the WebChannel + wc.on('message', function (msg, sender) { //Channel msg + onMsg(sender, msg, wc, network); + }); + wc.on('join', function (m) { onJoin(m); /*sframeChan.event('EV_RT_JOIN', m);*/ }); + wc.on('leave', function (m) { onLeave(m); /*sframeChan.event('EV_RT_LEAVE', m);*/ }); + + if (firstConnection) { + // Sending a message... + messageFromOuter = function(message, cb) { + // Filter messages sent by Chainpad to make it compatible with Netflux + message = msgOut(message); + if (message) { + // Do not remove wcObject, it allows us to use a new 'wc' without changing the handler if we + // want to keep the same chainpad (realtime) object + try { + wcObject.wc.bcast(message).then(function() { + cb(); + }, function(err) { + // The message has not been sent, display the error. + console.error(err); + }); + } catch (e) { + console.log(e); + // Just skip calling back and it will fail on the inside. + } + } + }; + } + + onConnect(wc, messageFromOuter); + + // Get the channel history + if (USE_HISTORY) { + var hk; + + wc.members.forEach(function (p) { + if (p.length === 16) { hk = p; } + }); + network.historyKeeper = hk; + + var msg = ['GET_HISTORY', wc.id]; + // Add the validateKey if we are the channel creator and we have a validateKey + msg.push(validateKey); + msg.push(lastKnownHash); + if (hk) { network.sendto(hk, JSON.stringify(msg)); } + } else { + onRdy(); + } + }; + + /*var isIntentionallyLeaving = false; + window.addEventListener("beforeunload", function () { + isIntentionallyLeaving = true; + });*/ + + var findChannelById = function (webChannels, channelId) { + var webChannel; + + // Array.some terminates once a truthy value is returned + // best case is faster than forEach, though webchannel arrays seem + // to consistently have a length of 1 + webChannels.some(function(chan) { + if(chan.id === channelId) { webChannel = chan; return true;} + }); + return webChannel; + }; + + var connectTo = function (network, firstConnection) { + // join the netflux network, promise to handle opening of the channel + network.join(channel || null).then(function(wc) { + onOpen(wc, network, firstConnection); + }, function(error) { + console.error(error); + }); + }; + + network.on('disconnect', function (reason) { + console.log('disconnect'); + //if (isIntentionallyLeaving) { return; } + if (reason === "network.disconnect() called") { return; } + onDisconnect(); + //sframeChan.event('EV_RT_DISCONNECT'); + }); + + network.on('reconnect', function () { + initializing = true; + connectTo(network, false); + }); + + network.on('message', function (msg, sender) { // Direct message + var wchan = findChannelById(network.webChannels, channel); + if (wchan) { + onMsg(sender, msg, wchan, network, true); + } + }); + + connectTo(network, true); + }; + + return { + start: start + /*function (config) { + config.sframeChan.whenReg('EV_RT_READY', function () { + start(config); + }); + }*/ + }; +}); + diff --git a/www/common/outer/store-rpc.js b/www/common/outer/store-rpc.js index e1db437ec..cabaeaea2 100644 --- a/www/common/outer/store-rpc.js +++ b/www/common/outer/store-rpc.js @@ -153,6 +153,13 @@ define([ case 'JOIN_PAD': { Store.joinPad(data, cb); break; } + case 'GET_FULL_HISTORY': { + Store.getFullHistory(data, cb); break; + } + // Drive + case 'DRIVE_USEROBJECT': { + Store.userObjectCommand(data, cb); break; + } default: { break; diff --git a/www/common/outer/userObject.js b/www/common/outer/userObject.js new file mode 100644 index 000000000..10a796e3c --- /dev/null +++ b/www/common/outer/userObject.js @@ -0,0 +1,582 @@ +define([ + '/customize/application_config.js', + '/common/common-util.js', + '/common/common-hash.js', + '/common/common-realtime.js', + '/customize/messages.js' +], function (AppConfig, Util, Hash, Realtime, Messages) { + var module = {}; + + var clone = function (o) { + try { return JSON.parse(JSON.stringify(o)); } + catch (e) { return undefined; } + }; + + module.init = function (config, exp, files) { + var pinPads = config.pinPads; + var loggedIn = config.loggedIn; + var workgroup = config.workgroup; + + var ROOT = exp.ROOT; + var FILES_DATA = exp.FILES_DATA; + var OLD_FILES_DATA = exp.OLD_FILES_DATA; + var UNSORTED = exp.UNSORTED; + var TRASH = exp.TRASH; + var TEMPLATE = exp.TEMPLATE; + + var debug = exp.debug; + + exp.setPadAttribute = function (href, attr, value, cb) { + cb = cb || function () {}; + var id = exp.getIdFromHref(href); + if (!id) { return void cb("E_INVAL_HREF"); } + if (!attr || !attr.trim()) { return void cb("E_INVAL_ATTR"); } + var data = exp.getFileData(id); + data[attr] = clone(value); + cb(null); + }; + exp.getPadAttribute = function (href, attr, cb) { + cb = cb || function () {}; + var id = exp.getIdFromHref(href); + if (!id) { return void cb(null, undefined); } + var data = exp.getFileData(id); + cb(null, clone(data[attr])); + }; + var removePadAttribute = exp.removePadAttribute = function (f) { + if (typeof(f) !== 'string') { + console.error("Can't find pad attribute for an undefined pad"); + return; + } + Object.keys(files).forEach(function (key) { + var hash = f.indexOf('#') !== -1 ? f.slice(f.indexOf('#') + 1) : null; + if (hash && key.indexOf(hash) === 0) { + exp.debug("Deleting pad attribute in the realtime object"); + delete files[key]; + } + }); + }; + + exp.pushData = function (data, cb) { + if (typeof cb !== "function") { cb = function () {}; } + var todo = function () { + var id = Util.createRandomInteger(); + files[FILES_DATA][id] = data; + cb(null, id); + }; + if (!loggedIn || !AppConfig.enablePinning || config.testMode) { + return void todo(); + } + if (!pinPads) { return; } + pinPads([Hash.hrefToHexChannelId(data.href)], function (obj) { + if (obj && obj.error) { return void cb(obj.error); } + todo(); + }); + }; + + // FILES DATA + var spliceFileData = function (id) { + delete files[FILES_DATA][id]; + }; + + exp.checkDeletedFiles = function () { + // Nothing in OLD_FILES_DATA for workgroups + if (workgroup || (!loggedIn && !config.testMode)) { return; } + + var filesList = exp.getFiles([ROOT, 'hrefArray', TRASH]); + exp.getFiles([FILES_DATA]).forEach(function (id) { + if (filesList.indexOf(id) === -1) { + spliceFileData(id); + } + }); + }; + var deleteHrefs = function (ids) { + ids.forEach(function (obj) { + var idx = files[obj.root].indexOf(obj.id); + files[obj.root].splice(idx, 1); + }); + }; + var deleteMultipleTrashRoot = function (roots) { + roots.forEach(function (obj) { + var idx = files[TRASH][obj.name].indexOf(obj.el); + files[TRASH][obj.name].splice(idx, 1); + }); + }; + exp.deleteMultiplePermanently = function (paths, nocheck) { + var hrefPaths = paths.filter(function(x) { return exp.isPathIn(x, ['hrefArray']); }); + var rootPaths = paths.filter(function(x) { return exp.isPathIn(x, [ROOT]); }); + var trashPaths = paths.filter(function(x) { return exp.isPathIn(x, [TRASH]); }); + var allFilesPaths = paths.filter(function(x) { return exp.isPathIn(x, [FILES_DATA]); }); + + if (!loggedIn && !config.testMode) { + allFilesPaths.forEach(function (path) { + var el = exp.find(path); + if (!el) { return; } + var id = exp.getIdFromHref(el.href); + if (!id) { return; } + spliceFileData(id); + removePadAttribute(el.href); + }); + return; + } + + var ids = []; + hrefPaths.forEach(function (path) { + var id = exp.find(path); + ids.push({ + root: path[0], + id: id + }); + }); + deleteHrefs(ids); + + rootPaths.forEach(function (path) { + var parentPath = path.slice(); + var key = parentPath.pop(); + var parentEl = exp.find(parentPath); + delete parentEl[key]; + }); + + var trashRoot = []; + trashPaths.forEach(function (path) { + var parentPath = path.slice(); + var key = parentPath.pop(); + var parentEl = exp.find(parentPath); + // Trash root: we have array here, we can't just splice with the path otherwise we might break the path + // of another element in the loop + if (path.length === 4) { + trashRoot.push({ + name: path[1], + el: parentEl + }); + return; + } + // Trash but not root: it's just a tree so remove the key + delete parentEl[key]; + }); + deleteMultipleTrashRoot(trashRoot); + + // In some cases, we want to remove pads from a location without removing them from + // OLD_FILES_DATA (replaceHref) + if (!nocheck) { exp.checkDeletedFiles(); } + }; + + // Move + var pushToTrash = function (name, element, path) { + var trash = files[TRASH]; + if (typeof(trash[name]) === "undefined") { trash[name] = []; } + var trashArray = trash[name]; + var trashElement = { + element: element, + path: path + }; + trashArray.push(trashElement); + }; + exp.copyElement = function (elementPath, newParentPath) { + if (exp.comparePath(elementPath, newParentPath)) { return; } // Nothing to do... + var element = exp.find(elementPath); + var newParent = exp.find(newParentPath); + + // Move to Trash + if (exp.isPathIn(newParentPath, [TRASH])) { + if (!elementPath || elementPath.length < 2 || elementPath[0] === TRASH) { + debug("Can't move an element from the trash to the trash: ", elementPath); + return; + } + var key = elementPath[elementPath.length - 1]; + var elName = exp.isPathIn(elementPath, ['hrefArray']) ? exp.getTitle(element) : key; + var parentPath = elementPath.slice(); + parentPath.pop(); + pushToTrash(elName, element, parentPath); + return true; + } + // Move to hrefArray + if (exp.isPathIn(newParentPath, ['hrefArray'])) { + if (exp.isFolder(element)) { + exp.log(Messages.fo_moveUnsortedError); + return; + } else { + if (elementPath[0] === newParentPath[0]) { return; } + var fileRoot = newParentPath[0]; + if (files[fileRoot].indexOf(element) === -1) { + files[fileRoot].push(element); + } + return true; + } + } + // Move to root + var newName = exp.isFile(element) ? + exp.getAvailableName(newParent, Hash.createChannelId()) : + exp.isInTrashRoot(elementPath) ? + elementPath[1] : elementPath.pop(); + + if (typeof(newParent[newName]) !== "undefined") { + exp.log(Messages.fo_unavailableName); + return; + } + newParent[newName] = element; + return true; + }; + + // FORGET (move with href not path) + exp.forget = function (href) { + var id = exp.getIdFromHref(href); + if (!id) { return; } + if (!loggedIn && !config.testMode) { + // delete permanently + exp.removePadAttribute(href); + spliceFileData(id); + return; + } + var paths = exp.findFile(id); + exp.move(paths, [TRASH]); + }; + + // REPLACE + exp.replace = function (o, n) { + var idO = exp.getIdFromHref(o); + if (!idO || !exp.isFile(idO)) { return; } + var data = exp.getFileData(idO); + if (!data) { return; } + data.href = n; + }; + // If all the occurences of an href are in the trash, remvoe them and add the file in root. + // This is use with setPadTitle when we open a stronger version of a deleted pad + exp.restoreHref = function (href) { + var idO = exp.getIdFromHref(href); + + if (!idO || !exp.isFile(idO)) { return; } + + var paths = exp.findFile(idO); + + // Remove all the occurences in the trash + // If all the occurences are in the trash or no occurence, add the pad to root + var allInTrash = true; + paths.forEach(function (p) { + if (p[0] === TRASH) { + exp.delete(p, null, true); // 3rd parameter means skip "checkDeletedFiles" + return; + } + allInTrash = false; + }); + if (allInTrash) { + exp.add(idO); + } + }; + + exp.add = function (id, path) { + // TODO WW + if (!loggedIn && !config.testMode) { return; } + var data = files[FILES_DATA][id]; + if (!data || typeof(data) !== "object") { return; } + var newPath = path, parentEl; + if (path && !Array.isArray(path)) { + newPath = decodeURIComponent(path).split(','); + } + // Add to href array + if (path && exp.isPathIn(newPath, ['hrefArray'])) { + parentEl = exp.find(newPath); + parentEl.push(id); + return; + } + // Add to root if path is ROOT or if no path + var filesList = exp.getFiles([ROOT, TRASH, 'hrefArray']); + if (path && exp.isPathIn(newPath, [ROOT]) || filesList.indexOf(id) === -1) { + parentEl = exp.find(newPath || [ROOT]); + if (parentEl) { + var newName = exp.getAvailableName(parentEl, Hash.createChannelId()); + parentEl[newName] = id; + return; + } + } + }; + + /** + * INTEGRITY CHECK + */ + + exp.migrate = function (cb) { + // Make sure unsorted doesn't exist anymore + // Note: Unsorted only works with the old structure where pads are href + // It should be called before the migration code + var fixUnsorted = function () { + if (!files[UNSORTED] || !files[OLD_FILES_DATA]) { return; } + debug("UNSORTED still exists in the object, removing it..."); + var us = files[UNSORTED]; + if (us.length === 0) { + delete files[UNSORTED]; + return; + } + us.forEach(function (el) { + if (typeof el !== "string") { + return; + } + var data = files[OLD_FILES_DATA].filter(function (x) { + return x.href === el; + }); + if (data.length === 0) { + files[OLD_FILES_DATA].push({ + href: el + }); + } + return; + }); + delete files[UNSORTED]; + }; + // mergeDrive... + var migrateToNewFormat = function (todo) { + if (!files[OLD_FILES_DATA]) { + return void todo(); + } + try { + debug("Migrating file system..."); + files.migrate = 1; + var next = function () { + var oldData = files[OLD_FILES_DATA].slice(); + if (!files[FILES_DATA]) { + files[FILES_DATA] = {}; + } + var newData = files[FILES_DATA]; + //var oldFiles = oldData.map(function (o) { return o.href; }); + oldData.forEach(function (obj) { + if (!obj || !obj.href) { return; } + var href = obj.href; + var id = Util.createRandomInteger(); + var paths = exp.findFile(href); + var data = obj; + var key = Hash.createChannelId(); + if (data) { + newData[id] = data; + } else { + newData[id] = {href: href}; + } + paths.forEach(function (p) { + var parentPath = p.slice(); + var okey = parentPath.pop(); // get the parent + var parent = exp.find(parentPath); + if (exp.isInTrashRoot(p)) { + parent.element = id; + newData[id].filename = p[1]; + return; + } + if (exp.isPathIn(p, ['hrefArray'])) { + parent[okey] = id; + return; + } + // else root or trash (not trashroot) + parent[key] = id; + newData[id].filename = okey; + delete parent[okey]; + }); + }); + delete files[OLD_FILES_DATA]; + delete files.migrate; + console.log('done'); + todo(); + }; + if (exp.rt) { + exp.rt.sync(); + // TODO + Realtime.whenRealtimeSyncs(exp.rt, next); + } else { + window.setTimeout(next, 1000); + } + } catch(e) { + console.error(e); + todo(); + } + }; + + fixUnsorted(); + migrateToNewFormat(cb); + }; + + exp.fixFiles = function () { + // Explore the tree and check that everything is correct: + // * 'root', 'trash', 'unsorted' and 'filesData' exist and are objects + // * ROOT: Folders are objects, files are href + // * TRASH: Trash root contains only arrays, each element of the array is an object {element:.., path:..} + // * OLD_FILES_DATA: - Data (title, cdate, adte) are stored in filesData. filesData contains only href keys linking to object with title, cdate, adate. + // - Dates (adate, cdate) can be parsed/formatted + // - All files in filesData should be either in 'root', 'trash' or 'unsorted'. If that's not the case, copy the fily to 'unsorted' + // * TEMPLATE: Contains only files (href), and does not contains files that are in ROOT + debug("Cleaning file system..."); + + var before = JSON.stringify(files); + + var fixRoot = function (elem) { + if (typeof(files[ROOT]) !== "object") { debug("ROOT was not an object"); files[ROOT] = {}; } + var element = elem || files[ROOT]; + for (var el in element) { + if (!exp.isFile(element[el], true) && !exp.isFolder(element[el])) { + debug("An element in ROOT was not a folder nor a file. ", element[el]); + delete element[el]; + continue; + } + if (exp.isFolder(element[el])) { + fixRoot(element[el]); + continue; + } + if (typeof element[el] === "string") { + // We have an old file (href) which is not in filesData: add it + var id = Util.createRandomInteger(); + var key = Hash.createChannelId(); + files[FILES_DATA][id] = {href: element[el], filename: el}; + element[key] = id; + delete element[el]; + } + if (typeof element[el] === "number") { + var data = files[FILES_DATA][element[el]]; + if (!data) { + debug("An element in ROOT doesn't have associated data", element[el], el); + delete element[el]; + } + } + } + }; + var fixTrashRoot = function () { + if (typeof(files[TRASH]) !== "object") { debug("TRASH was not an object"); files[TRASH] = {}; } + var tr = files[TRASH]; + var toClean; + var addToClean = function (obj, idx, el) { + if (typeof(obj) !== "object") { toClean.push(idx); return; } + if (!exp.isFile(obj.element, true) && + !exp.isFolder(obj.element)) { toClean.push(idx); return; } + if (!Array.isArray(obj.path)) { toClean.push(idx); return; } + if (typeof obj.element === "string") { + // We have an old file (href) which is not in filesData: add it + var id = Util.createRandomInteger(); + files[FILES_DATA][id] = {href: obj.element, filename: el}; + obj.element = id; + } + if (exp.isFolder(obj.element)) { fixRoot(obj.element); } + if (typeof obj.element === "number") { + var data = files[FILES_DATA][obj.element]; + if (!data) { + debug("An element in TRASH doesn't have associated data", obj.element, el); + toClean.push(idx); + } + } + + }; + for (var el in tr) { + if (!Array.isArray(tr[el])) { + debug("An element in TRASH root is not an array. ", tr[el]); + delete tr[el]; + } else if (tr[el].length === 0) { + debug("Empty array in TRASH root. ", tr[el]); + delete tr[el]; + } else { + toClean = []; + for (var j=0; j=0; i--) { + tr[el].splice(toClean[i], 1); + } + } + } + }; + var fixTemplate = function () { + if (!Array.isArray(files[TEMPLATE])) { debug("TEMPLATE was not an array"); files[TEMPLATE] = []; } + files[TEMPLATE] = Util.deduplicateString(files[TEMPLATE].slice()); + var us = files[TEMPLATE]; + var rootFiles = exp.getFiles([ROOT]).slice(); + var toClean = []; + us.forEach(function (el, idx) { + if (!exp.isFile(el, true) || rootFiles.indexOf(el) !== -1) { + toClean.push(el); + } + if (typeof el === "string") { + // We have an old file (href) which is not in filesData: add it + var id = Util.createRandomInteger(); + files[FILES_DATA][id] = {href: el}; + us[idx] = id; + } + if (typeof el === "number") { + var data = files[FILES_DATA][el]; + if (!data) { + debug("An element in TEMPLATE doesn't have associated data", el); + toClean.push(el); + } + } + }); + toClean.forEach(function (el) { + var idx = us.indexOf(el); + if (idx !== -1) { + us.splice(idx, 1); + } + }); + }; + var fixFilesData = function () { + if (typeof files[FILES_DATA] !== "object") { debug("OLD_FILES_DATA was not an object"); files[FILES_DATA] = {}; } + var fd = files[FILES_DATA]; + var rootFiles = exp.getFiles([ROOT, TRASH, 'hrefArray']); + var root = exp.find([ROOT]); + var toClean = []; + for (var id in fd) { + id = Number(id); + var el = fd[id]; + if (!el || typeof(el) !== "object") { + debug("An element in filesData was not an object.", el); + toClean.push(id); + continue; + } + if (!el.href) { + debug("Removing an element in filesData with a missing href.", el); + toClean.push(id); + continue; + } + if (/^https*:\/\//.test(el.href)) { el.href = Hash.getRelativeHref(el.href); } + if (!el.ctime) { el.ctime = el.atime; } + + var parsed = Hash.parsePadUrl(el.href); + if (!el.title) { el.title = Hash.getDefaultName(parsed); } + if (!parsed.hash) { + debug("Removing an element in filesData with a invalid href.", el); + toClean.push(id); + continue; + } + if (!parsed.type) { + debug("Removing an element in filesData with a invalid type.", el); + toClean.push(id); + continue; + } + + if ((loggedIn || config.testMode) && rootFiles.indexOf(id) === -1) { + debug("An element in filesData was not in ROOT, TEMPLATE or TRASH.", id, el); + var newName = Hash.createChannelId(); + root[newName] = id; + continue; + } + } + toClean.forEach(function (id) { + spliceFileData(id); + }); + }; + + var fixDrive = function () { + Object.keys(files).forEach(function (key) { + if (key.slice(0,1) === '/') { delete files[key]; } + }); + }; + + fixRoot(); + fixTrashRoot(); + if (!workgroup) { + fixTemplate(); + fixFilesData(); + } + fixDrive(); + + if (JSON.stringify(files) !== before) { + debug("Your file system was corrupted. It has been cleaned so that the pads you visit can be stored safely"); + return; + } + debug("File system was clean"); + }; + + return exp; + }; + + return module; +}); diff --git a/www/common/sframe-chainpad-netflux-outer.js b/www/common/sframe-chainpad-netflux-outer.js index 8ca920bbe..7d991a273 100644 --- a/www/common/sframe-chainpad-netflux-outer.js +++ b/www/common/sframe-chainpad-netflux-outer.js @@ -15,8 +15,6 @@ * along with this program. If not, see . */ define([], function () { - var USE_HISTORY = true; - var verbose = function (x) { console.log(x); }; verbose = function () {}; // comment out to enable verbose logging @@ -81,7 +79,6 @@ define([], function () { var onOpen = function(data) { // Add the existing peers in the userList - console.log(data); onConnect(data.id); onConnect = function () {}; diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index da08bdc37..ca7d1d0e0 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -298,36 +298,15 @@ define([ }; sframeChan.on('Q_GET_FULL_HISTORY', function (data, cb) { - var hkn = network.historyKeeper; var crypto = Crypto.createEncryptor(secret.keys); - // Get the history messages and send them to the iframe - var parse = function (msg) { - try { - return JSON.parse(msg); - } catch (e) { - return null; - } - }; - var msgs = []; - var onMsg = function (msg) { - var parsed = parse(msg); - if (parsed[0] === 'FULL_HISTORY_END') { - cb(msgs); - return; - } - if (parsed[0] !== 'FULL_HISTORY') { return; } - if (parsed[1] && parsed[1].validateKey) { // First message - return; - } - msg = parsed[1][4]; - if (msg) { - msg = msg.replace(/^cp\|/, ''); - var decryptedMsg = crypto.decrypt(msg, true); - msgs.push(decryptedMsg); - } - }; - network.on('message', onMsg); - network.sendto(hkn, JSON.stringify(['GET_FULL_HISTORY', secret.channel, secret.keys.validateKey])); + Cryptpad.getFullHistory({ + channel: secret.channel, + validateKey: secret.keys.validateKey + }, function (encryptedMsgs) { + cb(encryptedMsgs.map(function (msg) { + return crypto.decrypt(msg, true); + })); + }); }); sframeChan.on('Q_GET_PAD_ATTRIBUTE', function (data, cb) { @@ -594,7 +573,7 @@ define([ return; } if (readOnly || cfg.noHash) { return; } - replaceHash(Utils.Hash.getEditHashFromKeys(wc.id, secret.keys)); + replaceHash(Utils.Hash.getEditHashFromKeys(wc, secret.keys)); } }); }); diff --git a/www/common/sframe-protocol.js b/www/common/sframe-protocol.js index b63e516bc..f33d52c13 100644 --- a/www/common/sframe-protocol.js +++ b/www/common/sframe-protocol.js @@ -197,4 +197,14 @@ define({ // Anonymous users can empty their drive and remove FS_hash from localStorage 'EV_BURN_ANON_DRIVE': true, + + // Inner drive needs to send command and receive updates from the async store + 'Q_DRIVE_USEROBJECT': true, + 'Q_DRIVE_GETOBJECT': true, + // Store's userObject need to send log messages to inner to display them in the UI + 'EV_DRIVE_LOG': true, + + // Notifications about connection and disconnection from the network + 'EV_NETWORK_DISCONNECT': true, + 'EV_NETWORK_RECONNECT': true }); diff --git a/www/common/userObject.js b/www/common/userObject.js index 344677ea8..2b9b47d38 100644 --- a/www/common/userObject.js +++ b/www/common/userObject.js @@ -4,8 +4,9 @@ define([ '/common/common-hash.js', '/common/common-realtime.js', '/common/common-constants.js', + '/common/outer/userObject.js', '/customize/messages.js' -], function (AppConfig, Util, Hash, Realtime, Constants, Messages) { +], function (AppConfig, Util, Hash, Realtime, Constants, OuterFO, Messages) { var module = {}; var ROOT = module.ROOT = "root"; @@ -13,15 +14,10 @@ define([ var TRASH = module.TRASH = "trash"; var TEMPLATE = module.TEMPLATE = "template"; - var clone = function (o) { - try { return JSON.parse(JSON.stringify(o)); } - catch (e) { return undefined; } - }; - module.init = function (files, config) { var exp = {}; var pinPads = config.pinPads; - var loggedIn = config.loggedIn; + var sframeChan = config.sframeChan; var FILES_DATA = module.FILES_DATA = exp.FILES_DATA = Constants.storageKey; var OLD_FILES_DATA = module.OLD_FILES_DATA = exp.OLD_FILES_DATA = Constants.oldStorageKey; @@ -39,7 +35,7 @@ define([ }; var log = config.log || logging; var logError = config.logError || logging; - var debug = config.debug || logging; + var debug = exp.debug = config.debug || logging; var error = exp.error = function() { exp.fixFiles(); console.error.apply(console, arguments); @@ -48,6 +44,11 @@ define([ // TODO: workgroup var workgroup = config.workgroup; + if (pinPads) { + // Extend "exp" with methods used only outside of the iframe (requires access to store) + OuterFO.init(config, exp, files); + } + /* * UTILS @@ -146,26 +147,10 @@ define([ if (type === 'name') { return data.filename; } return data.filename || data.title || NEW_FILE_NAME; }; - exp.getPadAttribute = function (href, attr, cb) { - cb = cb || function () {}; - var id = exp.getIdFromHref(href); - if (!id) { return void cb(null, undefined); } - var data = getFileData(id); - cb(null, clone(data[attr])); - }; - exp.setPadAttribute = function (href, attr, value, cb) { - cb = cb || function () {}; - var id = exp.getIdFromHref(href); - if (!id) { return void cb("E_INVAL_HREF"); } - if (!attr || !attr.trim()) { return void cb("E_INVAL_ATTR"); } - var data = getFileData(id); - data[attr] = clone(value); - cb(null); - }; // PATHS - var comparePath = exp.comparePath = function (a, b) { + var comparePath = exp.comparePath = function (a, b) { if (!a || !b || !Array.isArray(a) || !Array.isArray(b)) { return false; } if (a.length !== b.length) { return false; } var result = true; @@ -473,7 +458,7 @@ define([ * OPERATIONS */ - var getAvailableName = function (parentEl, name) { + var getAvailableName = exp.getAvailableName = function (parentEl, name) { if (typeof(parentEl[name]) === "undefined") { return name; } var newName = name; var i = 1; @@ -484,84 +469,17 @@ define([ return newName; }; - // FILES DATA - exp.pushData = function (data, cb) { - if (typeof cb !== "function") { cb = function () {}; } - var todo = function () { - var id = Util.createRandomInteger(); - files[FILES_DATA][id] = data; - cb(null, id); - }; - if (!loggedIn || !AppConfig.enablePinning || config.testMode) { - return void todo(); - } - if (!pinPads) { return; } - pinPads([Hash.hrefToHexChannelId(data.href)], function (obj) { - if (obj && obj.error) { return void cb(obj.error); } - todo(); - }); - }; - var spliceFileData = exp.removeData = function (id) { - delete files[FILES_DATA][id]; - }; - // MOVE - var pushToTrash = function (name, element, path) { - var trash = files[TRASH]; - if (typeof(trash[name]) === "undefined") { trash[name] = []; } - var trashArray = trash[name]; - var trashElement = { - element: element, - path: path - }; - trashArray.push(trashElement); - }; - var copyElement = function (elementPath, newParentPath) { - if (comparePath(elementPath, newParentPath)) { return; } // Nothing to do... - var element = find(elementPath); - var newParent = find(newParentPath); - - // Move to Trash - if (isPathIn(newParentPath, [TRASH])) { - if (!elementPath || elementPath.length < 2 || elementPath[0] === TRASH) { - debug("Can't move an element from the trash to the trash: ", elementPath); - return; - } - var key = elementPath[elementPath.length - 1]; - var elName = isPathIn(elementPath, ['hrefArray']) ? getTitle(element) : key; - var parentPath = elementPath.slice(); - parentPath.pop(); - pushToTrash(elName, element, parentPath); - return true; - } - // Move to hrefArray - if (isPathIn(newParentPath, ['hrefArray'])) { - if (isFolder(element)) { - log(Messages.fo_moveUnsortedError); - return; - } else { - if (elementPath[0] === newParentPath[0]) { return; } - var fileRoot = newParentPath[0]; - if (files[fileRoot].indexOf(element) === -1) { - files[fileRoot].push(element); + var move = exp.move = function (paths, newPath, cb) { + if (sframeChan) { + return void sframeChan.query("Q_DRIVE_USEROBJECT", { + cmd: "move", + data: { + paths: paths, + newPath: newPath } - return true; - } - } - // Move to root - var newName = isFile(element) ? - getAvailableName(newParent, Hash.createChannelId()) : - isInTrashRoot(elementPath) ? - elementPath[1] : elementPath.pop(); - - if (typeof(newParent[newName]) !== "undefined") { - log(Messages.fo_unavailableName); - return; + }, cb); } - newParent[newName] = element; - return true; - }; - var move = exp.move = function (paths, newPath, cb) { // Copy the elements to their new location var toRemove = []; paths.forEach(function (p) { @@ -573,13 +491,21 @@ define([ return; } // Try to copy, and if success, remove the element from the old location - if (copyElement(p.slice(), newPath)) { + if (exp.copyElement(p.slice(), newPath)) { toRemove.push(p); } }); exp.delete(toRemove, cb); }; exp.restore = function (path, cb) { + if (sframeChan) { + return void sframeChan.query("Q_DRIVE_USEROBJECT", { + cmd: "restore", + data: { + path: path + } + }, cb); + } if (!isInTrashRoot(path)) { return; } var parentPath = path.slice(); parentPath.pop(); @@ -589,165 +515,66 @@ define([ // ADD - var add = exp.add = function (id, path) { - if (!loggedIn && !config.testMode) { return; } - var data = files[FILES_DATA][id]; - if (!data || typeof(data) !== "object") { return; } - var newPath = path, parentEl; - if (path && !Array.isArray(path)) { - newPath = decodeURIComponent(path).split(','); - } - // Add to href array - if (path && isPathIn(newPath, ['hrefArray'])) { - parentEl = find(newPath); - parentEl.push(id); - return; - } - // Add to root if path is ROOT or if no path - var filesList = getFiles([ROOT, TRASH, 'hrefArray']); - if (path && isPathIn(newPath, [ROOT]) || filesList.indexOf(id) === -1) { - parentEl = find(newPath || [ROOT]); - if (parentEl) { - var newName = getAvailableName(parentEl, Hash.createChannelId()); - parentEl[newName] = id; - return; - } - } - }; exp.addFolder = function (folderPath, name, cb) { + if (sframeChan) { + return void sframeChan.query("Q_DRIVE_USEROBJECT", { + cmd: "addFolder", + data: { + path: folderPath, + name: name + } + }, cb); + } var parentEl = find(folderPath); var folderName = getAvailableName(parentEl, name || NEW_FOLDER_NAME); parentEl[folderName] = {}; var newPath = folderPath.slice(); newPath.push(folderName); - cb(void 0, { + cb({ newPath: newPath }); }; - // FORGET (move with href not path) - exp.forget = function (href) { - var id = getIdFromHref(href); - if (!id) { return; } - if (!loggedIn && !config.testMode) { - // delete permanently - exp.removePadAttribute(href); - spliceFileData(id); - return; - } - var paths = findFile(id); - move(paths, [TRASH]); - }; - // DELETE // Permanently delete multiple files at once using a list of paths // NOTE: We have to be careful when removing elements from arrays (trash root, unsorted or template) - var removePadAttribute = exp.removePadAttribute = function (f) { - if (typeof(f) !== 'string') { - console.error("Can't find pad attribute for an undefined pad"); - return; - } - Object.keys(files).forEach(function (key) { - var hash = f.indexOf('#') !== -1 ? f.slice(f.indexOf('#') + 1) : null; - if (hash && key.indexOf(hash) === 0) { - debug("Deleting pad attribute in the realtime object"); - delete files[key]; - } - }); - }; - var checkDeletedFiles = function () { - // Nothing in OLD_FILES_DATA for workgroups - if (workgroup || (!loggedIn && !config.testMode)) { return; } - - var filesList = getFiles([ROOT, 'hrefArray', TRASH]); - getFiles([FILES_DATA]).forEach(function (id) { - if (filesList.indexOf(id) === -1) { - spliceFileData(id); - } - }); - }; - var deleteHrefs = function (ids) { - ids.forEach(function (obj) { - var idx = files[obj.root].indexOf(obj.id); - files[obj.root].splice(idx, 1); - }); - }; - var deleteMultipleTrashRoot = function (roots) { - roots.forEach(function (obj) { - var idx = files[TRASH][obj.name].indexOf(obj.el); - files[TRASH][obj.name].splice(idx, 1); - }); - }; - var deleteMultiplePermanently = function (paths, nocheck) { - var hrefPaths = paths.filter(function(x) { return isPathIn(x, ['hrefArray']); }); - var rootPaths = paths.filter(function(x) { return isPathIn(x, [ROOT]); }); - var trashPaths = paths.filter(function(x) { return isPathIn(x, [TRASH]); }); - var allFilesPaths = paths.filter(function(x) { return isPathIn(x, [FILES_DATA]); }); - - if (!loggedIn && !config.testMode) { - allFilesPaths.forEach(function (path) { - var el = find(path); - if (!el) { return; } - var id = getIdFromHref(el.href); - if (!id) { return; } - spliceFileData(id); - removePadAttribute(el.href); - }); - return; - } - - var ids = []; - hrefPaths.forEach(function (path) { - var id = find(path); - ids.push({ - root: path[0], - id: id - }); - }); - deleteHrefs(ids); - - rootPaths.forEach(function (path) { - var parentPath = path.slice(); - var key = parentPath.pop(); - var parentEl = find(parentPath); - delete parentEl[key]; - }); - - var trashRoot = []; - trashPaths.forEach(function (path) { - var parentPath = path.slice(); - var key = parentPath.pop(); - var parentEl = find(parentPath); - // Trash root: we have array here, we can't just splice with the path otherwise we might break the path - // of another element in the loop - if (path.length === 4) { - trashRoot.push({ - name: path[1], - el: parentEl - }); - return; - } - // Trash but not root: it's just a tree so remove the key - delete parentEl[key]; - }); - deleteMultipleTrashRoot(trashRoot); - - // In some cases, we want to remove pads from a location without removing them from - // OLD_FILES_DATA (replaceHref) - if (!nocheck) { checkDeletedFiles(); } - }; exp.delete = function (paths, cb, nocheck) { - deleteMultiplePermanently(paths, nocheck); + if (sframeChan) { + return void sframeChan.query("Q_DRIVE_USEROBJECT", { + cmd: "delete", + data: { + paths: paths, + nocheck: nocheck + } + }, cb); + } + exp.deleteMultiplePermanently(paths, nocheck); if (typeof cb === "function") { cb(); } }; exp.emptyTrash = function (cb) { + if (sframeChan) { + return void sframeChan.query("Q_DRIVE_USEROBJECT", { + cmd: "emptyTrash" + }, cb); + } files[TRASH] = {}; - checkDeletedFiles(); + exp.checkDeletedFiles(); if(cb) { cb(); } }; // RENAME exp.rename = function (path, newName, cb) { + if (sframeChan) { + console.log(path, newName); + return void sframeChan.query("Q_DRIVE_USEROBJECT", { + cmd: "rename", + data: { + path: path, + newName: newName + } + }, cb); + } + console.log(path, newName); if (path.length <= 1) { logError('Renaming `root` is forbidden'); return; @@ -784,322 +611,6 @@ define([ if (typeof cb === "function") { cb(); } }; - // REPLACE - exp.replace = function (o, n) { - var idO = getIdFromHref(o); - if (!idO || !isFile(idO)) { return; } - var data = getFileData(idO); - if (!data) { return; } - data.href = n; - }; - // If all the occurences of an href are in the trash, remvoe them and add the file in root. - // This is use with setPadTitle when we open a stronger version of a deleted pad - exp.restoreHref = function (href) { - var idO = getIdFromHref(href); - - if (!idO || !isFile(idO)) { return; } - - var paths = findFile(idO); - - // Remove all the occurences in the trash - // If all the occurences are in the trash or no occurence, add the pad to root - var allInTrash = true; - paths.forEach(function (p) { - if (p[0] === TRASH) { - exp.delete(p, null, true); // 3rd parameter means skip "checkDeletedFiles" - return; - } - allInTrash = false; - }); - if (allInTrash) { - add(idO); - } - }; - - /** - * INTEGRITY CHECK - */ - - exp.migrate = function (cb) { - // Make sure unsorted doesn't exist anymore - // Note: Unsorted only works with the old structure where pads are href - // It should be called before the migration code - var fixUnsorted = function () { - if (!files[UNSORTED] || !files[OLD_FILES_DATA]) { return; } - debug("UNSORTED still exists in the object, removing it..."); - var us = files[UNSORTED]; - if (us.length === 0) { - delete files[UNSORTED]; - return; - } - us.forEach(function (el) { - if (typeof el !== "string") { - return; - } - var data = files[OLD_FILES_DATA].filter(function (x) { - return x.href === el; - }); - if (data.length === 0) { - files[OLD_FILES_DATA].push({ - href: el - }); - } - return; - }); - delete files[UNSORTED]; - }; - // mergeDrive... - var migrateToNewFormat = function (todo) { - if (!files[OLD_FILES_DATA]) { - return void todo(); - } - try { - debug("Migrating file system..."); - files.migrate = 1; - var next = function () { - var oldData = files[OLD_FILES_DATA].slice(); - if (!files[FILES_DATA]) { - files[FILES_DATA] = {}; - } - var newData = files[FILES_DATA]; - //var oldFiles = oldData.map(function (o) { return o.href; }); - oldData.forEach(function (obj) { - if (!obj || !obj.href) { return; } - var href = obj.href; - var id = Util.createRandomInteger(); - var paths = findFile(href); - var data = obj; - var key = Hash.createChannelId(); - if (data) { - newData[id] = data; - } else { - newData[id] = {href: href}; - } - paths.forEach(function (p) { - var parentPath = p.slice(); - var okey = parentPath.pop(); // get the parent - var parent = find(parentPath); - if (isInTrashRoot(p)) { - parent.element = id; - newData[id].filename = p[1]; - return; - } - if (isPathIn(p, ['hrefArray'])) { - parent[okey] = id; - return; - } - // else root or trash (not trashroot) - parent[key] = id; - newData[id].filename = okey; - delete parent[okey]; - }); - }); - delete files[OLD_FILES_DATA]; - delete files.migrate; - console.log('done'); - todo(); - }; - if (exp.rt) { - exp.rt.sync(); - // TODO - Realtime.whenRealtimeSyncs(exp.rt, next); - } else { - window.setTimeout(next, 1000); - } - } catch(e) { - console.error(e); - todo(); - } - }; - - fixUnsorted(); - migrateToNewFormat(cb); - }; - - exp.fixFiles = function () { - // Explore the tree and check that everything is correct: - // * 'root', 'trash', 'unsorted' and 'filesData' exist and are objects - // * ROOT: Folders are objects, files are href - // * TRASH: Trash root contains only arrays, each element of the array is an object {element:.., path:..} - // * OLD_FILES_DATA: - Data (title, cdate, adte) are stored in filesData. filesData contains only href keys linking to object with title, cdate, adate. - // - Dates (adate, cdate) can be parsed/formatted - // - All files in filesData should be either in 'root', 'trash' or 'unsorted'. If that's not the case, copy the fily to 'unsorted' - // * TEMPLATE: Contains only files (href), and does not contains files that are in ROOT - debug("Cleaning file system..."); - - var before = JSON.stringify(files); - - var fixRoot = function (elem) { - if (typeof(files[ROOT]) !== "object") { debug("ROOT was not an object"); files[ROOT] = {}; } - var element = elem || files[ROOT]; - for (var el in element) { - if (!isFile(element[el], true) && !isFolder(element[el])) { - debug("An element in ROOT was not a folder nor a file. ", element[el]); - delete element[el]; - continue; - } - if (isFolder(element[el])) { - fixRoot(element[el]); - continue; - } - if (typeof element[el] === "string") { - // We have an old file (href) which is not in filesData: add it - var id = Util.createRandomInteger(); - var key = Hash.createChannelId(); - files[FILES_DATA][id] = {href: element[el], filename: el}; - element[key] = id; - delete element[el]; - } - if (typeof element[el] === "number") { - var data = files[FILES_DATA][element[el]]; - if (!data) { - debug("An element in ROOT doesn't have associated data", element[el], el); - delete element[el]; - } - } - } - }; - var fixTrashRoot = function () { - if (typeof(files[TRASH]) !== "object") { debug("TRASH was not an object"); files[TRASH] = {}; } - var tr = files[TRASH]; - var toClean; - var addToClean = function (obj, idx, el) { - if (typeof(obj) !== "object") { toClean.push(idx); return; } - if (!isFile(obj.element, true) && !isFolder(obj.element)) { toClean.push(idx); return; } - if (!Array.isArray(obj.path)) { toClean.push(idx); return; } - if (typeof obj.element === "string") { - // We have an old file (href) which is not in filesData: add it - var id = Util.createRandomInteger(); - files[FILES_DATA][id] = {href: obj.element, filename: el}; - obj.element = id; - } - if (isFolder(obj.element)) { fixRoot(obj.element); } - if (typeof obj.element === "number") { - var data = files[FILES_DATA][obj.element]; - if (!data) { - debug("An element in TRASH doesn't have associated data", obj.element, el); - toClean.push(idx); - } - } - - }; - for (var el in tr) { - if (!Array.isArray(tr[el])) { - debug("An element in TRASH root is not an array. ", tr[el]); - delete tr[el]; - } else if (tr[el].length === 0) { - debug("Empty array in TRASH root. ", tr[el]); - delete tr[el]; - } else { - toClean = []; - for (var j=0; j=0; i--) { - tr[el].splice(toClean[i], 1); - } - } - } - }; - var fixTemplate = function () { - if (!Array.isArray(files[TEMPLATE])) { debug("TEMPLATE was not an array"); files[TEMPLATE] = []; } - files[TEMPLATE] = Util.deduplicateString(files[TEMPLATE].slice()); - var us = files[TEMPLATE]; - var rootFiles = getFiles([ROOT]).slice(); - var toClean = []; - us.forEach(function (el, idx) { - if (!isFile(el, true) || rootFiles.indexOf(el) !== -1) { - toClean.push(el); - } - if (typeof el === "string") { - // We have an old file (href) which is not in filesData: add it - var id = Util.createRandomInteger(); - files[FILES_DATA][id] = {href: el}; - us[idx] = id; - } - if (typeof el === "number") { - var data = files[FILES_DATA][el]; - if (!data) { - debug("An element in TEMPLATE doesn't have associated data", el); - toClean.push(el); - } - } - }); - toClean.forEach(function (el) { - var idx = us.indexOf(el); - if (idx !== -1) { - us.splice(idx, 1); - } - }); - }; - var fixFilesData = function () { - if (typeof files[FILES_DATA] !== "object") { debug("OLD_FILES_DATA was not an object"); files[FILES_DATA] = {}; } - var fd = files[FILES_DATA]; - var rootFiles = getFiles([ROOT, TRASH, 'hrefArray']); - var root = find([ROOT]); - var toClean = []; - for (var id in fd) { - id = Number(id); - var el = fd[id]; - if (!el || typeof(el) !== "object") { - debug("An element in filesData was not an object.", el); - toClean.push(id); - continue; - } - if (!el.href) { - debug("Removing an element in filesData with a missing href.", el); - toClean.push(id); - continue; - } - if (/^https*:\/\//.test(el.href)) { el.href = Hash.getRelativeHref(el.href); } - if (!el.ctime) { el.ctime = el.atime; } - - var parsed = Hash.parsePadUrl(el.href); - if (!el.title) { el.title = Hash.getDefaultName(parsed); } - if (!parsed.hash) { - debug("Removing an element in filesData with a invalid href.", el); - toClean.push(id); - continue; - } - if (!parsed.type) { - debug("Removing an element in filesData with a invalid type.", el); - toClean.push(id); - continue; - } - - if ((loggedIn || config.testMode) && rootFiles.indexOf(id) === -1) { - debug("An element in filesData was not in ROOT, TEMPLATE or TRASH.", id, el); - var newName = Hash.createChannelId(); - root[newName] = id; - continue; - } - } - toClean.forEach(function (id) { - spliceFileData(id); - }); - }; - - var fixDrive = function () { - Object.keys(files).forEach(function (key) { - if (key.slice(0,1) === '/') { delete files[key]; } - }); - }; - - fixRoot(); - fixTrashRoot(); - if (!workgroup) { - fixTemplate(); - fixFilesData(); - } - fixDrive(); - - if (JSON.stringify(files) !== before) { - debug("Your file system was corrupted. It has been cleaned so that the pads you visit can be stored safely"); - return; - } - debug("File system was clean"); - }; - return exp; }; return module; diff --git a/www/drive/inner.js b/www/drive/inner.js index cd25fb99d..15a3299ea 100644 --- a/www/drive/inner.js +++ b/www/drive/inner.js @@ -185,11 +185,25 @@ define([ isHistoryMode: false, }; + var copyObjectValue = function (objRef, objToCopy) { + for (var k in objRef) { delete objRef[k]; } + $.extend(true, objRef, objToCopy); + }; + var updateObject = function (sframeChan, obj, cb) { + sframeChan.query('Q_DRIVE_GETOBJECT', null, function (err, newObj) { + copyObjectValue(obj, newObj); + cb(); + }); + }; + var andThen = function (common, proxy) { var files = proxy.drive; var metadataMgr = common.getMetadataMgr(); + var sframeChan = common.getSframeChannel(); var priv = metadataMgr.getPrivateData(); var user = metadataMgr.getUserData(); + + APP.origin = priv.origin; var isOwnDrive = function () { return true; // TODO }; @@ -198,12 +212,10 @@ define([ }; config.workgroup = isWorkgroup(); config.loggedIn = APP.loggedIn; + config.sframeChan = sframeChan; - APP.origin = priv.origin; var filesOp = FO.init(files, config); - filesOp.fixFiles(); - var error = filesOp.error; var $tree = APP.$tree = $("#cp-app-drive-tree"); @@ -2056,7 +2068,7 @@ define([ // Display the selected directory into the content part (rightside) // NOTE: Elements in the trash are not using the same storage structure as the others // _WORKGROUP_ : do not change the lastOpenedFolder value in localStorage - var displayDirectory = APP.displayDirectory = function (path, force) { + var _displayDirectory = function (path, force) { APP.hideMenu(); if (!APP.editable) { debug("Read-only mode"); } if (!appStatus.isReady && !force) { return; } @@ -2065,7 +2077,7 @@ define([ if (!path || displayedCategories.indexOf(path[0]) === -1) { log(Messages.categoryError); currentPath = [ROOT]; - displayDirectory(currentPath); + _displayDirectory(currentPath); return; } appStatus.ready(false); @@ -2092,7 +2104,7 @@ define([ debug("Unable to locate the selected directory: ", path); var parentPath = path.slice(); parentPath.pop(); - displayDirectory(parentPath, true); + _displayDirectory(parentPath, true); return; } if (!isSearch) { delete APP.Search.oldLocation; } @@ -2220,6 +2232,12 @@ define([ $content.scrollTop(s); appStatus.ready(true); }; + var displayDirectory = APP.displayDirectory = function (path, force) { + updateObject(sframeChan, proxy, function () { + copyObjectValue(files, proxy.drive); + _displayDirectory(path, force); + }); + }; var createTreeElement = function (name, $icon, path, draggable, droppable, collapsable, active) { var $name = $('', { 'class': 'cp-app-drive-element' }).text(name); @@ -2851,7 +2869,7 @@ define([ onRefresh.to = window.setTimeout(refresh, 500); } }; - proxy.on('change', [], function () { + /*proxy.on('change', [], function () { if (history.isHistoryMode) { return; } var path = arguments[2]; if (path[0] !== 'drive') { return false; } @@ -2885,7 +2903,7 @@ define([ if (path[1] === "migrate" && value === 1) { if (APP.onDisconnect) { APP.onDisconnect(true); } } - }); + });*/ history.onEnterHistory = function (obj) { var files = obj.drive; @@ -2922,7 +2940,7 @@ define([ var main = function () { var common; - var proxy; + var proxy = {}; var readOnly; nThen(function (waitFor) { @@ -2942,11 +2960,11 @@ define([ } metadataMgr.onChange(function () { if (typeof(metadataMgr.getPrivateData().readOnly) === 'boolean') { - readOnly = metadataMgr.getPrivateData().readOnly; + readOnly = APP.readOnly = metadataMgr.getPrivateData().readOnly; privReady(); } }); - }).nThen(function (/* waitFor */) { + }).nThen(function (waitFor) { APP.loggedIn = common.isLoggedIn(); APP.SFCommon = common; if (!APP.loggedIn) { Feedback.send('ANONYMOUS_DRIVE'); } @@ -2955,85 +2973,79 @@ define([ common.setTabTitle(Messages.type.drive); - var listmapConfig = { + /*var listmapConfig = { data: {}, common: common, logging: false + };*/ + + var sframeChan = common.getSframeChannel(); + updateObject(sframeChan, proxy, waitFor()); + }).nThen(function () { + var sframeChan = common.getSframeChannel(); + var metadataMgr = common.getMetadataMgr(); + var configTb = { + displayed: ['useradmin', 'pageTitle', 'newpad', 'limit'], + pageTitle: Messages.type.drive, + metadataMgr: metadataMgr, + readOnly: readOnly, + sfCommon: common, + $container: APP.$bar }; + var toolbar = APP.toolbar = Toolbar.create(configTb); - var metadataMgr; - var rt = APP.rt = Listmap.create(listmapConfig); - proxy = rt.proxy; - var onCreate = function (info) { - APP.realtime = info.realtime; - - metadataMgr = common.getMetadataMgr(); - - var configTb = { - displayed: ['useradmin', 'pageTitle', 'newpad', 'limit'], - pageTitle: Messages.type.drive, - metadataMgr: metadataMgr, - readOnly: readOnly, - realtime: info.realtime, - sfCommon: common, - $container: APP.$bar - }; - var toolbar = APP.toolbar = Toolbar.create(configTb); - - var $rightside = toolbar.$rightside; - $rightside.html(''); // Remove the drawer if we don't use it to hide the toolbar - APP.$displayName = APP.$bar.find('.' + Toolbar.constants.username); - - /* add the usage */ - if (APP.loggedIn) { - common.createUsageBar(function (err, $limitContainer) { - if (err) { return void logError(err); } - APP.$limit = $limitContainer; - }, true); - } + var $rightside = toolbar.$rightside; + $rightside.html(''); // Remove the drawer if we don't use it to hide the toolbar + APP.$displayName = APP.$bar.find('.' + Toolbar.constants.username); - /* add a history button */ - APP.histConfig = { - onLocal: function () { - proxy.drive = history.currentObj.drive; - }, - onRemote: function () {}, - setHistory: setHistory, - applyVal: function (val) { - var obj = JSON.parse(val || '{}'); - history.currentObj = obj; - history.onEnterHistory(obj); - }, - $toolbar: APP.$bar, - }; + /* add the usage */ + if (APP.loggedIn) { + common.createUsageBar(function (err, $limitContainer) { + if (err) { return void logError(err); } + APP.$limit = $limitContainer; + }, true); + } + + /* add a history button */ + APP.histConfig = { + onLocal: function () { + proxy.drive = history.currentObj.drive; + }, + onRemote: function () {}, + setHistory: setHistory, + applyVal: function (val) { + var obj = JSON.parse(val || '{}'); + history.currentObj = obj; + history.onEnterHistory(obj); + }, + $toolbar: APP.$bar, + }; - // Add a "Burn this drive" button - if (!APP.loggedIn) { - APP.$burnThisDrive = common.createButton(null, true).click(function () { - UI.confirm(Messages.fm_burnThisDrive, function (yes) { - if (!yes) { return; } - common.getSframeChannel().event('EV_BURN_ANON_DRIVE'); - }, null, true); - }).attr('title', Messages.fm_burnThisDriveButton) - .removeClass('fa-question') - .addClass('fa-ban'); - } + // Add a "Burn this drive" button + if (!APP.loggedIn) { + APP.$burnThisDrive = common.createButton(null, true).click(function () { + UI.confirm(Messages.fm_burnThisDrive, function (yes) { + if (!yes) { return; } + common.getSframeChannel().event('EV_BURN_ANON_DRIVE'); + }, null, true); + }).attr('title', Messages.fm_burnThisDriveButton) + .removeClass('fa-question') + .addClass('fa-ban'); + } + + metadataMgr.onChange(function () { + var name = metadataMgr.getUserData().name || Messages.anonymous; + APP.$displayName.text(name); + }); + + $('body').css('display', ''); + APP.files = proxy; + if (!proxy.drive || typeof(proxy.drive) !== 'object') { + throw new Error("Corrupted drive"); + } + andThen(common, proxy); + UI.removeLoadingScreen(); - metadataMgr.onChange(function () { - var name = metadataMgr.getUserData().name || Messages.anonymous; - APP.$displayName.text(name); - }); - }; - var firstConnection = true; - var onReady = function () { - if (!firstConnection) { return; } // TODO fix this issue in listmap - firstConnection = false; - $('body').css('display', ''); - APP.files = proxy; - if (!proxy.drive || typeof(proxy.drive) !== 'object') { proxy.drive = {}; } - andThen(common, proxy); - UI.removeLoadingScreen(); - }; var onDisconnect = APP.onDisconnect = function (noAlert) { setEditable(false); if (APP.refresh) { APP.refresh(); } @@ -3047,16 +3059,15 @@ define([ UI.findOKButton().click(); }; - proxy.on('create', function (info) { - onCreate(info); - }).on('ready', function () { - onReady(); + sframeChan.on('EV_DRIVE_LOG', function (msg) { + UI.log(msg); }); - proxy.on('disconnect', function () { + sframeChan.on('EV_NETWORK_DISCONNECT', function () { onDisconnect(); }); - proxy.on('reconnect', function (info) { - onReconnect(info); + sframeChan.on('EV_NETWORK_RECONNECT', function (data) { + // data.myId; + onReconnect(data); }); common.onLogout(function () { setEditable(false); }); }); diff --git a/www/drive/main.js b/www/drive/main.js index de668fc4d..f8799d9eb 100644 --- a/www/drive/main.js +++ b/www/drive/main.js @@ -48,6 +48,24 @@ define([ Utils.LocalStore.clearThumbnail(); window.location.reload(); }); + sframeChan.on('Q_DRIVE_USEROBJECT', function (data, cb) { + Cryptpad.userObjectCommand(data, cb); + }); + sframeChan.on('Q_DRIVE_GETOBJECT', function (data, cb) { + Cryptpad.getUserObject(function (obj) { + cb(obj); + }); + }); + Cryptpad.onNetworkDisconnect.reg(function () { + sframeChan.event('EV_NETWORK_DISCONNECT'); + }); + Cryptpad.onNetworkReconnect.reg(function (data) { + sframeChan.event('EV_NETWORK_RECONNECT', data); + }); + Cryptpad.onDriveLog.reg(function (msg) { + sframeChan.event('EV_DRIVE_LOG', msg); + }); + // History? }; //Netflux.connect(NetConfig.getWebsocketURL()).then(function (network) { SFCommonO.start({