define([ '/common/userObject.js', '/common/common-util.js', '/common/common-hash.js', '/common/outer/sharedfolder.js', '/customize/messages.js', '/common/common-feedback.js', '/bower_components/nthen/index.js', ], function (UserObject, Util, Hash, SF, Messages, Feedback, nThen) { var getConfig = function (Env) { var cfg = {}; for (var k in Env.cfg) { cfg[k] = Env.cfg[k]; } return cfg; }; // Add a shared folder to the list var addProxy = function (Env, id, lm, leave, editKey, force) { if (Env.folders[id] && !force) { // Shared folder already added to the proxy-manager, probably // a cached version if (Env.folders[id].offline && !lm.cache) { Env.folders[id].offline = false; Env.Store.refreshDriveUI(); } return; } var cfg = getConfig(Env); cfg.sharedFolder = true; cfg.id = id; cfg.editKey = editKey; cfg.rt = lm.realtime; cfg.readOnly = Boolean(!editKey); var userObject = UserObject.init(lm.proxy, cfg); if (userObject.fixFiles) { // Only in outer userObject.fixFiles(); } var proxy = lm.proxy; if (proxy.metadata && proxy.metadata.title) { var sf = Env.user.proxy[UserObject.SHARED_FOLDERS][id]; if (sf) { sf.lastTitle = proxy.metadata.title; } } Env.folders[id] = { proxy: lm.proxy, userObject: userObject, leave: leave, offline: Boolean(lm.cache) }; if (proxy.on) { proxy.on('disconnect', function () { Env.folders[id].offline = true; }); proxy.on('reconnect', function () { Env.folders[id].offline = false; }); } return userObject; }; var removeProxy = function (Env, id) { var f = Env.folders[id]; if (!f) { return; } f.leave(); delete Env.folders[id]; }; // Password may have changed var deprecateProxy = function (Env, id, channel) { if (Env.folders[id] && Env.folders[id].deleting) { // Folder is being deleted by its owner, don't deprecate it return; } if (Env.user.userObject.readOnly) { // In a read-only team, we can't deprecate a shared folder // Use a empty object with a deprecated flag... var lm = { proxy: { deprecated: true } }; removeProxy(Env, id); addProxy(Env, id, lm, function () {}); return void Env.Store.refreshDriveUI(); } if (channel) { Env.unpinPads([channel], function () {}); } Env.user.userObject.deprecateSharedFolder(id); removeProxy(Env, id); if (Env.Store && Env.Store.refreshDriveUI) { Env.Store.refreshDriveUI(); } }; var restrictedProxy = function (Env, id) { var lm = { proxy: { restricted: true, root: {}, filesData: {} } }; removeProxy(Env, id); addProxy(Env, id, lm, function () {}); return void Env.Store.refreshDriveUI(); }; /* Tools */ var _ownedByMe = function (Env, owners) { return Array.isArray(owners) && owners.indexOf(Env.edPublic) !== -1; }; var _ownedByOther = function (Env, owners) { return Array.isArray(owners) && owners.length && (!Env.edPublic || owners.indexOf(Env.edPublic) === -1); }; var _getUserObjects = function (Env) { var userObjects = [Env.user.userObject]; var foldersUO = Object.keys(Env.folders).map(function (k) { return Env.folders[k].userObject; }); Array.prototype.push.apply(userObjects, foldersUO); return userObjects; }; var _getUserObjectFromId = function (Env, id) { var userObjects = _getUserObjects(Env); var userObject = Env.user.userObject; userObjects.some(function (uo) { if (Object.keys(uo.getFileData(id)).length) { userObject = uo; return true; } }); return userObject; }; var _getUserObjectPath = function (Env, uo) { var fId = Number(uo.id); if (!fId) { return; } var fPath = Env.user.userObject.findFile(fId)[0]; return fPath; }; // Return files data objects associated to a channel for setPadTitle // All occurences are returned, in drive or shared folders // If "editable" is true, the data returned is a proxy, otherwise // it's a cloned object (NOTE: href should never be edited directly) var findChannel = function (Env, channel, editable) { var ret = []; Env.user.userObject.findChannels([channel], true).forEach(function (id) { // Check in shared folders, then clone if needed var data = Env.user.proxy[UserObject.SHARED_FOLDERS][id]; if (data && !editable) { data = JSON.parse(JSON.stringify(data)); } // If it's not a shared folder, check the pads if (!data) { data = Env.user.userObject.getFileData(id, editable); } ret.push({ id: id, data: data, userObject: Env.user.userObject }); }); Object.keys(Env.folders).forEach(function (fId) { Env.folders[fId].userObject.findChannels([channel]).forEach(function (id) { ret.push({ id: id, fId: fId, data: Env.folders[fId].userObject.getFileData(id, editable), userObject: Env.folders[fId].userObject }); }); }); return ret; }; // Return files data objects associated to a given href for setPadAttribute... // If "editable" is true, the data returned is a proxy, otherwise // it's a cloned object (NOTE: href should never be edited directly) var findHref = function (Env, href) { var ret = []; var id = Env.user.userObject.getIdFromHref(href); if (id) { ret.push({ data: Env.user.userObject.getFileData(id), userObject: Env.user.userObject }); } Object.keys(Env.folders).forEach(function (fId) { var id = Env.folders[fId].userObject.getIdFromHref(href); if (!id) { return; } ret.push({ fId: fId, data: Env.folders[fId].userObject.getFileData(id), userObject: Env.folders[fId].userObject }); }); return ret; }; // Return paths linked to a file ID var findFile = function (Env, id) { var ret = []; var userObjects = _getUserObjects(Env); userObjects.forEach(function (uo) { var fPath = _getUserObjectPath(Env, uo); var results = uo.findFile(id); if (fPath) { // This is a shared folder, we have to fix the paths in the results results.forEach(function (p) { Array.prototype.unshift.apply(p, fPath); }); } // Push the results from this proxy Array.prototype.push.apply(ret, results); }); return ret; }; // Returns file IDs corresponding to the provided channels var _findChannels = function (Env, channels, onlyMain) { if (onlyMain) { return Env.user.userObject.findChannels(channels); } var ret = []; var userObjects = _getUserObjects(Env); userObjects.forEach(function (uo) { var results = uo.findChannels(channels); Array.prototype.push.apply(ret, results); }); ret = Util.deduplicateString(ret); return ret; }; var _getFileData = function (Env, id, editable) { var userObjects = _getUserObjects(Env); var data = {}; userObjects.some(function (uo) { data = uo.getFileData(id, editable); if (data && Object.keys(data).length) { return true; } }); return data; }; var getSharedFolderData = function (Env, id) { if (!Env.folders[id]) { return {}; } var obj = Env.folders[id].proxy.metadata || {}; for (var k in Env.user.proxy[UserObject.SHARED_FOLDERS][id] || {}) { var data = Util.clone(Env.user.proxy[UserObject.SHARED_FOLDERS][id][k] || {}); if (k === "href" && data.indexOf('#') === -1) { try { data = Env.user.userObject.cryptor.decrypt(data); } catch (e) {} } if (k === "href" && data.indexOf('#') === -1) { data = undefined; } obj[k] = data; } return obj; }; // Transform an absolute path into a path relative to the correct shared folder var _resolvePath = function (Env, path) { var res = { id: null, userObject: Env.user.userObject, path: path }; if (!Array.isArray(path) || path.length <= 1) { return res; } var current; var uo = Env.user.userObject; // We don't need to check the last element of the path because we only need to split it // when the path contains an element inside the shared folder for (var i=2; i<path.length; i++) { current = uo.find(path.slice(0,i)); if (uo.isSharedFolder(current)) { res = { id: current, userObject: Env.folders[current].userObject, path: path.slice(i) }; break; } } return res; }; var _resolvePaths = function (Env, paths) { var main = []; var folders = {}; paths.forEach(function (path) { var r = _resolvePath(Env, path); if (r.id) { if (!folders[r.id]) { folders[r.id] = [r.path]; } else { folders[r.id].push(r.path); } } else { main.push(r.path); } }); return { main: main, folders: folders }; }; // Check if a given path is resolved to a shared folder or to the main drive var _isInSharedFolder = function (Env, path) { var resolved = _resolvePath(Env, path); return typeof resolved.id === "number" ? resolved.id : false; }; // Get the owned files in the main drive that are also duplicated in shared folders var _isDuplicateOwned = function (Env, path, id) { if (path && _isInSharedFolder(Env, path)) { return; } var data = _getFileData(Env, id || Env.user.userObject.find(path)); if (!data) { return; } if (!_ownedByMe(Env, data.owners)) { return; } var channel = data.channel; if (!channel) { return; } var foldersUO = Object.keys(Env.folders).map(function (k) { return Env.folders[k].userObject; }); return foldersUO.some(function (uo) { return uo.findChannels([channel]).length; }); }; // Get a copy of the elements located in the given paths, with their files data // Note: This function is only called to move files from a proxy to another var _getCopyFromPaths = function (Env, paths, userObject) { var data = []; var toNotRemove = []; paths.forEach(function (path, idx) { var el = userObject.find(path); var files = []; var key = path[path.length - 1]; // Get the files ID from the current path (file or folder) if (userObject.isFile(el)) { files.push(el); } else if (userObject.isSharedFolder(el)) { files.push(el); var obj = Env.folders[el].proxy.metadata || {}; if (obj) { key = obj.title; } } else { try { el = JSON.parse(JSON.stringify(el)); } catch (e) { return undefined; } userObject.getFilesRecursively(el, files); } // If the element is a folder and it contains a shared folder, abort! // We don't want nested shared folders! if (files.some(function (f) { return userObject.isSharedFolder(f); })) { if (Env.cfg && Env.cfg.log) { Env.cfg.log(Messages._getKey('fm_moveNestedSF', [key])); } toNotRemove.unshift(idx); return; } // Deduplicate files = Util.deduplicateString(files); // Get the files data associated to these files var filesData = {}; files.forEach(function (f) { filesData[f] = userObject.getFileData(f); }); data.push({ el: el, data: filesData, key: key }); }); // Remove from the "paths" array the elements that we don't want to move toNotRemove.forEach(function (idx) { paths.splice(idx, 1); }); return data; }; var getEditHash = function (Env, channel) { var res = findChannel(Env, channel); var stronger; res.some(function (obj) { if (!obj || !obj.data || !obj.data.href) { return; } var parsed = Hash.parsePadUrl(obj.data.href); var parsedHash = parsed.hashData; if (!parsedHash || parsedHash.mode === 'view') { return; } // We've found an edit hash! stronger = parsed.hash; return true; }); return stronger; }; /* Drive RPC */ // Move files or folders in the drive var _move = function (Env, data, cb) { var resolved = _resolvePaths(Env, data.paths); var newResolved = _resolvePath(Env, data.newPath); // NOTE: we can only copy when moving from one drive to another. We don't want // duplicates in the same drive var copy = data.copy; if (!newResolved.userObject.isFolder(newResolved.path)) { return void cb(); } nThen(function (waitFor) { if (resolved.main.length) { // Move from the main drive if (!newResolved.id) { // Move from the main drive to the main drive Env.user.userObject.move(resolved.main, newResolved.path, waitFor()); } else { // Move from the main drive to a shared folder // Copy the elements to the new location var toCopy = _getCopyFromPaths(Env, resolved.main, Env.user.userObject); var newUserObject = newResolved.userObject; toCopy.forEach(function (obj) { newUserObject.copyFromOtherDrive(newResolved.path, obj.el, obj.data, obj.key); }); if (copy) { return; } if (resolved.main.length) { // Remove the elements from the old location (without unpinning) Env.user.userObject.delete(resolved.main, waitFor()); // FIXME waitFor() is called synchronously } } } var folderIds = Object.keys(resolved.folders); if (folderIds.length) { // Move from a shared folder folderIds.forEach(function (fIdStr) { var fId = Number(fIdStr); var paths = resolved.folders[fId]; if (newResolved.id === fId) { // Move to the same shared folder newResolved.userObject.move(paths, newResolved.path, waitFor()); } else { // Move to a different shared folder or to main drive var uoFrom = Env.folders[fId].userObject; var uoTo = newResolved.userObject; // Copy the elements to the new location var toCopy = _getCopyFromPaths(Env, paths, uoFrom); toCopy.forEach(function (obj) { uoTo.copyFromOtherDrive(newResolved.path, obj.el, obj.data, obj.key); }); if (copy) { return; } // Remove the elements from the old location (without unpinning) uoFrom.delete(paths, waitFor()); // FIXME waitFor() is called synchronously } }); } }).nThen(function () { cb(); }); }; // Restore from the trash (main drive only) var _restore = function (Env, data, cb) { var userObject = Env.user.userObject; data = data || {}; userObject.restore(data.path, cb); }; // Add a folder/subfolder var _addFolder = function (Env, data, cb) { data = data || {}; var resolved = _resolvePath(Env, data.path); if (!resolved || !resolved.userObject) { return void cb({error: 'E_NOTFOUND'}); } resolved.userObject.addFolder(resolved.path, data.name, function (obj) { // The result is the relative path of the new folder. We have to make it absolute. if (obj.newPath && resolved.id) { var fPath = _getUserObjectPath(Env, resolved.userObject); if (fPath) { // This is a shared folder, we have to fix the paths in the search results Array.prototype.unshift.apply(obj.newPath, fPath); } } cb(obj); }); }; // Add a shared folder var _addSharedFolder = function (Env, data, cb) { data = data || {}; var resolved = _resolvePath(Env, data.path); if (!resolved || !resolved.userObject) { return void cb({error: 'E_NOTFOUND'}); } if (resolved.id) { return void cb({error: 'EINVAL'}); } if (!Env.pinPads) { return void cb({error: 'EAUTH'}); } var folderData = data.folderData || {}; var id; nThen(function () { // Check if it is an imported folder or a folder creation if (data.folderData) { return; } // Folder creation var hash = Hash.createRandomHash('drive', data.password); var secret = Hash.getSecrets('drive', hash, data.password); var hashes = Hash.getHashes(secret); folderData = { href: '/drive/#' + hashes.editHash, roHref: '/drive/#' + hashes.viewHash, channel: secret.channel, ctime: +new Date(), }; if (data.password) { folderData.password = data.password; } if (data.owned) { folderData.owners = [Env.edPublic]; } }).nThen(function (waitFor) { Env.Store.getPadMetadata(null, { channel: folderData.channel }, waitFor(function (obj) { if (obj && (obj.error || obj.rejected)) { waitFor.abort(); return void cb({ error: obj.error || 'ERESTRICTED' }); } })); }).nThen(function (waitFor) { Env.pinPads([folderData.channel], waitFor()); }).nThen(function (waitFor) { // 1. add the shared folder to our list of shared folders // NOTE: pushSharedFolder will encrypt the href directly in the object if needed Env.user.userObject.pushSharedFolder(folderData, waitFor(function (err, folderId) { if (err === "EEXISTS" && folderData.href && folderId) { var parsed = Hash.parsePadUrl(folderData.href); var secret = Hash.getSecrets('drive', parsed.hash, folderData.password); SF.upgrade(secret.channel, secret); Env.folders[folderId].userObject.setReadOnly(false, secret.keys.secondaryKey); } if (err) { waitFor.abort(); return void cb(err); } id = folderId; })); }).nThen(function (waitFor) { // 2a. add the shared folder to the path in our drive Env.user.userObject.add(id, resolved.path); // 2b. load the proxy Env.loadSharedFolder(id, folderData, waitFor(function (rt) { if (!rt) { waitFor.abort(); return void cb({ error: 'EDELETED' }); } if (!rt.proxy.metadata) { // Creating a new shared folder rt.proxy.metadata = { title: data.name || Messages.fm_newFolder }; } if (data.folderData) { // If we're importing a folder, check its serverside metadata Env.Store.getPadMetadata(null, { channel: folderData.channel }, function (md) { var fData = Env.user.proxy[UserObject.SHARED_FOLDERS][id]; if (md.owners) { fData.owners = md.owners; } if (md.expire) { fData.expire = +md.expire; } }); } }), !Boolean(data.folderData)); }).nThen(function () { Env.onSync(function () { cb(id); }); }); }; var _restoreSharedFolder = function (Env, _data, cb) { var fId = _data.id; var newPassword = _data.password; var temp = Util.find(Env, ['user', 'proxy', UserObject.SHARED_FOLDERS_TEMP]); var data = temp && temp[fId]; if (!data) { return void cb({ error: 'EINVAL' }); } if (!Env.Store) { return void cb({ error: 'ESTORE' }); } var href = Env.user.userObject.getHref ? Env.user.userObject.getHref(data) : data.href; var isNew = false; nThen(function (waitFor) { Env.Store.isNewChannel(null, { href: href, password: newPassword }, waitFor(function (obj) { if (!obj || obj.error) { isNew = false; return; } isNew = obj.isNew; })); }).nThen(function () { if (isNew) { return void cb({ error: 'ENOTFOUND' }); } var parsed = Hash.parsePadUrl(href); var secret = Hash.getSecrets(parsed.type, parsed.hash, newPassword); data.password = newPassword; data.channel = secret.channel; if (secret.keys.editKeyStr) { data.href = '/drive/#'+Hash.getEditHashFromKeys(secret); } data.roHref = '/drive/#'+Hash.getViewHashFromKeys(secret); _addSharedFolder(Env, { path: ['root'], folderData: data, }, function () { delete temp[fId]; Env.onSync(cb); }); }); }; // convert a folder to a Shared Folder var _convertFolderToSharedFolder = function (Env, data, cb) { var path = data.path; var folderElement = Env.user.userObject.find(path); // don't try to convert top-level elements (trash, root, etc) to shared-folders if (path.length <= 1 || path[0] !== UserObject.ROOT) { return void cb({ error: 'E_INVAL_PATH', }); } if (_isInSharedFolder(Env, path)) { return void cb({ error: 'E_INVAL_NESTING', }); } if (Env.user.userObject.hasSubSharedFolder(folderElement)) { return void cb({ error: 'E_INVAL_NESTING', }); } var parentPath = path.slice(0, -1); var parentFolder = Env.user.userObject.find(parentPath); var folderName = path[path.length - 1]; var SFId; nThen(function (waitFor) { // create shared folder _addSharedFolder(Env, { path: parentPath, name: folderName, owned: data.owned, password: data.password || '', }, waitFor(function (id) { // _addSharedFolder can be an id or an error if (typeof(id) === 'object' && id && id.error) { waitFor.abort(); return void cb(id); } else { SFId = id; } })); }).nThen(function (waitFor) { // move everything from folder to SF if (!SFId) { waitFor.abort(); return void cb({ error: 'E_NO_ID' }); } var paths = []; for (var el in folderElement) { if (Env.user.userObject.isFolder(folderElement[el]) || Env.user.userObject.isFile(folderElement[el])) { paths.push(path.concat(el)); } } var SFKey; // this is basically Array.find, except it works in IE Object.keys(parentFolder).some(function (el) { if (parentFolder[el] === SFId) { SFKey = el; return true; } }); if (!SFKey) { waitFor.abort(); return void cb({ error: 'E_NO_KEY' }); } var newPath = parentPath.concat(SFKey).concat(UserObject.ROOT); _move(Env, { paths: paths, newPath: newPath, copy: false, }, waitFor()); }).nThen(function (waitFor) { // Move the owned pads from the old folder to root var paths = []; Object.keys(folderElement).forEach(function (el) { if (!Env.user.userObject.isFile(folderElement[el])) { return; } var data = Env.user.userObject.getFileData(folderElement[el]); if (!data || !_ownedByMe(Env, data.owners)) { return; } // This is an owned pad: move it to ROOT before deleting the initial folder paths.push(path.concat(el)); }); _move(Env, { paths: paths, newPath: [UserObject.ROOT], copy: false, }, waitFor()); }).nThen(function () { // migrate metadata var sharedFolderElement = Env.user.proxy[UserObject.SHARED_FOLDERS][SFId]; var metadata = Env.user.userObject.getFolderData(folderElement); for (var key in metadata) { // it shouldn't be possible to have nested metadata // but this is a reasonable sanity check if (key === "metadata") { continue; } // copy the metadata from the original folder to the new shared folder sharedFolderElement[key] = metadata[key]; } // remove folder Env.user.userObject.delete([path], function () { cb({ fId: SFId }); }); }); }; var _delete = function (Env, data, cb) { data = data || {}; var resolved = data.resolved || _resolvePaths(Env, data.paths); if (!resolved.main.length && !Object.keys(resolved.folders).length) { return void cb({error: 'E_NOTFOUND'}); } // Deleted or password changed for a shared folder if (data.paths && data.paths.length === 1 && data.paths[0][0] === UserObject.SHARED_FOLDERS_TEMP) { var temp = Util.find(Env, ['user', 'proxy', UserObject.SHARED_FOLDERS_TEMP]); delete temp[data.paths[0][1]]; return void Env.onSync(cb); } var toUnpin = []; nThen(function (waitFor) { // Delete paths from the main drive and get the list of pads to unpin // We also get the list of owned pads that were removed if (resolved.main.length) { var uo = Env.user.userObject; if (Util.find(Env.settings, ['drive', 'hideDuplicate'])) { // If we hide duplicate owned pads in our drive, we have // to make sure we're not deleting a hidden own file // from inside a folder we're trying to delete resolved.main.forEach(function (p) { var el = uo.find(p); if (p[0] === UserObject.FILES_DATA) { return; } if (uo.isFile(el) || uo.isSharedFolder(el)) { return; } var arr = []; uo.getFilesRecursively(el, arr); arr.forEach(function (id) { if (_isDuplicateOwned(Env, null, id)) { Env.user.userObject.add(Number(id), [UserObject.ROOT]); } }); }); } uo.delete(resolved.main, waitFor(function (err, _toUnpin/*, _ownedRemoved*/) { //ownedRemoved = _ownedRemoved; if (!Env.unpinPads || !_toUnpin) { return; } Array.prototype.push.apply(toUnpin, _toUnpin); })); } }).nThen(function (waitFor) { // Check if removed owned pads are duplicated in some shared folders // If that's the case, we have to remove them from the shared folders too // We can do that by adding their paths to the list of pads to remove from shared folders /*if (ownedRemoved) { var ids = _findChannels(Env, ownedRemoved); ids.forEach(function (id) { var paths = findFile(Env, id); var _resolved = _resolvePaths(Env, paths); Object.keys(_resolved.folders).forEach(function (fId) { if (resolved.folders[fId]) { Array.prototype.push.apply(resolved.folders[fId], _resolved.folders[fId]); } else { resolved.folders[fId] = _resolved.folders[fId]; } }); }); }*/ // Delete paths from the shared folders Object.keys(resolved.folders).forEach(function (id) { Env.folders[id].userObject.delete(resolved.folders[id], waitFor(function (err, _toUnpin) { if (!Env.unpinPads || !_toUnpin) { return; } Array.prototype.push.apply(toUnpin, _toUnpin); })); }); }).nThen(function (waitFor) { if (!Env.unpinPads) { return; } // Deleted channels toUnpin = Util.deduplicateString(toUnpin); // Deleted channels that are still in another proxy var toKeep = []; _findChannels(Env, toUnpin).forEach(function (id) { var data = _getFileData(Env, id); var arr = [data.channel]; if (data.rtChannel) { arr.push(data.rtChannel); } if (data.lastVersion) { arr.push(Hash.hrefToHexChannelId(data.lastVersion)); } Array.prototype.push.apply(toKeep, arr); }); // Compute the unpin list and unpin var unpinList = []; toUnpin.forEach(function (chan) { if (toKeep.indexOf(chan) === -1) { unpinList.push(chan); // Check if need need to restore a full hash (hidden hash deleted from drive) Env.Store.checkDeletedPad(chan); } }); Env.unpinPads(unpinList, waitFor(function (response) { if (response && response.error) { return console.error(response.error); } })); }).nThen(function () { cb(); }); }; // Delete permanently some pads or folders var _deleteOwned = function (Env, data, cb) { data = data || {}; var resolved = _resolvePaths(Env, data.paths || []); if (!data.channel && !resolved.main.length && !Object.keys(resolved.folders).length) { return void cb({error: 'E_NOTFOUND'}); } var toDelete = { main: [], folders: {} }; var todo = function (channel, uo, p, _cb) { var cb = Util.once(Util.mkAsync(_cb)); var chan = channel; if (!chan && uo) { var el = uo.find(p); if (!uo.isFile(el) && !uo.isSharedFolder(el)) { return; } var data = uo.isFile(el) ? uo.getFileData(el) : getSharedFolderData(Env, el); chan = data.channel; } // If the pad was a shared folder, delete it too and leave it var fId; Object.keys(Env.user.proxy[UserObject.SHARED_FOLDERS] || {}).some(function (id) { var sfData = Env.user.proxy[UserObject.SHARED_FOLDERS][id] || {}; if (sfData.channel === chan) { fId = Number(id); Env.folders[id].deleting = true; return true; } }); Env.removeOwnedChannel(chan, function (obj) { // If the error is that the file is already removed, nothing to // report, it's a normal behavior (pad expired probably) if (obj && obj.error && obj.error !== "ENOENT") { // RPC may not be responding // Send a report that can be handled manually if (fId && Env.folders[fId] && Env.folders[fId].deleting) { delete Env.folders[fId].deleting; } console.error(obj.error, chan); Feedback.send('ERROR_DELETING_OWNED_PAD=' + chan + '|' + obj.error, true); return void cb(); } // No error: delete the pad and all its copies from our drive and shared folders var ids = _findChannels(Env, [chan]); // If the pad was a shared folder, delete it too and leave it if (fId) { ids.push(fId); } ids.forEach(function (id) { var paths = findFile(Env, id); var _resolved = _resolvePaths(Env, paths); Array.prototype.push.apply(toDelete.main, _resolved.main); Object.keys(_resolved.folders).forEach(function (fId) { if (toDelete.folders[fId]) { Array.prototype.push.apply(toDelete.folders[fId], _resolved.folders[fId]); } else { toDelete.folders[fId] = _resolved.folders[fId]; } }); }); cb(); }); }; nThen(function (w) { // Delete owned pads from the server if (data.channel) { todo(data.channel, null, null, w()); } resolved.main.forEach(function (p) { todo(null, Env.user.userObject, p, w()); }); Object.keys(resolved.folders).forEach(function (id) { var uo = Env.folders[id].userObject; resolved.folders[id].forEach(function (p) { todo(null, uo, p, w()); }); }); }).nThen(function () { // Remove deleted pads from the drive _delete(Env, { resolved: toDelete }, cb); // If we were using the access modal, send a refresh command if (data.channel) { Env.Store.refreshDriveUI(); } }); }; // Empty the trash (main drive only) var _emptyTrash = function (Env, data, cb) { nThen(function (waitFor) { if (data && data.deleteOwned) { // Delete owned pads in the trash from the server var owned = Env.user.userObject.ownedInTrash(function (owners) { return _ownedByMe(Env, owners); }); owned.forEach(function (chan) { Env.removeOwnedChannel(chan, waitFor(function (obj) { // If the error is that the file is already removed, nothing to // report, it's a normal behavior (pad expired probably) if (obj && obj.error && obj.error !== "ENOENT") { // RPC may not be responding // Send a report that can be handled manually console.error(obj.error, chan); Feedback.send('ERROR_EMPTYTRASH_OWNED=' + chan + '|' + obj.error, true); } console.warn('DELETED', chan); })); }); } // Empty the trash Env.user.userObject.emptyTrash(waitFor(function (err, toClean) { cb(); // Don't block nThen for the lower-priority tasks setTimeout(function () { // Unpin deleted pads if needed // Check if we need to restore a full hash (hidden hash deleted from drive) if (!Array.isArray(toClean)) { return; } var toCheck = Util.deduplicateString(toClean); var toUnpin = []; toCheck.forEach(function (channel) { // Check unpin var data = findChannel(Env, channel, true); if (!data.length) { toUnpin.push(channel); } // Check hidden hash Env.Store.checkDeletedPad(channel); }); Env.unpinPads(toUnpin, function () {}); }); })); }).nThen(cb); }; // Rename files or folders var _rename = function (Env, data, cb) { data = data || {}; var resolved = _resolvePath(Env, data.path); if (!resolved || !resolved.userObject) { return void cb({error: 'E_NOTFOUND'}); } if (!resolved.id) { var el = Env.user.userObject.find(resolved.path); if (Env.user.userObject.isSharedFolder(el) && Env.folders[el]) { Env.folders[el].proxy.metadata.title = data.newName || Messages.fm_folder; Env.user.proxy[UserObject.SHARED_FOLDERS][el].lastTitle = data.newName || Messages.fm_folder; return void cb(); } } resolved.userObject.rename(resolved.path, data.newName, cb); }; var _setFolderData = function (Env, data, cb) { data = data || {}; var resolved = _resolvePath(Env, data.path); if (!resolved || !resolved.userObject) { return void cb({error: 'E_NOTFOUND'}); } if (!resolved.id) { var el = Env.user.userObject.find(resolved.path); if (Env.user.userObject.isSharedFolder(el) && Env.folders[el]) { Env.user.proxy[UserObject.SHARED_FOLDERS][el][data.key] = data.value; return void Env.onSync(cb); } } resolved.userObject.setFolderData(resolved.path, data.key, data.value, function () { Env.onSync(cb); }); }; var onCommand = function (Env, cmdData, cb) { var cmd = cmdData.cmd; var data = cmdData.data || {}; switch (cmd) { case 'move': _move(Env, data, cb); break; case 'restore': _restore(Env, data, cb); break; case 'addFolder': _addFolder(Env, data, cb); break; case 'addSharedFolder': _addSharedFolder(Env, data, cb); break; case 'restoreSharedFolder': _restoreSharedFolder(Env, data, cb); break; case 'convertFolderToSharedFolder': _convertFolderToSharedFolder(Env, data, cb); break; case 'delete': _delete(Env, data, cb); break; case 'deleteOwned': _deleteOwned(Env, data, cb); break; case 'emptyTrash': _emptyTrash(Env, data, cb); break; case 'rename': _rename(Env, data, cb); break; case 'setFolderData': _setFolderData(Env, data, cb); break; default: cb(); } }; // Set the value everywhere the given pad is stored (main and shared folders) var setPadAttribute = function (Env, data, cb) { cb = cb || function () {}; if (!data.attr || !data.attr.trim()) { return void cb("E_INVAL_ATTR"); } var sfId = Env.user.userObject.getSFIdFromHref(data.href); if (sfId) { if (data.attr === "href") { data.value = Env.user.userObject.cryptor.encrypt(data.value); } Env.user.proxy[UserObject.SHARED_FOLDERS][sfId][data.attr] = data.value; } var datas = findHref(Env, data.href); var nt = nThen; datas.forEach(function (d) { nt = nt(function (waitFor) { d.userObject.setPadAttribute(data.href, data.attr, data.value, waitFor()); }).nThen; }); nt(function () { cb(); }); }; // Get pad attribute must return only one value, even if the pad is stored in multiple places // (main or shared folders) // We're going to return the value with the most recent atime. The attributes may have been // updated in a shared folder by another user, so the most recent one is more likely to be the // correct one. // NOTE: we also return the atime, so that we can also check with each team manager var getPadAttribute = function (Env, data, cb) { cb = cb || function () {}; var sfId = Env.user.userObject.getSFIdFromHref(data.href); if (sfId) { var sfData = getSharedFolderData(Env, sfId); var sfValue = data.attr ? sfData[data.attr] : JSON.parse(JSON.stringify(sfData)); setTimeout(function () { cb(null, { value: sfValue, atime: 1 }); }); return; } var datas = findHref(Env, data.href); var res = {}; datas.forEach(function (d) { var atime = d.data.atime; var value = data.attr ? d.data[data.attr] : JSON.parse(JSON.stringify(d.data)); if (!res.value || res.atime < atime) { res.atime = atime; res.value = value; } }); setTimeout(function () { cb(null, res); }); }; var getTagsList = function (Env) { var list = {}; var userObjects = _getUserObjects(Env); userObjects.forEach(function (uo) { var l = uo.getTagsList(); Object.keys(l).forEach(function (t) { list[t] = list[t] ? (list[t] + l[t]) : l[t]; }); }); return list; }; var getSecureFilesList = function (Env, where) { var userObjects = _getUserObjects(Env); var list = []; var channels = []; userObjects.forEach(function (uo) { var toPush = uo.getFiles(where).map(function (id) { return { id: id, data: uo.getFileData(id) }; }).filter(function (d) { if (channels.indexOf(d.data.channel) === -1) { channels.push(d.data.channel); return true; } }); Array.prototype.push.apply(list, toPush); }); return list; }; /* Store */ // Get the list of channels filtered by a type (expirable channels, owned channels, pin list) var getChannelsList = function (Env, type) { var result = []; var addChannel = function (userObject) { if (type === 'expirable') { return function (fileId) { var data = userObject.getFileData(fileId); if (!data) { return; } // Don't push duplicates if (result.indexOf(data.channel) !== -1) { return; } // Return pads owned by someone else or expired by time if (_ownedByOther(Env, data.owners) || (data.expire && data.expire < (+new Date()))) { result.push(data.channel); } }; } if (type === 'owned') { return function (fileId) { var data = userObject.getFileData(fileId); if (!data) { return; } // Don't push duplicates if (result.indexOf(data.channel) !== -1) { return; } // Return owned pads if (_ownedByMe(Env, data.owners)) { result.push(data.channel); } }; } if (type === "pin") { return function (fileId) { var data = userObject.getFileData(fileId); if (!data) { return; } // Don't pin pads owned by someone else if (_ownedByOther(Env, data.owners)) { return; } // Pin onlyoffice checkpoints if (data.lastVersion) { var otherChan = Hash.hrefToHexChannelId(data.lastVersion); if (result.indexOf(otherChan) === -1) { result.push(otherChan); } } // Pin onlyoffice realtime patches if (data.rtChannel && result.indexOf(data.rtChannel) === -1) { result.push(data.rtChannel); } // Pin onlyoffice images if (data.ooImages && Array.isArray(data.ooImages)) { Array.prototype.push.apply(result, data.ooImages); } // Pin the pad if (result.indexOf(data.channel) === -1) { result.push(data.channel); } }; } }; if (type === 'owned' && !Env.edPublic) { return result; } if (type === 'pin' && !Env.edPublic) { return result; } // Get the list of user objects var userObjects = _getUserObjects(Env); userObjects.forEach(function (uo) { var files = uo.getFiles([UserObject.FILES_DATA]); files.forEach(addChannel(uo)); }); // NOTE: expirable shared folder should be added here if we ever decide to enable them if (type === "owned") { var sfOwned = Object.keys(Env.user.proxy[UserObject.SHARED_FOLDERS]).filter(function (fId) { var owners = Env.user.proxy[UserObject.SHARED_FOLDERS][fId].owners; if (_ownedByMe(Env, owners)) { return true; } }).map(function (fId) { return Env.user.proxy[UserObject.SHARED_FOLDERS][fId].channel; }); Array.prototype.push.apply(result, sfOwned); } if (type === "pin") { var sfChannels = Object.keys(Env.folders).map(function (fId) { return Env.user.proxy[UserObject.SHARED_FOLDERS][fId].channel; }); Array.prototype.push.apply(result, sfChannels); } return result; }; var addPad = function (Env, path, pad, cb) { var uo = Env.user.userObject; var p = ['root']; if (path) { var resolved = _resolvePath(Env, path); uo = resolved.userObject; p = resolved.path; } var todo = function () { var error; nThen(function (waitFor) { uo.pushData(pad, waitFor(function (e, id) { if (e) { error = e; return; } uo.add(id, p); })); }).nThen(function () { cb(error); }); }; if (!Env.pinPads) { return void todo(); } Env.pinPads([pad.channel], function (obj) { if (obj && obj.error) { return void cb(obj.error); } todo(); }); }; var create = function (proxy, data, uoConfig) { var Env = { pinPads: data.pin, unpinPads: data.unpin, onSync: data.onSync, Store: data.Store, removeOwnedChannel: data.removeOwnedChannel, loadSharedFolder: data.loadSharedFolder, cfg: uoConfig, edPublic: data.edPublic, settings: data.settings, user: { proxy: proxy, }, folders: {} }; uoConfig.removeProxy = function (id) { removeProxy(Env, id); }; Env.user.userObject = UserObject.init(proxy, uoConfig); var callWithEnv = function (f) { return function () { [].unshift.call(arguments, Env); return f.apply(null, arguments); }; }; var addPin = function (pin, unpin) { Env.pinPads = pin; Env.unpinPads = unpin; }; var removePin = function () { delete Env.pinPads; delete Env.unpinPads; }; return { // Manager addProxy: callWithEnv(addProxy), removeProxy: callWithEnv(removeProxy), deprecateProxy: callWithEnv(deprecateProxy), restrictedProxy: callWithEnv(restrictedProxy), addSharedFolder: callWithEnv(_addSharedFolder), addPin: addPin, removePin: removePin, // Drive command: callWithEnv(onCommand), getPadAttribute: callWithEnv(getPadAttribute), setPadAttribute: callWithEnv(setPadAttribute), getTagsList: callWithEnv(getTagsList), getSecureFilesList: callWithEnv(getSecureFilesList), getSharedFolderData: callWithEnv(getSharedFolderData), // Store getChannelsList: callWithEnv(getChannelsList), addPad: callWithEnv(addPad), delete: callWithEnv(_delete), deleteOwned: callWithEnv(_deleteOwned), // Tools findChannel: callWithEnv(findChannel), findHref: callWithEnv(findHref), findFile: callWithEnv(findFile), getEditHash: callWithEnv(getEditHash), user: Env.user, folders: Env.folders }; }; /* ============================================================================= * ============================================================================= * Inner only * ============================================================================= * ============================================================================= */ var renameInner = function (Env, path, newName, cb) { return void Env.sframeChan.query("Q_DRIVE_USEROBJECT", { cmd: "rename", data: { path: path, newName: newName } }, cb); }; var moveInner = function (Env, paths, newPath, cb, copy) { return void Env.sframeChan.query("Q_DRIVE_USEROBJECT", { cmd: "move", data: { paths: paths, newPath: newPath, copy: copy } }, cb); }; var emptyTrashInner = function (Env, deleteOwned, cb) { return void Env.sframeChan.query("Q_DRIVE_USEROBJECT", { cmd: "emptyTrash", data: { deleteOwned: deleteOwned } }, cb); }; var addFolderInner = function (Env, path, name, cb) { return void Env.sframeChan.query("Q_DRIVE_USEROBJECT", { cmd: "addFolder", data: { path: path, name: name } }, cb); }; var addSharedFolderInner = function (Env, path, data, cb) { return void Env.sframeChan.query("Q_DRIVE_USEROBJECT", { cmd: "addSharedFolder", data: { path: path, name: data.name, owned: data.owned, password: data.password } }, cb); }; var restoreSharedFolderInner = function (Env, fId, password, cb) { return void Env.sframeChan.query("Q_DRIVE_USEROBJECT", { cmd: "restoreSharedFolder", data: { id: fId, password: password } }, cb); }; var convertFolderToSharedFolderInner = function (Env, path, owned, password, cb) { return void Env.sframeChan.query("Q_DRIVE_USEROBJECT", { cmd: "convertFolderToSharedFolder", data: { path: path, owned: owned, password: password } }, cb); }; var deleteInner = function (Env, paths, cb) { return void Env.sframeChan.query("Q_DRIVE_USEROBJECT", { cmd: "delete", data: { paths: paths, } }, cb); }; var deleteOwnedInner = function (Env, paths, cb) { return void Env.sframeChan.query("Q_DRIVE_USEROBJECT", { cmd: "deleteOwned", data: { paths: paths, } }, cb); }; var restoreInner = function (Env, path, cb) { return void Env.sframeChan.query("Q_DRIVE_USEROBJECT", { cmd: "restore", data: { path: path } }, cb); }; var setFolderDataInner = function (Env, data, cb) { return void Env.sframeChan.query("Q_DRIVE_USEROBJECT", { cmd: "setFolderData", data: data }, cb); }; /* Tools */ var findChannels = _findChannels; var getFileData = _getFileData; var getUserObjectPath = _getUserObjectPath; var find = function (Env, path, fId) { if (fId) { return Env.folders[fId].userObject.find(path); } var resolved = _resolvePath(Env, path); return resolved.userObject.find(resolved.path); }; var getTitle = function (Env, id, type) { var uo = _getUserObjectFromId(Env, id); return String(uo.getTitle(id, type)); }; var isReadOnlyFile = function (Env, id) { var uo = _getUserObjectFromId(Env, id); return uo.isReadOnlyFile(id); }; var getFiles = function (Env, categories) { var files = []; var userObjects = _getUserObjects(Env); userObjects.forEach(function (uo) { Array.prototype.push.apply(files, uo.getFiles(categories)); }); files = Util.deduplicateString(files); return files; }; var search = function (Env, value) { var ret = []; var userObjects = _getUserObjects(Env); userObjects.forEach(function (uo) { var fPath = _getUserObjectPath(Env, uo); var results = uo.search(value); if (!results.length) { return; } if (fPath) { // This is a shared folder, we have to fix the paths in the search results results.forEach(function (r) { r.inSharedFolder = true; r.paths.forEach(function (p) { Array.prototype.unshift.apply(p, fPath); }); }); } // Push the results from this proxy Array.prototype.push.apply(ret, results); }); return ret; }; var getRecentPads = function (Env) { var files = []; var userObjects = _getUserObjects(Env); userObjects.forEach(function (uo) { var data = uo.getFiles([UserObject.FILES_DATA]).map(function (id) { return [Number(id), uo.getFileData(id)]; }); Array.prototype.push.apply(files, data); }); var sorted = files.filter(function (a) { return a[1].atime; }) .sort(function (a,b) { return b[1].atime - a[1].atime; }); return sorted; //return Env.user.userObject.getRecentPads(); }; var getOwnedPads = function (Env) { return Env.user.userObject.getOwnedPads(Env.edPublic); }; var getFolderData = function (Env, path) { var resolved = _resolvePath(Env, path); if (!resolved || !resolved.userObject) { return {}; } if (!resolved.id) { var el = Env.user.userObject.find(resolved.path); if (Env.user.userObject.isSharedFolder(el)) { return getSharedFolderData(Env, el); } } var folder = resolved.userObject.find(resolved.path); return resolved.userObject.getFolderData(folder); }; var isInSharedFolder = _isInSharedFolder; /* Generic: doesn't need access to a proxy */ var isValidDrive = function (Env, obj) { return Env.user.userObject.isValidDrive(obj); }; var isFile = function (Env, el, allowStr) { return Env.user.userObject.isFile(el, allowStr); }; var isFolder = function (Env, el) { return Env.user.userObject.isFolder(el); }; var isSharedFolder = function (Env, el) { return Env.user.userObject.isSharedFolder(el); }; var isFolderEmpty = function (Env, el) { if (Env.folders[el]) { var uo = Env.folders[el].userObject; return uo.isFolderEmpty(uo.find[uo.ROOT]); } return Env.user.userObject.isFolderEmpty(el); }; var isPathIn = function (Env, path, categories) { return Env.user.userObject.isPathIn(path, categories); }; var isSubpath = function (Env, path, parentPath) { return Env.user.userObject.isSubpath(path, parentPath); }; var isInTrashRoot = function (Env, path) { return Env.user.userObject.isInTrashRoot(path); }; var comparePath = function (Env, a, b) { return Env.user.userObject.comparePath(a, b); }; var hasSubfolder = function (Env, el, trashRoot) { if (Env.folders[el]) { var uo = Env.folders[el].userObject; return uo.hasSubfolder(uo.find[uo.ROOT]); } return Env.user.userObject.hasSubfolder(el, trashRoot); }; var hasSubSharedFolder = function (Env, el) { return Env.user.userObject.hasSubSharedFolder(el); }; var hasFile = function (Env, el, trashRoot) { if (Env.folders[el]) { var uo = Env.folders[el].userObject; return uo.hasFile(uo.find[uo.ROOT]); } return Env.user.userObject.hasFile(el, trashRoot); }; var ownedInTrash = function (Env) { return Env.user.userObject.ownedInTrash(function (owners) { return _ownedByMe(Env, owners); }); }; var isDuplicateOwned = _isDuplicateOwned; var createInner = function (proxy, sframeChan, edPublic, uoConfig) { var Env = { cfg: uoConfig, sframeChan: sframeChan, edPublic: edPublic, user: { proxy: proxy, userObject: UserObject.init(proxy, uoConfig) }, folders: {} }; var callWithEnv = function (f) { return function () { [].unshift.call(arguments, Env); return f.apply(null, arguments); }; }; return { // Manager addProxy: callWithEnv(addProxy), removeProxy: callWithEnv(removeProxy), // Drive RPC commands rename: callWithEnv(renameInner), move: callWithEnv(moveInner), emptyTrash: callWithEnv(emptyTrashInner), addFolder: callWithEnv(addFolderInner), addSharedFolder: callWithEnv(addSharedFolderInner), restoreSharedFolder: callWithEnv(restoreSharedFolderInner), convertFolderToSharedFolder: callWithEnv(convertFolderToSharedFolderInner), delete: callWithEnv(deleteInner), deleteOwned: callWithEnv(deleteOwnedInner), restore: callWithEnv(restoreInner), setFolderData: callWithEnv(setFolderDataInner), // Tools getFileData: callWithEnv(getFileData), find: callWithEnv(find), getTitle: callWithEnv(getTitle), isReadOnlyFile: callWithEnv(isReadOnlyFile), getFiles: callWithEnv(getFiles), search: callWithEnv(search), getRecentPads: callWithEnv(getRecentPads), getOwnedPads: callWithEnv(getOwnedPads), getTagsList: callWithEnv(getTagsList), findFile: callWithEnv(findFile), findChannels: callWithEnv(findChannels), getSharedFolderData: callWithEnv(getSharedFolderData), getFolderData: callWithEnv(getFolderData), isInSharedFolder: callWithEnv(isInSharedFolder), getUserObjectPath: callWithEnv(getUserObjectPath), isDuplicateOwned: callWithEnv(isDuplicateOwned), ownedInTrash: callWithEnv(ownedInTrash), // Generic isValidDrive: callWithEnv(isValidDrive), isFile: callWithEnv(isFile), isFolder: callWithEnv(isFolder), isSharedFolder: callWithEnv(isSharedFolder), isFolderEmpty: callWithEnv(isFolderEmpty), isPathIn: callWithEnv(isPathIn), isSubpath: callWithEnv(isSubpath), isInTrashRoot: callWithEnv(isInTrashRoot), comparePath: callWithEnv(comparePath), hasSubfolder: callWithEnv(hasSubfolder), hasSubSharedFolder: callWithEnv(hasSubSharedFolder), hasFile: callWithEnv(hasFile), // Data user: Env.user, folders: Env.folders }; }; return { create: create, createInner: createInner }; });