diff --git a/customize.dist/ckeditor-contents.css b/customize.dist/ckeditor-contents.css index 000162c00..663cd8cb9 100644 --- a/customize.dist/ckeditor-contents.css +++ b/customize.dist/ckeditor-contents.css @@ -213,3 +213,54 @@ media-tag * { width: 100%; height: 100%; } +media-tag button.btn { + background-color: #fff; + box-sizing: border-box; + outline: 0; + display: inline-flex; + align-items: center; + padding: 0 6px; + min-height: 36px; + line-height: 22px; + white-space: nowrap; + text-align: center; + text-transform: uppercase; + font-size: 14px; + text-decoration: none; + cursor: pointer; + border-radius: 0; + transition: none; + color: #3F4141; + border: 1px solid #3F4141; + max-width: 250px; +} +media-tag button.mediatag-download-btn { + flex-flow: column; + min-height: 38px; + justify-content: center; +} +media-tag button.mediatag-download-btn > span { + display: flex; + line-height: 1.5; + align-items: center; + justify-content: center; +} +media-tag button.mediatag-download-btn * { + width: auto; +} +media-tag button.mediatag-download-btn > span.mediatag-download-name b { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +media-tag button.btn:hover, media-tag button.btn:active, media-tag button.btn:focus { + background-color: #ccc; +} +media-tag button.btn b { + margin-left: 5px; +} +media-tag button.btn .fa { + display: inline; + margin-right: 5px; +} diff --git a/customize.dist/loading.js b/customize.dist/loading.js index b0d66981c..6e4781aaf 100644 --- a/customize.dist/loading.js +++ b/customize.dist/loading.js @@ -241,7 +241,7 @@ p.cp-password-info{ animation-timing-function: cubic-bezier(.6,0.15,0.4,0.85); } -button.primary{ +button:not(.btn).primary{ border: 1px solid #4591c4; padding: 8px 12px; text-transform: uppercase; @@ -250,7 +250,7 @@ button.primary{ font-weight: bold; } -button.primary:hover{ +button:not(.btn).primary:hover{ background-color: rgb(52, 118, 162); } @@ -279,7 +279,7 @@ button.primary:hover{ var built = false; var types = ['less', 'drive', 'migrate', 'sf', 'team', 'pad', 'end']; - var current; + var current, progress; var makeList = function (data) { var c = types.indexOf(data.type); current = c; @@ -295,7 +295,7 @@ button.primary:hover{ }; var list = ''; @@ -303,7 +303,7 @@ button.primary:hover{ }; var makeBar = function (data) { var c = types.indexOf(data.type); - var l = types.length; + var l = types.length - 1; // don't count "end" as a type var progress = Math.min(data.progress, 100); var p = (progress / l) + (100 * c / l); var bar = '
'+ @@ -315,14 +315,22 @@ button.primary:hover{ var hasErrored = false; var updateLoadingProgress = function (data) { if (!built || !data) { return; } + + // Make sure progress doesn't go backward var c = types.indexOf(data.type); if (c < current) { return console.error(data); } + if (c === current && progress > data.progress) { return console.error(data); } + progress = data.progress; + try { - document.querySelector('.cp-loading-spinner-container').style.display = 'none'; - document.querySelector('.cp-loading-progress-list').innerHTML = makeList(data); - document.querySelector('.cp-loading-progress-container').innerHTML = makeBar(data); + var el1 = document.querySelector('.cp-loading-spinner-container'); + if (el1) { el1.style.display = 'none'; } + var el2 = document.querySelector('.cp-loading-progress-list'); + if (el2) { el2.innerHTML = makeList(data); } + var el3 = document.querySelector('.cp-loading-progress-container'); + if (el3) { el3.innerHTML = makeBar(data); } } catch (e) { - if (!hasErrored) { console.error(e); } + //if (!hasErrored) { console.error(e); } } }; window.CryptPad_updateLoadingProgress = updateLoadingProgress; diff --git a/customize.dist/src/less2/include/fileupload.less b/customize.dist/src/less2/include/fileupload.less index 8fb1c8857..0353b06eb 100644 --- a/customize.dist/src/less2/include/fileupload.less +++ b/customize.dist/src/less2/include/fileupload.less @@ -14,7 +14,7 @@ right: 10vw; bottom: 10vh; box-sizing: border-box; - z-index: 100000; //Z file upload table container + z-index: 100001; //Z file upload table container: just above the file picker display: none; color: darken(@colortheme_drive-bg, 10%); max-height: 180px; diff --git a/customize.dist/src/less2/include/forms.less b/customize.dist/src/less2/include/forms.less index 0acbd49b4..65eedf263 100644 --- a/customize.dist/src/less2/include/forms.less +++ b/customize.dist/src/less2/include/forms.less @@ -2,6 +2,10 @@ @import (reference) "./variables.less"; .forms_main() { + --LessLoader_require: LessLoader_currentFile(); +} + +& { @alertify-fore: @colortheme_modal-fg; @alertify-btn-fg: @alertify-fore; @alertify-light-bg: fade(@alertify-fore, 25%); @@ -124,6 +128,14 @@ font-weight: bold; } + &.btn-default { + border-color: @cryptpad_text_col; + color: @cryptpad_text_col; + &:hover, &:active, &:focus { + background-color: #ccc; + } + } + &.danger, &.btn-danger { background-color: @colortheme_alertify-red; border-color: @colortheme_alertify-red-border; diff --git a/customize.dist/src/less2/include/markdown.less b/customize.dist/src/less2/include/markdown.less index 23eb056b0..677ec13a3 100644 --- a/customize.dist/src/less2/include/markdown.less +++ b/customize.dist/src/less2/include/markdown.less @@ -64,6 +64,54 @@ } } +.mediatag_cryptpad() { + media-tag { + &:empty { + display: none !important; + } + cursor: pointer; + * { + max-width: 100%; + } + iframe[src$=".pdf"] { + width: 100%; + height: 80vh; + max-height: 90vh; + } + button.mediatag-download-btn { + flex-flow: column; + & > span { + display: flex; + line-height: 1.5; + align-items: center; + &.mediatag-download-name b { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + } + button.btn-default { + display: inline-flex; + max-width: 250px; + min-height: 38px; + justify-content: center; + .fa { + margin-right: 5px; + } + b { + margin-left: 5px; + } + } + } + media-tag:empty { + width: 100px; + height: 100px; + display: inline-block; + border: 1px solid #BBB; + } +} + .markdown_cryptpad() { word-wrap: break-word; @@ -84,23 +132,8 @@ margin-top: 4px; } } - media-tag { - cursor: pointer; - * { - max-width: 100%; - } - iframe[src$=".pdf"] { - width: 100%; - height: 80vh; - max-height: 90vh; - } - } - media-tag:empty { - width: 100px; - height: 100px; - display: inline-block; - border: 1px solid #BBB; - } + + .mediatag_cryptpad(); pre.markmap { border: 1px solid #ddd; diff --git a/customize.dist/src/less2/include/modals-ui-elements.less b/customize.dist/src/less2/include/modals-ui-elements.less index 27eb233da..ff40729a6 100644 --- a/customize.dist/src/less2/include/modals-ui-elements.less +++ b/customize.dist/src/less2/include/modals-ui-elements.less @@ -1,6 +1,7 @@ @import (reference) "./colortheme-all.less"; @import (reference) "./variables.less"; @import (reference) "./browser.less"; +@import (reference) "./markdown.less"; .modals-ui-elements_main() { --LessLoader_require: LessLoader_currentFile(); @@ -214,6 +215,7 @@ flex: 1; min-width: 0; overflow: auto; + .mediatag_cryptpad(); media-tag { & > * { max-width: 100%; diff --git a/customize.dist/src/less2/include/sidebar-layout.less b/customize.dist/src/less2/include/sidebar-layout.less index ace7350df..4273b0b9a 100644 --- a/customize.dist/src/less2/include/sidebar-layout.less +++ b/customize.dist/src/less2/include/sidebar-layout.less @@ -118,7 +118,7 @@ //border-radius: 0 0.25em 0.25em 0; //border: 1px solid #adadad; border-left: 0px; - height: @variables_input-height; + height: 40px; margin: 0 !important; } } diff --git a/docs/example.nginx.conf b/docs/example.nginx.conf index 8319c657b..0a2edcf57 100644 --- a/docs/example.nginx.conf +++ b/docs/example.nginx.conf @@ -177,8 +177,8 @@ server { add_header Cache-Control max-age=31536000; add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; - add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range'; - add_header 'Access-Control-Expose-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range'; + add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range,Content-Length'; + add_header 'Access-Control-Expose-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range,Content-Length'; try_files $uri =404; } diff --git a/lib/hk-util.js b/lib/hk-util.js index 14263e481..20e0ce5d4 100644 --- a/lib/hk-util.js +++ b/lib/hk-util.js @@ -419,9 +419,11 @@ const getHistoryOffset = (Env, channelName, lastKnownHash, _cb) => { // fall through to the next block if the offset of the hash in question is not in memory if (lastKnownHash && typeof(lkh) !== "number") { return; } + // If we have a lastKnownHash or we didn't ask for one, we don't need the next blocks + waitFor.abort(); + // Since last 2 checkpoints if (!lastKnownHash) { - waitFor.abort(); // Less than 2 checkpoints in the history: return everything if (index.cpIndex.length < 2) { return void cb(null, 0); } // Otherwise return the second last checkpoint's index @@ -436,7 +438,16 @@ const getHistoryOffset = (Env, channelName, lastKnownHash, _cb) => { to reconcile their differences. */ } - offset = lkh; + // If our lastKnownHash is older than the 2nd to last checkpoint, + // only send the last 2 checkpoints and ignore "lkh" + // XXX XXX this is probably wrong! ChainPad may not accept checkpoints that are not connected to root + // XXX We probably need to send an EUNKNOWN here so that the client can recreate a new chainpad + /*if (lkh && index.cpIndex.length >= 2 && lkh < index.cpIndex[0].offset) { + return void cb(null, index.cpIndex[0].offset); + }*/ + + // Otherwise use our lastKnownHash + cb(null, lkh); })); }).nThen((w) => { // skip past this block if the offset is anything other than -1 diff --git a/server.js b/server.js index eb2787475..3869af509 100644 --- a/server.js +++ b/server.js @@ -136,6 +136,20 @@ app.head(/^\/common\/feedback\.html/, function (req, res, next) { }); }()); +app.use('/blob', function (req, res, next) { + if (req.method === 'HEAD') { + Express.static(Path.join(__dirname, (config.blobPath || './blob')), { + setHeaders: function (res, path, stat) { + res.set('Access-Control-Allow-Origin', '*'); + res.set('Access-Control-Allow-Headers', 'Content-Length'); + res.set('Access-Control-Expose-Headers', 'Content-Length'); + } + })(req, res, next); + return; + } + next(); +}); + app.use(function (req, res, next) { if (req.method === 'OPTIONS' && /\/blob\//.test(req.url)) { res.setHeader('Access-Control-Allow-Origin', '*'); diff --git a/www/common/common-interface.js b/www/common/common-interface.js index 9fae121dc..3e9d848cd 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -65,12 +65,13 @@ define([ switch (e.which) { case 27: // cancel if (typeof(no) === 'function') { no(e); } + $(el || window).off('keydown', handler); break; case 13: // enter if (typeof(yes) === 'function') { yes(e); } + $(el || window).off('keydown', handler); break; } - $(el || window).off('keydown', handler); }; $(el || window).keydown(handler); diff --git a/www/common/common-thumbnail.js b/www/common/common-thumbnail.js index 8c78d9d6d..012c2c9b4 100644 --- a/www/common/common-thumbnail.js +++ b/www/common/common-thumbnail.js @@ -3,9 +3,9 @@ define([ '/common/common-util.js', '/common/visible.js', '/common/common-hash.js', - '/file/file-crypto.js', + '/common/media-tag.js', '/bower_components/tweetnacl/nacl-fast.min.js', -], function ($, Util, Visible, Hash, FileCrypto) { +], function ($, Util, Visible, Hash, MediaTag) { var Nacl = window.nacl; var Thumb = { dimension: 100, @@ -314,7 +314,7 @@ define([ var hexFileName = secret.channel; var src = fileHost + Hash.getBlobPathFromHex(hexFileName); var key = secret.keys && secret.keys.cryptKey; - FileCrypto.fetchDecryptedMetadata(src, key, function (e, metadata) { + MediaTag.fetchDecryptedMetadata(src, key, function (e, metadata) { if (e) { if (e === 'XHR_ERROR') { return; } return console.error(e); diff --git a/www/common/common-util.js b/www/common/common-util.js index dfc6e12d7..603e38a30 100644 --- a/www/common/common-util.js +++ b/www/common/common-util.js @@ -274,28 +274,73 @@ // given a path, asynchronously return an arraybuffer - Util.fetch = function (src, cb, progress) { - var CB = Util.once(cb); + var getCacheKey = function (src) { + var _src = src.replace(/(\/)*$/, ''); // Remove trailing slashes + var idx = _src.lastIndexOf('/'); + var cacheKey = _src.slice(idx+1); + if (!/^[a-f0-9]{48}$/.test(cacheKey)) { cacheKey = undefined; } + return cacheKey; + }; + Util.fetch = function (src, cb, progress, cache) { + var CB = Util.once(Util.mkAsync(cb)); + + var cacheKey = getCacheKey(src); + var getBlobCache = function (id, cb) { + if (!cache || typeof(cache.getBlobCache) !== "function") { return void cb('EINVAL'); } + cache.getBlobCache(id, cb); + }; + var setBlobCache = function (id, u8, cb) { + if (!cache || typeof(cache.setBlobCache) !== "function") { return void cb('EINVAL'); } + cache.setBlobCache(id, u8, cb); + }; - var xhr = new XMLHttpRequest(); - xhr.open("GET", src, true); - if (progress) { - xhr.addEventListener("progress", function (evt) { - if (evt.lengthComputable) { - var percentComplete = evt.loaded / evt.total; - progress(percentComplete); + var xhr; + + var fetch = function () { + xhr = new XMLHttpRequest(); + xhr.open("GET", src, true); + if (progress) { + xhr.addEventListener("progress", function (evt) { + if (evt.lengthComputable) { + var percentComplete = evt.loaded / evt.total; + progress(percentComplete); + } + }, false); + } + xhr.responseType = "arraybuffer"; + xhr.onerror = function (err) { CB(err); }; + xhr.onload = function () { + if (/^4/.test(''+this.status)) { + return CB('XHR_ERROR'); } - }, false); - } - xhr.responseType = "arraybuffer"; - xhr.onerror = function (err) { CB(err); }; - xhr.onload = function () { - if (/^4/.test(''+this.status)) { - return CB('XHR_ERROR'); + + var arrayBuffer = xhr.response; + if (arrayBuffer) { + var u8 = new Uint8Array(arrayBuffer); + if (cacheKey) { + return void setBlobCache(cacheKey, u8, function () { + CB(null, u8); + }); + } + return void CB(void 0, u8); + } + CB('ENOENT'); + }; + xhr.send(null); + }; + + if (!cacheKey) { return void fetch(); } + + getBlobCache(cacheKey, function (err, u8) { + if (err || !u8) { return void fetch(); } + CB(void 0, u8); + }); + + return { + cancel: function () { + if (xhr && xhr.abort) { xhr.abort(); } } - return void CB(void 0, new Uint8Array(xhr.response)); }; - xhr.send(null); }; Util.dataURIToBlob = function (dataURI) { diff --git a/www/common/cryptget.js b/www/common/cryptget.js index e394788d7..1cbd5056e 100644 --- a/www/common/cryptget.js +++ b/www/common/cryptget.js @@ -6,10 +6,11 @@ define([ '/common/common-hash.js', '/common/common-realtime.js', '/common/outer/network-config.js', + '/common/outer/cache-store.js', '/common/pinpad.js', '/bower_components/nthen/index.js', '/bower_components/chainpad/chainpad.dist.js', -], function (Crypto, CPNetflux, Netflux, Util, Hash, Realtime, NetConfig, Pinpad, nThen) { +], function (Crypto, CPNetflux, Netflux, Util, Hash, Realtime, NetConfig, Cache, Pinpad, nThen) { var finish = function (S, err, doc) { if (S.done) { return; } S.cb(err, doc); @@ -92,7 +93,8 @@ define([ validateKey: secret.keys.validateKey || undefined, crypto: Crypto.createEncryptor(secret.keys), logLevel: 0, - initialState: opt.initialState + initialState: opt.initialState, + Cache: Cache }; return config; }; @@ -132,9 +134,11 @@ define([ }; config.onError = function (info) { + console.warn(info); finish(Session, info.error); }; config.onChannelError = function (info) { + console.error(info); finish(Session, info.error); }; diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 9f88e85a9..46f569a3e 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -3,6 +3,7 @@ define([ '/customize/messages.js', '/common/common-util.js', '/common/common-hash.js', + '/common/outer/cache-store.js', '/common/common-messaging.js', '/common/common-constants.js', '/common/common-feedback.js', @@ -14,7 +15,7 @@ define([ '/customize/application_config.js', '/bower_components/nthen/index.js', -], function (Config, Messages, Util, Hash, +], function (Config, Messages, Util, Hash, Cache, Messaging, Constants, Feedback, Visible, UserObject, LocalStore, Channel, Block, AppConfig, Nthen) { @@ -701,7 +702,7 @@ define([ }); }; - common.useFile = function (Crypt, cb, optsPut) { + common.useFile = function (Crypt, cb, optsPut, onProgress) { var fileHost = Config.fileHost || window.location.origin; var data = common.fromFileData; var parsed = Hash.parsePadUrl(data.href); @@ -758,7 +759,9 @@ define([ return void cb(err); } u8 = _u8; - })); + }), function (progress) { + onProgress(progress * 50); + }, Cache); }).nThen(function (waitFor) { require(["/file/file-crypto.js"], waitFor(function (FileCrypto) { FileCrypto.decrypt(u8, key, waitFor(function (err, _res) { @@ -767,7 +770,9 @@ define([ return void cb(err); } res = _res; - })); + }), function (progress) { + onProgress(50 + progress * 50); + }); })); }).nThen(function (waitFor) { var ext = Util.parseFilename(data.title).ext; @@ -991,6 +996,8 @@ define([ pad.onJoinEvent = Util.mkEvent(); pad.onLeaveEvent = Util.mkEvent(); pad.onDisconnectEvent = Util.mkEvent(); + pad.onCacheEvent = Util.mkEvent(); + pad.onCacheReadyEvent = Util.mkEvent(); pad.onConnectEvent = Util.mkEvent(); pad.onErrorEvent = Util.mkEvent(); pad.onMetadataEvent = Util.mkEvent(); @@ -1003,6 +1010,10 @@ define([ postMessage("GIVE_PAD_ACCESS", data, cb); }; + common.onCorruptedCache = function (channel) { + postMessage("CORRUPTED_CACHE", channel); + }; + common.setPadMetadata = function (data, cb) { postMessage('SET_PAD_METADATA', data, cb); }; @@ -1956,6 +1967,8 @@ define([ PAD_JOIN: common.padRpc.onJoinEvent.fire, PAD_LEAVE: common.padRpc.onLeaveEvent.fire, PAD_DISCONNECT: common.padRpc.onDisconnectEvent.fire, + PAD_CACHE: common.padRpc.onCacheEvent.fire, + PAD_CACHE_READY: common.padRpc.onCacheReadyEvent.fire, PAD_CONNECT: common.padRpc.onConnectEvent.fire, PAD_ERROR: common.padRpc.onErrorEvent.fire, PAD_METADATA: common.padRpc.onMetadataEvent.fire, diff --git a/www/common/diffMarked.js b/www/common/diffMarked.js index 734551983..2bbd51dde 100644 --- a/www/common/diffMarked.js +++ b/www/common/diffMarked.js @@ -303,14 +303,22 @@ define([ return renderParagraph(p); }; + // Note: iframe, video and audio are used in mediatags and are allowed in rich text pads. var forbiddenTags = [ 'SCRIPT', - 'IFRAME', + //'IFRAME', 'OBJECT', 'APPLET', - 'VIDEO', // privacy implications of videos are the same as images - 'AUDIO', // same with audio + //'VIDEO', // privacy implications of videos are the same as images + //'AUDIO', // same with audio + 'SOURCE' + ]; + var restrictedTags = [ + 'IFRAME', + 'VIDEO', + 'AUDIO' ]; + var unsafeTag = function (info) { /*if (info.node && $(info.node).parents('media-tag').length) { // Do not remove elements inside a media-tag @@ -347,9 +355,16 @@ define([ parent.removeChild(node); }; + // Only allow iframe, video and audio with local source + var checkSrc = function (root) { + if (restrictedTags.indexOf(root.nodeName.toUpperCase()) === -1) { return true; } + return root.getAttribute && /^blob\:/.test(root.getAttribute('src')); + }; + var removeForbiddenTags = function (root) { if (!root) { return; } if (forbiddenTags.indexOf(root.nodeName.toUpperCase()) !== -1) { removeNode(root); } + if (!checkSrc(root)) { removeNode(root); } slice(root.children).forEach(removeForbiddenTags); }; @@ -658,7 +673,7 @@ define([ $(contextMenu.menu).find('li').show(); contextMenu.show(e); }); - if ($mt.children().length) { + if ($mt.children().length && $mt[0]._mediaObject) { $mt.off('click dblclick preview'); $mt.on('preview', onPreview($mt)); if ($mt.find('img').length) { @@ -668,15 +683,15 @@ define([ } return; } - MediaTag(el); + var mediaObject = MediaTag(el); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.type === 'childList') { - var list_values = slice(mutation.target.children) + var list_values = slice(el.children) .map(function (el) { return el.outerHTML; }) .join(''); - mediaMap[mutation.target.getAttribute('src')] = list_values; - observer.disconnect(); + mediaMap[el.getAttribute('src')] = list_values; + if (mediaObject.complete) { observer.disconnect(); } } }); $mt.off('click dblclick preview'); @@ -689,6 +704,7 @@ define([ }); observer.observe(el, { attributes: false, + subtree: true, childList: true, characterData: false }); diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index 53e496ef4..e92e6e0fa 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -42,6 +42,7 @@ define([ var APP = window.APP = { editable: false, + online: true, mobile: function () { if (window.matchMedia) { return !window.matchMedia('(any-pointer:fine)').matches; } else { return $('body').width() <= 600; } @@ -267,13 +268,25 @@ define([ }; // Handle disconnect/reconnect - var setEditable = function (state, isHistory) { + // If isHistory and isSf are both false, update the "APP.online" flag + // If isHistory is true, update the "APP.history" flag + // isSf is used to detect offline shared folders: setEditable is called on displayDirectory + var setEditable = function (state, isHistory, isSf) { if (APP.closed || !APP.$content || !$.contains(document.documentElement, APP.$content[0])) { return; } + if (isHistory) { + APP.history = !state; + } else if (!isSf) { + APP.online = state; + } + state = APP.online && !APP.history && state; APP.editable = !APP.readOnly && state; + if (!state) { APP.$content.addClass('cp-app-drive-readonly'); - if (!isHistory) { + if (!APP.history || !APP.online) { $('#cp-app-drive-connection-state').show(); + } else { + $('#cp-app-drive-connection-state').hide(); } $('[draggable="true"]').attr('draggable', false); } @@ -3670,6 +3683,15 @@ define([ } var readOnlyFolder = false; + + // If the shared folder is offline, add the "DISCONNECTED" banner, otherwise + // use the normal "editable" behavior (based on drive offline or history mode) + if (sfId && manager.folders[sfId].offline) { + setEditable(false, false, true); + } else { + setEditable(true, false, true); + } + if (APP.readOnly) { // Read-only drive (team?) $content.prepend($readOnly.clone()); @@ -4149,6 +4171,17 @@ define([ data.name = Util.fixFileName(folderName); data.folderName = Util.fixFileName(folderName) + '.zip'; + var uo = manager.user.userObject; + if (sfId && manager.folders[sfId]) { + uo = manager.folders[sfId].userObject; + } + if (uo.getFilesRecursively) { + data.list = uo.getFilesRecursively(folderElement).map(function (el) { + var d = uo.getFileData(el); + return d.channel; + }); + } + APP.FM.downloadFolder(data, function (err, obj) { console.log(err, obj); console.log('DONE'); diff --git a/www/common/inner/cache.js b/www/common/inner/cache.js new file mode 100644 index 000000000..be5b781c2 --- /dev/null +++ b/www/common/inner/cache.js @@ -0,0 +1,33 @@ +define([ +], function () { + var S = {}; + + S.create = function (sframeChan) { + var getBlobCache = function (id, cb) { + sframeChan.query('Q_GET_BLOB_CACHE', {id:id}, function (err, data) { + var e = err || (data && data.error); + if (e) { return void cb(e); } + if (!data || typeof(data) !== "object") { return void cb('EINVAL'); } + cb(null, data); + }, { raw: true }); + }; + var setBlobCache = function (id, u8, cb) { + sframeChan.query('Q_SET_BLOB_CACHE', { + id: id, + u8: u8 + }, function (err, data) { + var e = err || (data && data.error) || undefined; + cb(e); + }, { raw: true }); + }; + + + return { + getBlobCache: getBlobCache, + setBlobCache: setBlobCache + }; + }; + + return S; +}); + diff --git a/www/common/inner/common-mediatag.js b/www/common/inner/common-mediatag.js index 3d28d126a..80195c71c 100644 --- a/www/common/inner/common-mediatag.js +++ b/www/common/inner/common-mediatag.js @@ -17,11 +17,17 @@ define([ var Nacl = window.nacl; // Configure MediaTags to use our local viewer + // This file is loaded by sframe-common so the following config is used in all the inner apps if (MediaTag) { MediaTag.setDefaultConfig('pdf', { viewer: '/common/pdfjs/web/viewer.html' }); + MediaTag.setDefaultConfig('download', { + text: Messages.mediatag_saveButton, + textDl: Messages.mediatag_loadButton, + }); } + MT.MediaTag = MediaTag; // Cache of the avatars outer html (including ) var avatars = {}; @@ -68,7 +74,7 @@ define([ childList: true, characterData: false }); - MediaTag($tag[0]).on('error', function (data) { + MediaTag($tag[0], {force: true}).on('error', function (data) { console.error(data); }); }; @@ -241,7 +247,6 @@ define([ var locked = false; var show = function (_i) { if (locked) { return; } - locked = true; if (_i < 0) { i = 0; } else if (_i > tags.length -1) { i = tags.length - 1; } else { i = _i; } @@ -285,7 +290,6 @@ define([ if (_key) { key = 'cryptpad:' + Nacl.util.encodeBase64(_key); } } if (!src || !key) { - locked = false; $spinner.hide(); return void UI.log(Messages.error); } @@ -299,13 +303,18 @@ define([ locked = false; $spinner.hide(); UI.log(Messages.error); + }).on('progress', function () { + $spinner.hide(); + locked = true; + }).on('complete', function () { + locked = false; + $spinner.hide(); }); }); } var observer = new MutationObserver(function(mutations) { mutations.forEach(function() { - locked = false; $spinner.hide(); }); }); @@ -377,6 +386,14 @@ define([ 'tabindex': '-1', 'data-icon': "fa-eye", }, Messages.pad_mediatagPreview)), + h('li.cp-svg', h('a.cp-app-code-context-openin.dropdown-item', { + 'tabindex': '-1', + 'data-icon': "fa-external-link", + }, Messages.pad_mediatagOpen)), + h('li.cp-svg', h('a.cp-app-code-context-share.dropdown-item', { + 'tabindex': '-1', + 'data-icon': "fa-shhare-alt", + }, Messages.pad_mediatagShare)), h('li', h('a.cp-app-code-context-saveindrive.dropdown-item', { 'tabindex': '-1', 'data-icon': "fa-cloud-upload", @@ -413,12 +430,29 @@ define([ } else if ($this.hasClass("cp-app-code-context-download")) { var media = Util.find($mt, [0, '_mediaObject']); + if (!media) { return void console.error('no media'); } + if (!media.complete) { return void UI.warn(Messages.mediatag_notReady); } if (!(media && media._blob)) { return void console.error($mt); } window.saveAs(media._blob.content, media.name); } else if ($this.hasClass("cp-app-code-context-open")) { $mt.trigger('preview'); } + else if ($this.hasClass("cp-app-code-context-openin")) { + var hash = common.getHashFromMediaTag($mt); + common.openURL(Hash.hashToHref(hash, 'file')); + } + else if ($this.hasClass("cp-app-code-context-share")) { + var data = { + file: true, + pathname: '/file/', + hashes: { + fileHash: common.getHashFromMediaTag($mt) + }, + title: Util.find($mt[0], ['_mediaObject', 'name']) || '' + }; + common.getSframeChannel().event('EV_SHARE_OPEN', data); + } }); return m; diff --git a/www/common/make-backup.js b/www/common/make-backup.js index b718ae9c1..9ba925fd7 100644 --- a/www/common/make-backup.js +++ b/www/common/make-backup.js @@ -1,17 +1,18 @@ define([ 'jquery', - '/common/cryptget.js', '/file/file-crypto.js', '/common/common-hash.js', '/common/common-util.js', '/common/common-interface.js', '/common/hyperscript.js', '/common/common-feedback.js', + '/common/inner/cache.js', '/customize/messages.js', '/bower_components/nthen/index.js', '/bower_components/saferphore/index.js', '/bower_components/jszip/dist/jszip.min.js', -], function ($, Crypt, FileCrypto, Hash, Util, UI, h, Feedback, Messages, nThen, Saferphore, JsZip) { +], function ($, FileCrypto, Hash, Util, UI, h, Feedback, + Cache, Messages, nThen, Saferphore, JsZip) { var saveAs = window.saveAs; var sanitize = function (str) { @@ -53,9 +54,6 @@ define([ var _downloadFile = function (ctx, fData, cb, updateProgress) { var cancelled = false; - var cancel = function () { - cancelled = true; - }; var href = (fData.href && fData.href.indexOf('#') !== -1) ? fData.href : fData.roHref; var parsed = Hash.parsePadUrl(href); var hash = parsed.hash; @@ -63,10 +61,13 @@ define([ var secret = Hash.getSecrets('file', hash, fData.password); var src = (ctx.fileHost || '') + Hash.getBlobPathFromHex(secret.channel); var key = secret.keys && secret.keys.cryptKey; - Util.fetch(src, function (err, u8) { + + var fetchObj, decryptObj; + + fetchObj = Util.fetch(src, function (err, u8) { if (cancelled) { return; } if (err) { return void cb('E404'); } - FileCrypto.decrypt(u8, key, function (err, res) { + decryptObj = FileCrypto.decrypt(u8, key, function (err, res) { if (cancelled) { return; } if (err) { return void cb(err); } if (!res.content) { return void cb('EEMPTY'); } @@ -78,8 +79,25 @@ define([ content: res.content, download: dl }); - }, updateProgress && updateProgress.progress2); - }, updateProgress && updateProgress.progress); + }, function (data) { + if (cancelled) { return; } + if (updateProgress && updateProgress.progress2) { + updateProgress.progress2(data); + } + }); + }, function (data) { + if (cancelled) { return; } + if (updateProgress && updateProgress.progress) { + updateProgress.progress(data); + } + }, ctx.cache); + + var cancel = function () { + cancelled = true; + if (fetchObj && fetchObj.cancel) { fetchObj.cancel(); } + if (decryptObj && decryptObj.cancel) { decryptObj.cancel(); } + }; + return { cancel: cancel }; @@ -162,10 +180,10 @@ define([ if (ctx.stop) { return; } if (to) { clearTimeout(to); } //setTimeout(g, 2000); - g(); - w(); ctx.done++; ctx.updateProgress('download', {max: ctx.max, current: ctx.done}); + g(); + w(); }; var error = function (err) { @@ -274,7 +292,7 @@ define([ }; // Main function. Create the empty zip and fill it starting from drive.root - var create = function (data, getPad, fileHost, cb, progress) { + var create = function (data, getPad, fileHost, cb, progress, cache) { if (!data || !data.uo || !data.uo.drive) { return void cb('EEMPTY'); } var sem = Saferphore.create(5); var ctx = { @@ -288,7 +306,8 @@ define([ sem: sem, updateProgress: progress, max: 0, - done: 0 + done: 0, + cache: cache }; var filesData = data.sharedFolderId && ctx.sf[data.sharedFolderId] ? ctx.sf[data.sharedFolderId].filesData : ctx.data.filesData; progress('reading', -1); @@ -312,13 +331,14 @@ define([ delete ctx.zip; }; return { - stop: stop + stop: stop, + cancel: stop }; }; var _downloadFolder = function (ctx, data, cb, updateProgress) { - create(data, ctx.get, ctx.fileHost, function (blob, errors) { + return create(data, ctx.get, ctx.fileHost, function (blob, errors) { if (errors && errors.length) { console.error(errors); } // TODO show user errors var dl = function () { saveAs(blob, data.folderName); @@ -332,10 +352,13 @@ define([ if (typeof progress.current !== "number") { return; } updateProgress.folderProgress(progress.current / progress.max); } + else if (state === "compressing") { + updateProgress.folderProgress(2); + } else if (state === "done") { - updateProgress.folderProgress(1); + updateProgress.folderProgress(3); } - }); + }, ctx.cache); }; var createExportUI = function (origin) { diff --git a/www/common/media-tag.js b/www/common/media-tag.js index 27683c54e..c328399c5 100644 --- a/www/common/media-tag.js +++ b/www/common/media-tag.js @@ -1,8 +1,6 @@ -(function(name, definition) { - if (typeof module !== 'undefined') { module.exports = definition(); } - else if (typeof define === 'function' && typeof define.amd === 'object') { define(definition); } - else { this[name] = definition(); } -}('MediaTag', function() { +(function (window) { +var factory = function () { + var Promise = window.Promise; var cache; var cypherChunkLength = 131088; @@ -63,7 +61,8 @@ ], pdf: {}, download: { - text: "Download" + text: "Save", + textDl: "Load attachment" }, Plugins: { /** @@ -114,8 +113,8 @@ }, download: function (metadata, url, content, cfg, cb) { var btn = document.createElement('button'); - btn.setAttribute('class', 'btn btn-success'); - btn.innerHTML = cfg.download.text + '
' + + btn.setAttribute('class', 'btn btn-default'); + btn.innerHTML = '' + cfg.download.text + '
' + (metadata.name ? '' + fixHTML(metadata.name) + '' : ''); btn.addEventListener('click', function () { saveFile(content, url, metadata.name); @@ -125,28 +124,185 @@ } }; + var makeProgressBar = function (cfg, mediaObject) { + if (mediaObject.bar) { return; } + mediaObject.bar = true; + var style = (function(){/* +.mediatag-progress-container { + position: relative; + border: 1px solid #0087FF; + background: white; + height: 25px; + display: inline-flex; + width: 200px; + align-items: center; + justify-content: center; + box-sizing: border-box; + vertical-align: top; +} +.mediatag-progress-bar { + position: absolute; + left: 0; + top: 0; + bottom: 0; + background: #0087FF; + width: 0%; +} +.mediatag-progress-text { + height: 25px; + width: 50px; + margin-left: 5px; + line-height: 25px; + vertical-align: top; + display: inline-block; + color: #3F4141; + font-weight: bold; +} +*/}).toString().slice(14, -3); + var container = document.createElement('div'); + container.classList.add('mediatag-progress-container'); + var bar = document.createElement('div'); + bar.classList.add('mediatag-progress-bar'); + container.appendChild(bar); + + var text = document.createElement('span'); + text.classList.add('mediatag-progress-text'); + text.innerText = '0%'; + + mediaObject.on('progress', function (obj) { + var percent = obj.progress; + text.innerText = (Math.round(percent*10))/10+'%'; + bar.setAttribute('style', 'width:'+percent+'%;'); + }); + + mediaObject.tag.innerHTML = ''; + mediaObject.tag.appendChild(container); + mediaObject.tag.appendChild(text); + }; + var makeDownloadButton = function (cfg, mediaObject, size, cb) { + var metadata = cfg.metadata || {}; + var i = ''; + var name = metadata.name ? ''+ i +''+ + fixHTML(metadata.name)+'' : ''; + var btn = document.createElement('button'); + btn.setAttribute('class', 'btn btn-default mediatag-download-btn'); + btn.innerHTML = name + '' + (name ? '' : i) + + cfg.download.textDl + ' (' + size + 'MB)'; + btn.addEventListener('click', function () { + makeProgressBar(cfg, mediaObject); + var a = (cfg.body || document).querySelectorAll('media-tag[src="'+mediaObject.tag.getAttribute('src')+'"] button.mediatag-download-btn'); + for(var i = 0; i < a.length; i++) { + if (a[i] !== btn) { a[i].click(); } + } + cb(); + }); + mediaObject.tag.innerHTML = ''; + mediaObject.tag.appendChild(btn); + }; + + var getCacheKey = function (src) { + var _src = src.replace(/(\/)*$/, ''); // Remove trailing slashes + var idx = _src.lastIndexOf('/'); + var cacheKey = _src.slice(idx+1); + if (!/^[a-f0-9]{48}$/.test(cacheKey)) { cacheKey = undefined; } + return cacheKey; + }; + + var getBlobCache = function (id, cb) { + if (!config.Cache || typeof(config.Cache.getBlobCache) !== "function") { + return void cb('EINVAL'); + } + config.Cache.getBlobCache(id, cb); + }; + var setBlobCache = function (id, u8, cb) { + if (!config.Cache || typeof(config.Cache.setBlobCache) !== "function") { + return void cb('EINVAL'); + } + config.Cache.setBlobCache(id, u8, cb); + }; + + var getFileSize = function (src, _cb) { + var cb = function (e, res) { + _cb(e, res); + cb = function () {}; + }; + + var cacheKey = getCacheKey(src); + + var check = function () { + var xhr = new XMLHttpRequest(); + xhr.open("HEAD", src); + xhr.onerror = function () { return void cb("XHR_ERROR"); }; + xhr.onreadystatechange = function() { + if (this.readyState === this.DONE) { + cb(null, Number(xhr.getResponseHeader("Content-Length"))); + } + }; + xhr.onload = function () { + if (/^4/.test('' + this.status)) { return void cb("XHR_ERROR " + this.status); } + }; + xhr.send(); + }; + + if (!cacheKey) { return void check(); } + + getBlobCache(cacheKey, function (err, u8) { + if (err || !u8) { return void check(); } + cb(null, 0); + }); + }; // Download a blob from href - var download = function (src, _cb) { + var download = function (src, _cb, progressCb) { var cb = function (e, res) { _cb(e, res); cb = function () {}; }; - var xhr = new XMLHttpRequest(); - xhr.open('GET', src, true); - xhr.responseType = 'arraybuffer'; + var cacheKey = getCacheKey(src); - xhr.onerror = function () { return void cb("XHR_ERROR"); }; - xhr.onload = function () { - // Error? - if (/^4/.test('' + this.status)) { return void cb("XHR_ERROR " + this.status); } + var fetch = function () { + var xhr = new XMLHttpRequest(); + xhr.open('GET', src, true); + xhr.responseType = 'arraybuffer'; + + var progress = function (offset) { + progressCb(offset * 100); + }; + xhr.addEventListener("progress", function (evt) { + if (evt.lengthComputable) { + var percentComplete = evt.loaded / evt.total; + progress(percentComplete); + } + }, false); + + xhr.onerror = function () { return void cb("XHR_ERROR"); }; + xhr.onload = function () { + // Error? + if (/^4/.test('' + this.status)) { return void cb("XHR_ERROR " + this.status); } + + var arrayBuffer = xhr.response; + if (arrayBuffer) { + var u8 = new Uint8Array(arrayBuffer); + if (cacheKey) { + return void setBlobCache(cacheKey, u8, function () { + cb(null, u8); + }); + } + cb(null, u8); + } + }; - var arrayBuffer = xhr.response; - if (arrayBuffer) { cb(null, new Uint8Array(arrayBuffer)); } + xhr.send(null); }; - xhr.send(null); + if (!cacheKey) { return void fetch(); } + + getBlobCache(cacheKey, function (err, u8) { + if (err || !u8) { return void fetch(); } + cb(null, u8); + }); + }; // Decryption tools @@ -192,6 +348,95 @@ } }; + // The metadata size can go up to 65535 (16 bits - 2 bytes) + // The first 8 bits are stored in A[0] + // The last 8 bits are stored in A[0] + var uint8ArrayJoin = function (AA) { + var l = 0; + var i = 0; + for (; i < AA.length; i++) { l += AA[i].length; } + var C = new Uint8Array(l); + + i = 0; + for (var offset = 0; i < AA.length; i++) { + C.set(AA[i], offset); + offset += AA[i].length; + } + return C; + }; + var fetchMetadata = function (src, _cb) { + var cb = function (e, res) { + _cb(e, res); + cb = function () {}; + }; + + var cacheKey = getCacheKey(src); + + var fetch = function () { + var xhr = new XMLHttpRequest(); + xhr.open('GET', src, true); + xhr.setRequestHeader('Range', 'bytes=0-1'); + xhr.responseType = 'arraybuffer'; + + xhr.onerror = function () { return void cb("XHR_ERROR"); }; + xhr.onload = function () { + // Error? + if (/^4/.test('' + this.status)) { return void cb("XHR_ERROR " + this.status); } + var res = new Uint8Array(xhr.response); + var size = Decrypt.decodePrefix(res); + var xhr2 = new XMLHttpRequest(); + + xhr2.open("GET", src, true); + xhr2.setRequestHeader('Range', 'bytes=2-' + (size + 2)); + xhr2.responseType = 'arraybuffer'; + xhr2.onload = function () { + if (/^4/.test('' + this.status)) { return void cb("XHR_ERROR " + this.status); } + var res2 = new Uint8Array(xhr2.response); + var all = uint8ArrayJoin([res, res2]); + cb(void 0, all); + }; + xhr2.send(null); + }; + + xhr.send(null); + }; + + if (!cacheKey) { return void fetch(); } + + getBlobCache(cacheKey, function (err, u8) { + if (err || !u8) { return void fetch(); } + + var size = Decrypt.decodePrefix(u8.subarray(0,2)); + console.error(size); + + cb(null, u8.subarray(0, size+2)); + }); + }; + var decryptMetadata = function (u8, key) { + var prefix = u8.subarray(0, 2); + var metadataLength = Decrypt.decodePrefix(prefix); + + var metaBox = new Uint8Array(u8.subarray(2, 2 + metadataLength)); + var metaChunk = window.nacl.secretbox.open(metaBox, Decrypt.createNonce(), key); + + try { + return JSON.parse(window.nacl.util.encodeUTF8(metaChunk)); + } + catch (e) { return null; } + }; + var fetchDecryptedMetadata = function (src, strKey, cb) { + if (typeof(src) !== 'string') { + return window.setTimeout(function () { + cb('NO_SOURCE'); + }); + } + fetchMetadata(src, function (e, buffer) { + if (e) { return cb(e); } + var key = Decrypt.getKeyFromStr(strKey); + cb(void 0, decryptMetadata(buffer, key)); + }); + }; + // Decrypts a Uint8Array with the given key. var decrypt = function (u8, strKey, done, progressCb) { var Nacl = window.nacl; @@ -372,6 +617,7 @@ var handlers = cfg.handlers || { 'progress': [], 'complete': [], + 'metadata': [], 'error': [] }; @@ -422,6 +668,7 @@ // End media-tag rendering: display the tag and emit the event var end = function (decrypted) { + mediaObject.complete = true; process(mediaObject, decrypted, cfg, function (err) { if (err) { return void emit('error', err); } mediaObject._blob = decrypted; @@ -429,32 +676,78 @@ }); }; - // If we have the blob in our cache, don't download & decrypt it again, just display - if (cache[uid]) { - end(cache[uid]); - return mediaObject; - } + var error = function (err) { + mediaObject.tag.innerHTML = ''; + emit('error', err); + }; - // Download the encrypted blob - download(src, function (err, u8Encrypted) { - if (err) { - if (err === "XHR_ERROR 404") { - mediaObject.tag.innerHTML = ''; - } - return void emit('error', err); + var getCache = function () { + var c = cache[uid]; + if (!c || !c.promise || !c.mt) { return; } + return c; + }; + + var dl = function () { + // Download the encrypted blob + cache[uid] = getCache() || { + promise: new Promise(function (resolve, reject) { + download(src, function (err, u8Encrypted) { + if (err) { + return void reject(err); + } + // Decrypt the blob + decrypt(u8Encrypted, strKey, function (errDecryption, u8Decrypted) { + if (errDecryption) { + return void reject(errDecryption); + } + emit('metadata', u8Decrypted.metadata); + resolve(u8Decrypted); + }, function (progress) { + emit('progress', { + progress: 50+0.5*progress + }); + }); + }, function (progress) { + emit('progress', { + progress: 0.5*progress + }); + }); + }), + mt: mediaObject + }; + if (cache[uid].mt !== mediaObject) { + // Add progress for other instances of this tag + cache[uid].mt.on('progress', function (obj) { + if (!mediaObject.bar && !cfg.force) { makeProgressBar(cfg, mediaObject); } + emit('progress', { + progress: obj.progress + }); + }); } - // Decrypt the blob - decrypt(u8Encrypted, strKey, function (errDecryption, u8Decrypted) { - if (errDecryption) { - return void emit('error', errDecryption); + cache[uid].promise.then(function (u8) { + end(u8); + }, function (err) { + error(err); + }); + }; + + if (cfg.force) { dl(); return mediaObject; } + + var maxSize = typeof(config.maxDownloadSize) === "number" ? config.maxDownloadSize + : (5 * 1024 * 1024); + fetchDecryptedMetadata(src, strKey, function (err, md) { + if (err) { return void error(err); } + cfg.metadata = md; + emit('metadata', md); + getFileSize(src, function (err, size) { + // If the size is smaller than the autodownload limit, load the blob. + // If the blob is already loaded or being loaded, don't show the button. + if (!size || size < maxSize || getCache()) { + makeProgressBar(cfg, mediaObject); + return void dl(); } - // Cache and display the decrypted blob - cache[uid] = u8Decrypted; - end(u8Decrypted); - }, function (progress) { - emit('progress', { - progress: progress - }); + var sizeMb = Math.round(10 * size / 1024 / 1024) / 10; + makeDownloadButton(cfg, mediaObject, sizeMb, dl); }); }); @@ -468,5 +761,20 @@ config[key] = value; }; + init.fetchDecryptedMetadata = fetchDecryptedMetadata; + return init; -})); +}; + + if (typeof(module) !== 'undefined' && module.exports) { + module.exports = factory(); + } else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) { + define([ + '/bower_components/es6-promise/es6-promise.min.js' + ], function () { + return factory(); + }); + } else { + // unsupported initialization + } +}(typeof(window) !== 'undefined'? window : {})); diff --git a/www/common/onlyoffice/inner.js b/www/common/onlyoffice/inner.js index 110b80a76..b4bdeef92 100644 --- a/www/common/onlyoffice/inner.js +++ b/www/common/onlyoffice/inner.js @@ -1423,7 +1423,7 @@ define([ console.error(e); callback(""); } - }); + }, void 0, common.getCache()); }; APP.docEditor = new window.DocsAPI.DocEditor("cp-app-oo-placeholder-a", APP.ooconfig); diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 62e078050..65078b287 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -10,6 +10,7 @@ define([ '/common/common-realtime.js', '/common/common-messaging.js', '/common/pinpad.js', + '/common/outer/cache-store.js', '/common/outer/sharedfolder.js', '/common/outer/cursor.js', '/common/outer/onlyoffice.js', @@ -28,7 +29,7 @@ define([ '/bower_components/nthen/index.js', '/bower_components/saferphore/index.js', ], function (Sortify, UserObject, ProxyManager, Migrate, Hash, Util, Constants, Feedback, - Realtime, Messaging, Pinpad, + Realtime, Messaging, Pinpad, Cache, SF, Cursor, OnlyOffice, Mailbox, Profile, Team, Messenger, History, NetConfig, AppConfig, Crypto, ChainPad, CpNetflux, Listmap, nThen, Saferphore) { @@ -120,10 +121,13 @@ define([ Store.getSharedFolder = function (clientId, data, cb) { var s = getStore(data.teamId); var id = data.id; + var proxy; if (!s || !s.manager) { return void cb({ error: 'ENOTFOUND' }); } if (s.manager.folders[id]) { + proxy = Util.clone(s.manager.folders[id].proxy); + proxy.offline = Boolean(s.manager.folders[id].offline); // If it is loaded, return the shared folder proxy - return void cb(s.manager.folders[id].proxy); + return void cb(proxy); } else { // Otherwise, check if we know this shared folder var shared = Util.find(s.proxy, ['drive', UserObject.SHARED_FOLDERS]) || {}; @@ -1133,7 +1137,7 @@ define([ var ownedByMe = Array.isArray(owners) && owners.indexOf(edPublic) !== -1; // Add the pad if it does not exist in our drive - if (!contains || (ownedByMe && !inMyDrive)) { + if (!contains) { // || (ownedByMe && !inMyDrive)) { var autoStore = Util.find(store.proxy, ['settings', 'general', 'autostore']); if (autoStore !== 1 && !data.forceSave && !data.path && !ownedByMe) { // send event to inner to display the corner popup @@ -1590,13 +1594,20 @@ define([ Store.leavePad(null, data, function () {}); }; var conf = { + Cache: Cache, + onCacheStart: function () { + postMessage(clientId, "PAD_CACHE"); + }, + onCacheReady: function () { + postMessage(clientId, "PAD_CACHE_READY"); + }, onReady: function (pad) { var padData = pad.metadata || {}; channel.data = padData; if (padData && padData.validateKey && store.messenger) { store.messenger.storeValidateKey(data.channel, padData.validateKey); } - postMessage(clientId, "PAD_READY"); + postMessage(clientId, "PAD_READY", pad.noCache); }, onMessage: function (m, user, validateKey, isCp, hash) { channel.lastHash = hash; @@ -1727,6 +1738,14 @@ define([ channel.sendMessage(msg, clientId, cb); }; + Store.corruptedCache = function (clientId, channel) { + var chan = channels[channel]; + if (!chan || !chan.cpNf) { return; } + Cache.clearChannel(channel); + if (!chan.cpNf.resetCache) { return; } + chan.cpNf.resetCache(); + }; + // Unpin and pin the new channel in all team when changing a pad password Store.changePadPasswordPin = function (clientId, data, cb) { var oldChannel = data.oldChannel; @@ -2640,6 +2659,7 @@ define([ readOnly: false, validateKey: secret.keys.validateKey || undefined, crypto: Crypto.createEncryptor(secret.keys), + Cache: Cache, userName: 'fs', logLevel: 1, ChainPad: ChainPad, diff --git a/www/common/outer/cache-store.js b/www/common/outer/cache-store.js new file mode 100644 index 000000000..cd88de6f6 --- /dev/null +++ b/www/common/outer/cache-store.js @@ -0,0 +1,93 @@ +define([ + '/common/common-util.js', + '/bower_components/localforage/dist/localforage.min.js', +], function (Util, localForage) { + var S = {}; + + var cache = localForage.createInstance({ + name: "cp_cache" + }); + + S.getBlobCache = function (id, cb) { + cb = Util.once(Util.mkAsync(cb || function () {})); + cache.getItem(id, function (err, obj) { + if (err || !obj || !obj.c) { + return void cb(err || 'EINVAL'); + } + cb(null, obj.c); + obj.t = +new Date(); + cache.setItem(id, obj); + }); + }; + S.setBlobCache = function (id, u8, cb) { + cb = Util.once(Util.mkAsync(cb || function () {})); + if (!u8) { return void cb('EINVAL'); } + cache.setItem(id, { + c: u8, + t: (+new Date()) // 't' represent the "lastAccess" of this cache (get or set) + }, function (err) { + cb(err); + }); + }; + + // id: channel ID or blob ID + // returns array of messages + S.getChannelCache = function (id, cb) { + cb = Util.once(Util.mkAsync(cb || function () {})); + cache.getItem(id, function (err, obj) { + if (err || !obj || !Array.isArray(obj.c)) { + return void cb(err || 'EINVAL'); + } + cb(null, obj); + obj.t = +new Date(); + cache.setItem(id, obj); + }); + }; + + // Keep the last two checkpoint + any checkpoint that may exist in the last 100 messages + // FIXME: duplicate system with sliceCpIndex from lib/hk-util.js + var checkCheckpoints = function (array) { + if (!Array.isArray(array)) { return; } + // Keep the last 100 messages + if (array.length > 100) { + array.splice(0, array.length - 100); + } + // Remove every message before the first checkpoint + var firstCpIdx; + array.some(function (el, i) { + if (!el.isCheckpoint) { return; } + firstCpIdx = i; + return true; + }); + array.splice(0, firstCpIdx); + }; + + S.storeCache = function (id, validateKey, val, cb) { + cb = Util.once(Util.mkAsync(cb || function () {})); + if (!Array.isArray(val) || !validateKey) { return void cb('EINVAL'); } + checkCheckpoints(val); + cache.setItem(id, { + k: validateKey, + c: val, + t: (+new Date()) // 't' represent the "lastAccess" of this cache (get or set) + }, function (err) { + cb(err); + }); + }; + + S.clearChannel = function (id, cb) { + cb = Util.once(Util.mkAsync(cb || function () {})); + cache.removeItem(id, function () { + cb(); + }); + }; + + S.clear = function (cb) { + cb = Util.once(Util.mkAsync(cb || function () {})); + cache.clear(cb); + }; + + self.CryptPad_clearIndexedDB = S.clear; + + return S; +}); diff --git a/www/common/outer/local-store.js b/www/common/outer/local-store.js index 0aff1f9ce..645493de5 100644 --- a/www/common/outer/local-store.js +++ b/www/common/outer/local-store.js @@ -1,9 +1,10 @@ define([ '/common/common-constants.js', '/common/common-hash.js', + '/common/outer/cache-store.js', '/bower_components/localforage/dist/localforage.min.js', '/customize/application_config.js', -], function (Constants, Hash, localForage, AppConfig) { +], function (Constants, Hash, Cache, localForage, AppConfig) { var LocalStore = {}; LocalStore.setThumbnail = function (key, value, cb) { @@ -119,7 +120,14 @@ define([ return void AppConfig.customizeLogout(cb); } - if (cb) { cb(); } + cb = cb || function () {}; + + try { + Cache.clear(cb); + } catch (e) { + console.error(e); + cb(); + } }; var loginHandlers = []; LocalStore.loginReload = function () { diff --git a/www/common/outer/sharedfolder.js b/www/common/outer/sharedfolder.js index e34cd64df..3c1c8632d 100644 --- a/www/common/outer/sharedfolder.js +++ b/www/common/outer/sharedfolder.js @@ -2,12 +2,13 @@ define([ '/common/common-hash.js', '/common/common-util.js', '/common/userObject.js', + '/common/outer/cache-store.js', '/bower_components/nthen/index.js', '/bower_components/chainpad-crypto/crypto.js', '/bower_components/chainpad-listmap/chainpad-listmap.js', '/bower_components/chainpad/chainpad.dist.js', -], function (Hash, Util, UserObject, +], function (Hash, Util, UserObject, Cache, nThen, Crypto, Listmap, ChainPad) { var SF = {}; @@ -174,6 +175,7 @@ define([ ChainPad: ChainPad, classic: true, network: network, + Cache: Cache, metadata: { validateKey: secret.keys.validateKey || undefined, owners: owners diff --git a/www/common/outer/store-rpc.js b/www/common/outer/store-rpc.js index 6ac3e52b0..767448284 100644 --- a/www/common/outer/store-rpc.js +++ b/www/common/outer/store-rpc.js @@ -88,6 +88,7 @@ define([ CHANGE_PAD_PASSWORD_PIN: Store.changePadPasswordPin, GET_LAST_HASH: Store.getLastHash, GET_SNAPSHOT: Store.getSnapshot, + CORRUPTED_CACHE: Store.corruptedCache, // Drive DRIVE_USEROBJECT: Store.userObjectCommand, // Settings, diff --git a/www/common/outer/team.js b/www/common/outer/team.js index de9206511..f23fb8a18 100644 --- a/www/common/outer/team.js +++ b/www/common/outer/team.js @@ -12,6 +12,7 @@ define([ '/common/common-feedback.js', '/common/outer/invitation.js', '/common/cryptget.js', + '/common/outer/cache-store.js', '/bower_components/chainpad-listmap/chainpad-listmap.js', '/bower_components/chainpad-crypto/crypto.js', @@ -21,7 +22,7 @@ define([ '/bower_components/saferphore/index.js', '/bower_components/tweetnacl/nacl-fast.min.js', ], function (Util, Hash, Constants, Realtime, - ProxyManager, UserObject, SF, Roster, Messaging, Feedback, Invite, Crypt, + ProxyManager, UserObject, SF, Roster, Messaging, Feedback, Invite, Crypt, Cache, Listmap, Crypto, CpNetflux, ChainPad, nThen, Saferphore) { var Team = {}; @@ -57,11 +58,11 @@ define([ }); proxy.on('disconnect', function () { team.offline = true; - team.sendEvent('NETWORK_DISCONNECT'); + team.sendEvent('NETWORK_DISCONNECT', team.id); }); proxy.on('reconnect', function () { team.offline = false; - team.sendEvent('NETWORK_RECONNECT'); + team.sendEvent('NETWORK_RECONNECT', team.id); }); } proxy.on('change', [], function (o, n, p) { @@ -426,6 +427,7 @@ define([ channel: secret.channel, crypto: crypto, ChainPad: ChainPad, + Cache: Cache, metadata: { validateKey: secret.keys.validateKey || undefined, }, @@ -573,6 +575,7 @@ define([ logLevel: 1, classic: true, ChainPad: ChainPad, + Cache: Cache, owners: [ctx.store.proxy.edPublic] }; nThen(function (waitFor) { @@ -931,7 +934,9 @@ define([ if (!team) { return void cb ({error: 'ENOENT'}); } if (!team.roster) { return void cb({error: 'NO_ROSTER'}); } var state = team.roster.getState() || {}; - cb(state.metadata || {}); + var md = state.metadata || {}; + md.offline = team.offline; + cb(md); }; var setTeamMetadata = function (ctx, data, cId, cb) { @@ -1879,15 +1884,15 @@ define([ var t = Util.clone(teams); Object.keys(t).forEach(function (id) { // If failure to load the team, don't send it - if (ctx.teams[id]) { return; } + if (ctx.teams[id]) { + t[id].offline = ctx.teams[id].offline; + return; + } t[id].error = true; }); cb(t); }; team.execCommand = function (clientId, obj, cb) { - if (ctx.store.offline) { - return void cb({ error: 'OFFLINE' }); - } var cmd = obj.cmd; var data = obj.data; @@ -1911,30 +1916,36 @@ define([ return void setTeamMetadata(ctx, data, clientId, cb); } if (cmd === 'OFFER_OWNERSHIP') { + if (ctx.store.offline) { return void cb({ error: 'OFFLINE' }); } return void offerOwnership(ctx, data, clientId, cb); } if (cmd === 'ANSWER_OWNERSHIP') { + if (ctx.store.offline) { return void cb({ error: 'OFFLINE' }); } return void answerOwnership(ctx, data, clientId, cb); } if (cmd === 'DESCRIBE_USER') { return void describeUser(ctx, data, clientId, cb); } if (cmd === 'INVITE_TO_TEAM') { + if (ctx.store.offline) { return void cb({ error: 'OFFLINE' }); } return void inviteToTeam(ctx, data, clientId, cb); } if (cmd === 'LEAVE_TEAM') { return void leaveTeam(ctx, data, clientId, cb); } if (cmd === 'JOIN_TEAM') { + if (ctx.store.offline) { return void cb({ error: 'OFFLINE' }); } return void joinTeam(ctx, data, clientId, cb); } if (cmd === 'REMOVE_USER') { return void removeUser(ctx, data, clientId, cb); } if (cmd === 'DELETE_TEAM') { + if (ctx.store.offline) { return void cb({ error: 'OFFLINE' }); } return void deleteTeam(ctx, data, clientId, cb); } if (cmd === 'CREATE_TEAM') { + if (ctx.store.offline) { return void cb({ error: 'OFFLINE' }); } return void createTeam(ctx, data, clientId, cb); } if (cmd === 'GET_EDITABLE_FOLDERS') { @@ -1947,6 +1958,7 @@ define([ return void getPreviewContent(ctx, data, clientId, cb); } if (cmd === 'ACCEPT_LINK_INVITATION') { + if (ctx.store.offline) { return void cb({ error: 'OFFLINE' }); } return void acceptLinkInvitation(ctx, data, clientId, cb); } }; diff --git a/www/common/outer/upload.js b/www/common/outer/upload.js index 62f305612..0615ed7a7 100644 --- a/www/common/outer/upload.js +++ b/www/common/outer/upload.js @@ -1,9 +1,11 @@ define([ '/file/file-crypto.js', '/common/common-hash.js', + '/common/common-util.js', + '/common/outer/cache-store.js', '/bower_components/nthen/index.js', '/bower_components/tweetnacl/nacl-fast.min.js', -], function (FileCrypto, Hash, nThen) { +], function (FileCrypto, Hash, Util, Cache, nThen) { var Nacl = window.nacl; var module = {}; @@ -31,9 +33,11 @@ define([ }; var actual = 0; + var encryptedArr = []; var again = function (err, box) { if (err) { onError(err); } if (box) { + encryptedArr.push(box); actual += box.length; var progressValue = (actual / estimate * 100); progressValue = Math.min(progressValue, 100); @@ -55,9 +59,11 @@ define([ var uri = ['', 'blob', id.slice(0,2), id].join('/'); console.log("encrypted blob is now available as %s", uri); - - - cb(); + var box_u8 = Util.uint8ArrayJoin(encryptedArr); + Cache.setBlobCache(id, box_u8, function (err) { + if (err) { console.warn(err); } + cb(); + }); }); }; diff --git a/www/common/outer/worker-channel.js b/www/common/outer/worker-channel.js index 545ce435b..a359d8836 100644 --- a/www/common/outer/worker-channel.js +++ b/www/common/outer/worker-channel.js @@ -65,11 +65,13 @@ define([ cb(undefined, data.content, msg); }; evReady.reg(function () { - postMsg(JSON.stringify({ + var toSend = { txid: txid, content: content, - q: q - })); + q: q, + raw: opts.raw + }; + postMsg(opts.raw ? toSend : JSON.stringify(toSend)); }); }; @@ -84,12 +86,13 @@ define([ // If the type is a query, your handler will be invoked with a reply function that takes // one argument (the content to reply with). chan.on = function (queryType, handler, quiet) { - var h = function (data, msg) { + var h = function (data, msg, raw) { handler(data.content, function (replyContent) { - postMsg(JSON.stringify({ + var toSend = { txid: data.txid, content: replyContent - })); + }; + postMsg(raw ? toSend : JSON.stringify(toSend)); }, msg); }; (handlers[queryType] = handlers[queryType] || []).push(h); @@ -150,7 +153,7 @@ define([ onMsg.reg(function (msg) { if (!chanLoaded) { return; } if (!msg.data || msg.data === '_READY') { return; } - var data = JSON.parse(msg.data); + var data = typeof(msg.data) === "object" ? msg.data : JSON.parse(msg.data); if (typeof(data.ack) !== "undefined") { if (acks[data.txid]) { acks[data.txid](!data.ack); } } else if (typeof(data.q) === 'string') { @@ -163,7 +166,7 @@ define([ })); } handlers[data.q].forEach(function (f) { - f(data || JSON.parse(msg.data), msg); + f(data || JSON.parse(msg.data), msg, data && data.raw); data = undefined; }); } else { diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js index 88f4a052b..74bfcd890 100644 --- a/www/common/proxy-manager.js +++ b/www/common/proxy-manager.js @@ -40,6 +40,14 @@ define([ userObject: userObject, leave: leave }; + if (proxy.on) { + proxy.on('disconnect', function () { + Env.folders[id].offline = true; + }); + proxy.on('reconnect', function () { + Env.folders[id].online = true; + }); + } return userObject; }; diff --git a/www/common/sframe-app-framework.js b/www/common/sframe-app-framework.js index 5f28373c2..8776573d7 100644 --- a/www/common/sframe-app-framework.js +++ b/www/common/sframe-app-framework.js @@ -467,7 +467,51 @@ define([ }); }; + var noCache = false; // Prevent reload loops + var onCorruptedCache = function () { + if (noCache) { + UI.errorLoadingScreen(Messages.unableToDisplay, false, function () { + common.gotoURL(''); + }); + } + noCache = true; + var sframeChan = common.getSframeChannel(); + sframeChan.event("EV_CORRUPTED_CACHE"); + }; + var onCacheReady = function () { + stateChange(STATE.DISCONNECTED); + toolbar.offline(true); + var newContentStr = cpNfInner.chainpad.getUserDoc(); + if (toolbar) { + // Check if we have a new chainpad instance + toolbar.resetChainpad(cpNfInner.chainpad); + } + + // Invalid cache + if (newContentStr === '') { return void onCorruptedCache(); } + + var privateDat = cpNfInner.metadataMgr.getPrivateData(); + var type = privateDat.app; + + var newContent = JSON.parse(newContentStr); + var metadata = extractMetadata(newContent); + + // Make sure we're using the correct app for this cache + if (metadata && typeof(metadata.type) !== 'undefined' && metadata.type !== type) { + return void onCorruptedCache(); + } + + cpNfInner.metadataMgr.updateMetadata(metadata); + newContent = normalize(newContent); + if (!unsyncMode) { + contentUpdate(newContent, function () { return function () {}; }); + } + + UI.removeLoadingScreen(emitResize); + }; var onReady = function () { + toolbar.offline(false); + var newContentStr = cpNfInner.chainpad.getUserDoc(); if (state === STATE.DELETED) { return; } @@ -508,14 +552,19 @@ define([ console.log("Either this is an empty document which has not been touched"); console.log("Or else something is terribly wrong, reloading."); Feedback.send("NON_EMPTY_NEWDOC"); - setTimeout(function () { common.gotoURL(); }, 1000); + // The cache may be wrong, empty it and reload after. + waitFor.abort(); + onCorruptedCache(); return; } - console.log('updating title'); title.updateTitle(title.defaultTitle); evOnDefaultContentNeeded.fire(); } }).nThen(function () { + // We have a valid chainpad, reenable cache fix in case with reconnect with + // a corrupted cache + noCache = false; + stateChange(STATE.READY); firstConnection = false; @@ -734,6 +783,7 @@ define([ onRemote: onRemote, onLocal: onLocal, onInit: onInit, + onCacheReady: onCacheReady, onReady: function () { evStart.reg(onReady); }, onConnectionChange: onConnectionChange, onError: onError, diff --git a/www/common/sframe-chainpad-netflux-inner.js b/www/common/sframe-chainpad-netflux-inner.js index abb1cebdf..1bdcc260f 100644 --- a/www/common/sframe-chainpad-netflux-inner.js +++ b/www/common/sframe-chainpad-netflux-inner.js @@ -34,6 +34,7 @@ define([ var onLocal = config.onLocal || function () { }; var setMyID = config.setMyID || function () { }; var onReady = config.onReady || function () { }; + var onCacheReady = config.onCacheReady || function () { }; var onError = config.onError || function () { }; var userName = config.userName; var initialState = config.initialState; @@ -93,6 +94,9 @@ define([ evInfiniteSpinner.fire(); }, 2000); + sframeChan.on('EV_RT_CACHE_READY', function () { + onCacheReady({realtime: chainpad}); + }); sframeChan.on('EV_RT_DISCONNECT', function (isPermanent) { isReady = false; chainpad.abort(); diff --git a/www/common/sframe-chainpad-netflux-outer.js b/www/common/sframe-chainpad-netflux-outer.js index 72cfdef9a..f47dc812a 100644 --- a/www/common/sframe-chainpad-netflux-outer.js +++ b/www/common/sframe-chainpad-netflux-outer.js @@ -46,6 +46,7 @@ define([], function () { // shim between chainpad and netflux var msgIn = function (peer, msg) { try { + if (/^\[/.test(msg)) { return msg; } // Already decrypted var isHk = peer.length !== 32; var key = isNewHash ? validateKey : false; var decryptedMsg = Crypto.decrypt(msg, key, isHk); @@ -114,16 +115,25 @@ define([], function () { if (firstConnection) { firstConnection = false; // Add the handlers to the WebChannel - padRpc.onMessageEvent.reg(function (msg) { onMessage(msg); }); padRpc.onJoinEvent.reg(function (m) { sframeChan.event('EV_RT_JOIN', m); }); padRpc.onLeaveEvent.reg(function (m) { sframeChan.event('EV_RT_LEAVE', m); }); } }; + padRpc.onMessageEvent.reg(function (msg) { onMessage(msg); }); + padRpc.onDisconnectEvent.reg(function (permanent) { sframeChan.event('EV_RT_DISCONNECT', permanent); }); + padRpc.onCacheReadyEvent.reg(function () { + sframeChan.event('EV_RT_CACHE_READY'); + }); + + padRpc.onCacheEvent.reg(function () { + sframeChan.event('EV_RT_CACHE'); + }); + padRpc.onConnectEvent.reg(function (data) { onOpen(data); }); diff --git a/www/common/sframe-common-file.js b/www/common/sframe-common-file.js index 32ff11052..7b8601f13 100644 --- a/www/common/sframe-common-file.js +++ b/www/common/sframe-common-file.js @@ -48,7 +48,7 @@ define([ }; var tableHeader = h('div.cp-fileupload-header', [ - h('div.cp-fileupload-header-title', h('span', Messages.fileuploadHeader || 'Uploaded files')), + h('div.cp-fileupload-header-title', h('span', Messages.fileTableHeader)), h('div.cp-fileupload-header-close', h('span.fa.fa-times')), ]); @@ -260,7 +260,8 @@ define([ // name $('').append($link).appendTo($tr); // size - $('').text(UIElements.prettySize(estimate)).appendTo($tr); + var size = estimate ? UIElements.prettySize(estimate) : ''; + $(h('td.cp-fileupload-size')).text(size).appendTo($tr); // progress $('', {'class': 'cp-fileupload-table-progress'}).append($progressContainer).appendTo($tr); // cancel @@ -588,12 +589,11 @@ define([ queue.next(); }; - /* var cancelled = function () { $row.find('.cp-fileupload-table-cancel').addClass('cancelled').html('').append(h('span.fa.fa-minus')); queue.inProgress = false; queue.next(); - };*/ + }; /** * Update progress in the download panel, for downloading a file @@ -627,6 +627,17 @@ define([ */ var updateProgress = function (progressValue) { var text = Math.round(progressValue*100) + '%'; + if (Array.isArray(data.list)) { + text = Messages._getKey('download_zip_file', [Math.round(progressValue * data.list.length), data.list.length]); + } + if (progressValue === 2) { + text = Messages.download_zip; + progressValue = 1; + } + if (progressValue === 3) { + text = "100%"; + progressValue = 1; + } $pv.text(text); $pb.css({ width: (progressValue * 100) + '%' @@ -638,8 +649,10 @@ define([ fileHost: privateData.fileHost, get: common.getPad, sframeChan: sframeChan, + cache: common.getCache() }; - downloadFunction(ctx, data, function (err, obj) { + + var dl = downloadFunction(ctx, data, function (err, obj) { $link.prepend($('', {'class': 'fa fa-external-link'})) .attr('href', '#') .click(function (e) { @@ -655,19 +668,17 @@ define([ folderProgress: updateProgress, }); -/* - var $cancel = $('', {'class': 'cp-fileupload-table-cancel-button fa fa-times'}).click(function () { - dl.cancel(); - $cancel.remove(); - $row.find('.cp-fileupload-table-progress-value').text(Messages.upload_cancelled); - cancelled(); - }); -*/ - - $row.find('.cp-fileupload-table-cancel') - .html('') - .append(h('span.fa.fa-minus')); - //.append($cancel); + var $cancel = $row.find('.cp-fileupload-table-cancel').html(''); + if (dl && dl.cancel) { + $('', { + 'class': 'cp-fileupload-table-cancel-button fa fa-times' + }).click(function () { + dl.cancel(); + $cancel.remove(); + $row.find('.cp-fileupload-table-progress-value').text(Messages.upload_cancelled); + cancelled(); + }).appendTo($cancel); + } }; File.downloadFile = function (fData, cb) { diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index be8bae19f..a03f61730 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -100,12 +100,13 @@ define([ '/common/common-constants.js', '/common/common-feedback.js', '/common/outer/local-store.js', + '/common/outer/cache-store.js', '/customize/application_config.js', '/common/test.js', '/common/userObject.js', ], waitFor(function (_CpNfOuter, _Cryptpad, _Crypto, _Cryptget, _SFrameChannel, _SecureIframe, _Messaging, _Notifier, _Hash, _Util, _Realtime, - _Constants, _Feedback, _LocalStore, _AppConfig, _Test, _UserObject) { + _Constants, _Feedback, _LocalStore, _Cache, _AppConfig, _Test, _UserObject) { CpNfOuter = _CpNfOuter; Cryptpad = _Cryptpad; Crypto = Utils.Crypto = _Crypto; @@ -120,6 +121,7 @@ define([ Utils.Constants = _Constants; Utils.Feedback = _Feedback; Utils.LocalStore = _LocalStore; + Utils.Cache = _Cache; Utils.UserObject = _UserObject; Utils.currentPad = currentPad; AppConfig = _AppConfig; @@ -678,6 +680,20 @@ define([ }); }); + sframeChan.on('Q_GET_BLOB_CACHE', function (data, cb) { + Utils.Cache.getBlobCache(data.id, function (err, obj) { + if (err) { return void cb({error: err}); } + cb(obj); + }); + }); + sframeChan.on('Q_SET_BLOB_CACHE', function (data, cb) { + if (!data || !data.u8 || typeof(data.u8) !== "object") { return void cb({error: 'EINVAL'}); } + Utils.Cache.setBlobCache(data.id, data.u8, function (err) { + if (err) { return void cb({error: err}); } + cb(); + }); + }); + sframeChan.on('Q_GET_ATTRIBUTE', function (data, cb) { Cryptpad.getAttribute(data.key, function (e, data) { cb({ @@ -1697,6 +1713,10 @@ define([ }); }; + sframeChan.on('EV_CORRUPTED_CACHE', function () { + Cryptpad.onCorruptedCache(secret.channel); + }); + sframeChan.on('Q_CREATE_PAD', function (data, cb) { if (!isNewFile || rtStarted) { return; } // Create a new hash @@ -1811,7 +1831,12 @@ define([ } startRealtime(); cb(); - }, cryptputCfg); + }, cryptputCfg, function (progress) { + sframeChan.event('EV_LOADING_INFO', { + type: 'pad', + progress: progress + }); + }); return; } // Start realtime outside the iframe and callback diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index 76906e217..c665f2ec6 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -11,6 +11,7 @@ define([ '/common/sframe-common-codemirror.js', '/common/sframe-common-cursor.js', '/common/sframe-common-mailbox.js', + '/common/inner/cache.js', '/common/inner/common-mediatag.js', '/common/metadata-manager.js', @@ -36,6 +37,7 @@ define([ CodeMirror, Cursor, Mailbox, + Cache, MT, MetadataMgr, AppConfig, @@ -142,7 +144,7 @@ define([ } return; }; - funcs.importMediaTag = function ($mt) { + var getMtData = function ($mt) { if (!$mt || !$mt.is('media-tag')) { return; } var chanStr = $mt.attr('src'); var keyStr = $mt.attr('data-crypto-key'); @@ -154,10 +156,27 @@ define([ var channel = src.replace(/\/blob\/[0-9a-f]{2}\//i, ''); // Get key var key = keyStr.replace(/cryptpad:/i, ''); + return { + channel: channel, + key: key + }; + }; + funcs.getHashFromMediaTag = function ($mt) { + var data = getMtData($mt); + if (!data) { return; } + return Hash.getFileHashFromKeys({ + version: 1, + channel: data.channel, + keys: { fileKeyStr: data.key } + }); + }; + funcs.importMediaTag = function ($mt) { + var data = getMtData($mt); + if (!data) { return; } var metadata = $mt[0]._mediaObject._blob.metadata; ctx.sframeChan.query('Q_IMPORT_MEDIATAG', { - channel: channel, - key: key, + channel: data.channel, + key: data.key, name: metadata.name, type: metadata.type, owners: metadata.owners @@ -588,6 +607,10 @@ define([ }); }; + funcs.getCache = function () { + return ctx.cache; + }; + /* funcs.storeLinkToClipboard = function (readOnly, cb) { ctx.sframeChan.query('Q_STORE_LINK_TO_CLIPBOARD', readOnly, function (err) { if (cb) { cb(err); } @@ -794,12 +817,24 @@ define([ modules[type].onEvent(obj.data); }); + ctx.cache = Cache.create(ctx.sframeChan); + ctx.metadataMgr.onReady(waitFor()); }).nThen(function () { var privateData = ctx.metadataMgr.getPrivateData(); funcs.addShortcuts(window, Boolean(privateData.app)); + var mt = Util.find(privateData, ['settings', 'general', 'mediatag-size']); + if (MT.MediaTag && typeof(mt) === "number") { + var maxMtSize = mt === -1 ? Infinity : mt * 1024 * 1024; + MT.MediaTag.setDefaultConfig('maxDownloadSize', maxMtSize); + } + + if (MT.MediaTag && ctx.cache) { + MT.MediaTag.setDefaultConfig('Cache', ctx.cache); + } + try { var feedback = privateData.feedbackAllowed; Feedback.init(feedback); diff --git a/www/common/toolbar.js b/www/common/toolbar.js index 4b72187ed..20cac8337 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -1388,6 +1388,18 @@ MessengerUI, Messages) { } }; + toolbar.offline = function (bool) { + toolbar.connected = !bool; // Can't edit title + toolbar.history = bool; // Stop "Initializing" state + toolbar.isErrorState = bool; // Stop kickSpinner + toolbar.title.toggleClass('cp-toolbar-unsync', bool); // "read only" next to the title + if (bool && toolbar.spinner) { + toolbar.spinner.text(Messages.offline); + } else { + kickSpinner(toolbar, config); + } + }; + // On log out, remove permanently the realtime elements of the toolbar Common.onLogout(function () { failed(); diff --git a/www/debug/main.js b/www/debug/main.js index b901beec0..af3d78a57 100644 --- a/www/debug/main.js +++ b/www/debug/main.js @@ -28,6 +28,8 @@ define([ // Loaded in load #2 nThen(function (waitFor) { $(waitFor()); + }).nThen(function (waitFor) { + SFCommonO.initIframe(waitFor); }).nThen(function (waitFor) { var req = { cfg: requireConfig, diff --git a/www/drive/inner.js b/www/drive/inner.js index 2be9e06e6..b9f91cd70 100644 --- a/www/drive/inner.js +++ b/www/drive/inner.js @@ -82,6 +82,8 @@ define([ var readOnly = !secret.keys.editKeyStr; if (!manager || !manager.folders[fId]) { return; } manager.folders[fId].userObject.setReadOnly(readOnly, secret.keys.secondaryKey); + + manager.folders[fId].offline = newObj.offline; })); }); // Remove from memory folders that have been deleted from the drive remotely diff --git a/www/drive/main.js b/www/drive/main.js index ea6345b02..0f2408f5b 100644 --- a/www/drive/main.js +++ b/www/drive/main.js @@ -54,7 +54,13 @@ define([ if (Utils.LocalStore.isLoggedIn()) { return; } Utils.LocalStore.setFSHash(''); Utils.LocalStore.clearThumbnail(); - window.location.reload(); + try { + Utils.Cache.clear(function () { + window.location.reload(); + }); + } catch (e) { + window.location.reload(); + } }); sframeChan.on('Q_DRIVE_USEROBJECT', function (data, cb) { Cryptpad.userObjectCommand(data, cb); diff --git a/www/file/app-file.less b/www/file/app-file.less index 4ac94ca33..be7e067f1 100644 --- a/www/file/app-file.less +++ b/www/file/app-file.less @@ -1,5 +1,6 @@ @import (reference) '../../customize/src/less2/include/tokenfield.less'; @import (reference) '../../customize/src/less2/include/framework.less'; +@import (reference) '../../customize/src/less2/include/markdown.less'; &.cp-app-file { @@ -47,6 +48,7 @@ z-index: -1; } + .mediatag_cryptpad(); media-tag { img { max-width: 100%; @@ -64,7 +66,49 @@ } } - #cp-app-file-upload-form, #cp-app-file-download-form { + #cp-app-file-download-form { + padding: 0px; + margin: 0px; + + position: relative; + display: block; + max-width: 90vw; + height: 150px; + width: ~"min(90vw, 600px)"; + .cp-app-file-progress-container { + margin-top: 5px; + height: 40px; + font-size: 20px; + border: 1px solid @colortheme_logo-2; + background: white; + color: @cryptpad_text_col; + display: flex; + justify-content: space-between; + position: relative; + .cp-app-file-progress-dl { + border-right: 1px solid @cryptpad_text_col; + } + .cp-app-file-progress-dl, .cp-app-file-progress-dc { + width: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + z-index: 2; + } + .cp-app-file-progress { + z-index: 1; + position: absolute; + top: 0; + left: 0; + bottom: 0; + background: @colortheme_logo-2; + } + } + .cp-app-file-progress-txt { + margin-left: 30px; + } + } + #cp-app-file-upload-form { padding: 0px; margin: 0px; @@ -156,7 +200,10 @@ max-height: 100%; max-width: 100%; } + &:empty { + display: none !important; + } } } -} \ No newline at end of file +} diff --git a/www/file/file-crypto.js b/www/file/file-crypto.js index a74439492..6a0c08816 100644 --- a/www/file/file-crypto.js +++ b/www/file/file-crypto.js @@ -48,69 +48,6 @@ define([ return new Blob(chunks); }; - var concatBuffer = function (a, b) { // TODO make this not so ugly - return new Uint8Array(slice(a).concat(slice(b))); - }; - - var fetchMetadata = function (src, cb) { - var done = false; - var CB = function (err, res) { - if (done) { return; } - done = true; - cb(err, res); - }; - - var xhr = new XMLHttpRequest(); - xhr.open("GET", src, true); - xhr.setRequestHeader('Range', 'bytes=0-1'); - xhr.responseType = 'arraybuffer'; - - xhr.onerror= function () { return CB('XHR_ERROR'); }; - xhr.onload = function () { - if (/^4/.test('' + this.status)) { return CB('XHR_ERROR'); } - var res = new Uint8Array(xhr.response); - var size = decodePrefix(res); - var xhr2 = new XMLHttpRequest(); - - xhr2.open("GET", src, true); - xhr2.setRequestHeader('Range', 'bytes=2-' + (size + 2)); - xhr2.responseType = 'arraybuffer'; - xhr2.onload = function () { - if (/^4/.test('' + this.status)) { return CB('XHR_ERROR'); } - var res2 = new Uint8Array(xhr2.response); - var all = concatBuffer(res, res2); - CB(void 0, all); - }; - xhr2.send(null); - }; - xhr.send(null); - }; - - var decryptMetadata = function (u8, key) { - var prefix = u8.subarray(0, 2); - var metadataLength = decodePrefix(prefix); - - var metaBox = new Uint8Array(u8.subarray(2, 2 + metadataLength)); - var metaChunk = Nacl.secretbox.open(metaBox, createNonce(), key); - - try { - return JSON.parse(Nacl.util.encodeUTF8(metaChunk)); - } - catch (e) { return null; } - }; - - var fetchDecryptedMetadata = function (src, key, cb) { - if (typeof(src) !== 'string') { - return window.setTimeout(function () { - cb('NO_SOURCE'); - }); - } - fetchMetadata(src, function (e, buffer) { - if (e) { return cb(e); } - cb(void 0, decryptMetadata(buffer, key)); - }); - }; - var decrypt = function (u8, key, done, progress) { var MAX = u8.length; var _progress = function (offset) { @@ -128,6 +65,11 @@ define([ metadata: undefined, }; + var cancelled = false; + var cancel = function () { + cancelled = true; + }; + var metaBox = new Uint8Array(u8.subarray(2, 2 + metadataLength)); var metaChunk = Nacl.secretbox.open(metaBox, nonce, key); @@ -168,6 +110,7 @@ define([ var chunks = []; var again = function () { + if (cancelled) { return; } takeChunk(function (e, plaintext) { if (e) { return setTimeout(function () { @@ -188,6 +131,10 @@ define([ }; again(); + + return { + cancel: cancel + }; }; // metadata @@ -258,8 +205,5 @@ define([ encrypt: encrypt, joinChunks: joinChunks, computeEncryptedSize: computeEncryptedSize, - decryptMetadata: decryptMetadata, - fetchMetadata: fetchMetadata, - fetchDecryptedMetadata: fetchDecryptedMetadata, }; }); diff --git a/www/file/inner.html b/www/file/inner.html index d474ee117..45ba831fe 100644 --- a/www/file/inner.html +++ b/www/file/inner.html @@ -16,11 +16,6 @@
- diff --git a/www/file/inner.js b/www/file/inner.js index b2aef53ed..59291c964 100644 --- a/www/file/inner.js +++ b/www/file/inner.js @@ -8,6 +8,7 @@ define([ '/common/common-util.js', '/common/common-hash.js', '/common/common-interface.js', + '/common/hyperscript.js', '/customize/messages.js', '/file/file-crypto.js', @@ -29,6 +30,7 @@ define([ Util, Hash, UI, + h, Messages, FileCrypto, MediaTag) @@ -37,18 +39,12 @@ define([ var Nacl = window.nacl; var APP = window.APP = {}; - MediaTag.setDefaultConfig('download', { - text: Messages.download_mt_button - }); var andThen = function (common) { var $appContainer = $('#cp-app-file-content'); var $form = $('#cp-app-file-upload-form'); - var $dlform = $('#cp-app-file-download-form'); var $dlview = $('#cp-app-file-download-view'); var $label = $form.find('label'); - var $dllabel = $dlform.find('label span'); - var $progress = $('#cp-app-file-dlprogress'); var $bar = $('.cp-toolbar-container'); var $body = $('body'); @@ -88,142 +84,86 @@ define([ var toolbar = APP.toolbar = Toolbar.create(configTb); if (!uploadMode) { - var hexFileName = secret.channel; - var src = fileHost + Hash.getBlobPathFromHex(hexFileName); - var key = secret.keys && secret.keys.cryptKey; - var cryptKey = Nacl.util.encodeBase64(key); - - FileCrypto.fetchDecryptedMetadata(src, key, function (e, metadata) { - if (e) { - if (e === 'XHR_ERROR') { - return void UI.errorLoadingScreen(Messages.download_resourceNotAvailable, false, function () { - common.gotoURL('/file/'); - }); + (function () { + var hexFileName = secret.channel; + var src = fileHost + Hash.getBlobPathFromHex(hexFileName); + var key = secret.keys && secret.keys.cryptKey; + var cryptKey = Nacl.util.encodeBase64(key); + + var $mt = $dlview.find('media-tag'); + $mt.attr('src', src); + $mt.attr('data-crypto-key', 'cryptpad:'+cryptKey); + $mt.css('transform', 'scale(2)'); + + var rightsideDisplayed = false; + var metadataReceived = false; + UI.removeLoadingScreen(); + $dlview.show(); + + MediaTag($mt[0]).on('complete', function (decrypted) { + $mt.css('transform', ''); + if (!rightsideDisplayed) { + toolbar.$drawer + .append(common.createButton('export', true, {}, function () { + saveAs(decrypted.content, decrypted.metadata.name); + })); + rightsideDisplayed = true; } - return void console.error(e); - } - - // Add pad attributes when the file is saved in the drive - Title.onTitleChange(function () { - var owners = metadata.owners; - if (owners) { common.setPadAttribute('owners', owners); } - common.setPadAttribute('fileType', metadata.type); - }); - $(document).on('cpPadStored', function () { - var owners = metadata.owners; - if (owners) { common.setPadAttribute('owners', owners); } - common.setPadAttribute('fileType', metadata.type); - }); - - // Save to the drive or update the acces time - var title = document.title = metadata.name; - Title.updateTitle(title || Title.defaultTitle); - - var owners = metadata.owners; - if (owners) { - common.setPadAttribute('owners', owners); - } - if (metadata.type) { - common.setPadAttribute('fileType', metadata.type); - } - - toolbar.addElement(['pageTitle'], { - pageTitle: title, - title: Title.getTitleConfig(), - }); - toolbar.$drawer.append(common.createButton('forget', true)); - toolbar.$drawer.append(common.createButton('properties', true)); - if (common.isLoggedIn()) { - toolbar.$drawer.append(common.createButton('hashtag', true)); - } - toolbar.$file.show(); - - var displayFile = function (ev, sizeMb, CB) { - var called_back; - var cb = function (e) { - if (called_back) { return; } - called_back = true; - if (CB) { CB(e); } - }; - - var $mt = $dlview.find('media-tag'); - $mt.attr('src', src); - $mt.attr('data-crypto-key', 'cryptpad:'+cryptKey); - var rightsideDisplayed = false; - - MediaTag($mt[0]).on('complete', function (decrypted) { - $dlview.show(); - $dlform.hide(); - var $dlButton = $dlview.find('media-tag button'); - if (ev) { $dlButton.click(); } - - if (!rightsideDisplayed) { - toolbar.$drawer - .append(common.createButton('export', true, {}, function () { - saveAs(decrypted.content, decrypted.metadata.name); - })); - rightsideDisplayed = true; - } - - // make pdfs big - var toolbarHeight = $('#cp-toolbar').height(); - var $another_iframe = $('media-tag iframe').css({ - 'height': 'calc(100vh - ' + toolbarHeight + 'px)', - 'width': '100vw', - 'position': 'absolute', - 'bottom': 0, - 'left': 0, - 'border': 0 - }); - - if ($another_iframe.length) { - $another_iframe.load(function () { - cb(); - }); - } else { - cb(); - } - }).on('progress', function (data) { - var p = data.progress +'%'; - $progress.width(p); - }).on('error', function (err) { - console.error(err); + // make pdfs big + var toolbarHeight = $('#cp-toolbar').height(); + $('media-tag iframe').css({ + 'height': 'calc(100vh - ' + toolbarHeight + 'px)', + 'width': '100vw', + 'position': 'absolute', + 'bottom': 0, + 'left': 0, + 'border': 0 + }); + }).on('metadata', function (metadata) { + if (metadataReceived) { return; } + metadataReceived = true; + // Add pad attributes when the file is saved in the drive + Title.onTitleChange(function () { + var owners = metadata.owners; + if (owners) { common.setPadAttribute('owners', owners); } + common.setPadAttribute('fileType', metadata.type); + }); + $(document).on('cpPadStored', function () { + var owners = metadata.owners; + if (owners) { common.setPadAttribute('owners', owners); } + common.setPadAttribute('fileType', metadata.type); }); - }; - var todoBigFile = function (sizeMb) { - $dlform.show(); - UI.removeLoadingScreen(); - $dllabel.append($('
')); - $dllabel.append(Util.fixHTML(metadata.name)); + // Save to the drive or update the acces time + var title = document.title = metadata.name; + Title.updateTitle(title || Title.defaultTitle); - // don't display the size if you don't know it. - if (typeof(sizeM) === 'number') { - $dllabel.append($('
')); - $dllabel.append(Messages._getKey('formattedMB', [sizeMb])); + var owners = metadata.owners; + if (owners) { + common.setPadAttribute('owners', owners); } - var decrypting = false; - var onClick = function (ev) { - if (decrypting) { return; } - decrypting = true; - displayFile(ev, sizeMb, function (err) { - $appContainer.css('background-color', - common.getAppConfig().appBackgroundColor); - if (err) { UI.alert(err); } - }); - }; - if (typeof(sizeMb) === 'number' && sizeMb < 5) { return void onClick(); } - $dlform.find('#cp-app-file-dlfile, #cp-app-file-dlprogress').click(onClick); - }; - common.getFileSize(hexFileName, function (e, data) { - if (e) { - return void UI.errorLoadingScreen(e); + if (metadata.type) { + common.setPadAttribute('fileType', metadata.type); } - var size = Util.bytesToMegabytes(data); - return void todoBigFile(size); + + toolbar.addElement(['pageTitle'], { + pageTitle: title, + title: Title.getTitleConfig(), + }); + toolbar.$drawer.append(common.createButton('forget', true)); + toolbar.$drawer.append(common.createButton('properties', true)); + if (common.isLoggedIn()) { + toolbar.$drawer.append(common.createButton('hashtag', true)); + } + toolbar.$file.show(); + }).on('error', function (err) { + $appContainer.css('background-color', + common.getAppConfig().appBackgroundColor); + UI.warn(Messages.error); + console.error(err); }); - }); + })(); return; } diff --git a/www/pad/inner.js b/www/pad/inner.js index 235b4fafa..4968cf4cd 100644 --- a/www/pad/inner.js +++ b/www/pad/inner.js @@ -46,6 +46,7 @@ define([ '/common/test.js', '/bower_components/diff-dom/diffDOM.js', + '/bower_components/file-saver/FileSaver.min.js', 'css!/customize/src/print.css', 'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', @@ -462,7 +463,9 @@ define([ setTimeout(function() { // Just in case var tags = dom.querySelectorAll('media-tag:empty'); Array.prototype.slice.call(tags).forEach(function(el) { - MediaTag(el); + var mediaObject = MediaTag(el, { + body: dom + }); $(el).on('keydown', function(e) { if ([8, 46].indexOf(e.which) !== -1) { $(el).remove(); @@ -472,13 +475,17 @@ define([ var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.type === 'childList') { - var list_values = [].slice.call(el.children); + var list_values = slice(el.children) + .map(function (el) { return el.outerHTML; }) + .join(''); mediaTagMap[el.getAttribute('src')] = list_values; + if (mediaObject.complete) { observer.disconnect(); } } }); }); observer.observe(el, { attributes: false, + subtree: true, childList: true, characterData: false }); @@ -491,9 +498,10 @@ define([ Array.prototype.slice.call(tags).forEach(function(tag) { var src = tag.getAttribute('src'); if (mediaTagMap[src]) { - mediaTagMap[src].forEach(function(n) { - tag.appendChild(n.cloneNode()); - }); + tag.innerHTML = mediaTagMap[src]; + /*mediaTagMap[src].forEach(function(n) { + tag.appendChild(n.cloneNode(true)); + });*/ } }); }; @@ -1084,6 +1092,9 @@ define([ border: Messages.pad_mediatagBorder, preview: Messages.pad_mediatagPreview, 'import': Messages.pad_mediatagImport, + download: Messages.download_mt_button, + share: Messages.pad_mediatagShare, + open: Messages.pad_mediatagOpen, options: Messages.pad_mediatagOptions }; Ckeditor._commentsTranslations = { @@ -1164,6 +1175,28 @@ define([ editor.plugins.mediatag.import = function($mt) { framework._.sfCommon.importMediaTag($mt); }; + editor.plugins.mediatag.download = function($mt) { + var media = Util.find($mt, [0, '_mediaObject']); + if (!media) { return void console.error('no media'); } + if (!media.complete) { return void UI.warn(Messages.mediatag_notReady); } + if (!(media && media._blob)) { return void console.error($mt); } + window.saveAs(media._blob.content, media.name); + }; + editor.plugins.mediatag.open = function($mt) { + var hash = framework._.sfCommon.getHashFromMediaTag($mt); + framework._.sfCommon.openURL(Hash.hashToHref(hash, 'file')); + }; + editor.plugins.mediatag.share = function($mt) { + var data = { + file: true, + pathname: '/file/', + hashes: { + fileHash: framework._.sfCommon.getHashFromMediaTag($mt) + }, + title: Util.find($mt[0], ['_mediaObject', 'name']) || '' + }; + framework._.sfCommon.getSframeChannel().event('EV_SHARE_OPEN', data); + }; Links.init(Ckeditor, editor); }).nThen(function() { // Move ckeditor parts to have a structure like the other apps diff --git a/www/pad/mediatag-plugin.js b/www/pad/mediatag-plugin.js index 46747b18a..22a732748 100644 --- a/www/pad/mediatag-plugin.js +++ b/www/pad/mediatag-plugin.js @@ -53,15 +53,57 @@ editor.plugins.mediatag.import($mt); } }); + editor.addCommand('downloadMT', { + exec: function (editor) { + var w = targetWidget; + targetWidget = undefined; + var $mt = $(w.$).find('media-tag'); + editor.plugins.mediatag.download($mt); + } + }); + editor.addCommand('openMT', { + exec: function (editor) { + var w = targetWidget; + targetWidget = undefined; + var $mt = $(w.$).find('media-tag'); + editor.plugins.mediatag.open($mt); + } + }); + editor.addCommand('shareMT', { + exec: function (editor) { + var w = targetWidget; + targetWidget = undefined; + var $mt = $(w.$).find('media-tag'); + editor.plugins.mediatag.share($mt); + } + }); if (editor.addMenuItems) { editor.addMenuGroup('mediatag'); + editor.addMenuItem('open', { + label: Messages.open, + icon: 'iframe', + command: 'openMT', + group: 'mediatag' + }); + editor.addMenuItem('share', { + label: Messages.share, + icon: 'link', + command: 'shareMT', + group: 'mediatag' + }); editor.addMenuItem('importMediatag', { label: Messages.import, icon: 'save', command: 'importMediatag', group: 'mediatag' }); + editor.addMenuItem('download', { + label: Messages.download, + icon: 'save', + command: 'downloadMT', + group: 'mediatag' + }); editor.addMenuItem('mediatag', { label: Messages.options, icon: 'image', @@ -76,6 +118,9 @@ targetWidget = element; return { mediatag: CKEDITOR.TRISTATE_OFF, + open: CKEDITOR.TRISTATE_OFF, + share: CKEDITOR.TRISTATE_OFF, + download: CKEDITOR.TRISTATE_OFF, importMediatag: CKEDITOR.TRISTATE_OFF, }; } diff --git a/www/secureiframe/inner.js b/www/secureiframe/inner.js index c20d53a80..4d5b8ee4f 100644 --- a/www/secureiframe/inner.js +++ b/www/secureiframe/inner.js @@ -52,10 +52,10 @@ define([ : Share.getShareModal; f(common, { origin: priv.origin, - pathname: priv.pathname, - password: priv.password, - isTemplate: priv.isTemplate, - hashes: priv.hashes, + pathname: data.pathname || priv.pathname, + password: data.hashes ? '' : priv.password, + isTemplate: data.hashes ? false : priv.isTemplate, + hashes: data.hashes || priv.hashes, common: common, title: data.title, versionHash: data.versionHash, @@ -64,8 +64,8 @@ define([ hideIframe(); }, fileData: { - hash: priv.hashes.fileHash, - password: priv.password + hash: (data.hashes && data.hashes.fileHash) || priv.hashes.fileHash, + password: data.hashes ? '' : priv.password } }, function (e, modal) { if (e) { console.error(e); } diff --git a/www/secureiframe/main.js b/www/secureiframe/main.js index 1bf5e3e7b..62175c5a0 100644 --- a/www/secureiframe/main.js +++ b/www/secureiframe/main.js @@ -33,7 +33,7 @@ define([ // loading screen setup. var done = waitFor(); var onMsg = function (msg) { - var data = JSON.parse(msg.data); + var data = typeof(msg.data) === "object" ? msg.data : JSON.parse(msg.data); if (data.q !== 'READY') { return; } window.removeEventListener('message', onMsg); var _done = done; diff --git a/www/settings/app-settings.less b/www/settings/app-settings.less index 1f0da1263..0ac3d10f2 100644 --- a/www/settings/app-settings.less +++ b/www/settings/app-settings.less @@ -74,6 +74,10 @@ margin-right: 100%; } } + & > .fa { + align-self: center; + margin-right: -16px; + } } .cp-settings-info-block { [type="text"] { diff --git a/www/settings/inner.js b/www/settings/inner.js index 8882f5861..8b5095cba 100644 --- a/www/settings/inner.js +++ b/www/settings/inner.js @@ -51,7 +51,7 @@ define([ 'cp-settings-info-block', 'cp-settings-displayname', 'cp-settings-language-selector', - 'cp-settings-resettips', + 'cp-settings-mediatag-size', 'cp-settings-change-password', 'cp-settings-delete' ], @@ -62,6 +62,7 @@ define([ 'cp-settings-userfeedback', ], 'drive': [ + 'cp-settings-resettips', 'cp-settings-drive-duplicate', 'cp-settings-thumbnails', 'cp-settings-drive-backup', @@ -576,6 +577,57 @@ define([ cb(form); }, true); + makeBlock('mediatag-size', function(cb) { + var $inputBlock = $('
', { + 'class': 'cp-sidebarlayout-input-block', + }); + + var spinner; + var $input = $('', { + 'min': -1, + 'max': 1000, + type: 'number', + }).appendTo($inputBlock); + + var oldVal; + + var todo = function () { + var val = parseInt($input.val()); + if (val === oldVal) { return; } + if (typeof(val) !== 'number') { return UI.warn(Messages.error); } + spinner.spin(); + common.setAttribute(['general', 'mediatag-size'], val, function (err) { + if (err) { + spinner.hide(); + console.error(err); + return UI.warn(Messages.error); + } + spinner.done(); + UI.log(Messages.saved); + }); + }; + var $save = $(h('button.btn.btn-primary', Messages.settings_save)).appendTo($inputBlock); + spinner = UI.makeSpinner($inputBlock); + + $save.click(todo); + $input.on('keyup', function(e) { + if (e.which === 13) { todo(); } + }); + + common.getAttribute(['general', 'mediatag-size'], function(e, val) { + if (e) { return void console.error(e); } + if (typeof(val) !== 'number') { + oldVal = 5; + $input.val(5); + } else { + oldVal = val; + $input.val(val); + } + }); + + cb($inputBlock); + }, true); + // Security makeBlock('safe-links', function(cb) { @@ -777,7 +829,7 @@ define([ Feedback.send('FULL_DRIVE_EXPORT_COMPLETE'); saveAs(blob, filename); }, errors); - }, ui.update); + }, ui.update, common.getCache()); ui.onCancel(function() { ui.close(); bu.stop(); diff --git a/www/support/ui.js b/www/support/ui.js index 9b86d6f20..a58fdf170 100644 --- a/www/support/ui.js +++ b/www/support/ui.js @@ -397,6 +397,7 @@ define([ var fmConfig = { body: $('body'), + noStore: true, // Don't store attachments into our drive onUploaded: function (ev, data) { if (ev.callback) { ev.callback(data); diff --git a/www/teams/inner.js b/www/teams/inner.js index a75ed94db..7af3d7afc 100644 --- a/www/teams/inner.js +++ b/www/teams/inner.js @@ -46,7 +46,9 @@ define([ Backup, Messages) { - var APP = {}; + var APP = { + teams: {} + }; var driveAPP = {}; var saveAs = window.saveAs; //var SHARED_FOLDER_NAME = Messages.fm_sharedFolderName; @@ -91,6 +93,8 @@ define([ var readOnly = !secret.keys.editKeyStr; if (!manager || !manager.folders[fId]) { return; } manager.folders[fId].userObject.setReadOnly(readOnly, secret.keys.secondaryKey); + + manager.folders[fId].offline = newObj.offline; })); }); // Remove from memory folders that have been deleted from the drive remotely @@ -211,6 +215,11 @@ define([ if (obj && obj.error) { return void UI.warn(Messages.error); } + + // Refresh offline state + APP.teams[APP.team] = APP.teams[APP.team] || {}; + APP.teams[APP.team].offline = obj.offline; + common.displayAvatar($avatar, obj.avatar, obj.name); $category.append($avatar); $avatar.append(h('span.cp-sidebarlayout-category-name', obj.name)); @@ -333,6 +342,11 @@ define([ }); APP.drive = drive; driveAPP.refresh = drive.refresh; + + if (APP.teams[id] && APP.teams[id].offline) { + setEditable(false); + drive.refresh(); + } }); }; @@ -406,7 +420,15 @@ define([ content.push(h('h3', Messages.team_listTitle + ' ' + slots)); + APP.teams = {}; + keys.forEach(function (id) { + if (!obj[id].empty) { + APP.teams[id] = { + offline: obj[id] && obj[id].offline + }; + } + var team = obj[id]; if (team.empty) { list.push(h('div.cp-team-list-team.empty', [ @@ -1049,7 +1071,7 @@ define([ Feedback.send('FULL_TEAMDRIVE_EXPORT_COMPLETE'); saveAs(blob, filename); }, errors); - }, ui.update); + }, ui.update, common.getCache); ui.onCancel(function() { ui.close(); bu.stop(); @@ -1433,13 +1455,15 @@ define([ } }); - var onDisconnect = function (noAlert) { + var onDisconnect = function (teamId) { + if (APP.team && teamId && APP.team !== teamId) { return; } setEditable(false); if (APP.team && driveAPP.refresh) { driveAPP.refresh(); } toolbar.failed(); - if (!noAlert) { UIElements.disconnectAlert(); } + UIElements.disconnectAlert(); }; - var onReconnect = function () { + var onReconnect = function (teamId) { + if (APP.team && teamId && APP.team !== teamId) { return; } setEditable(true); if (APP.team && driveAPP.refresh) { driveAPP.refresh(); } toolbar.reconnecting(); @@ -1449,11 +1473,17 @@ define([ sframeChan.on('EV_DRIVE_LOG', function (msg) { UI.log(msg); }); - sframeChan.on('EV_NETWORK_DISCONNECT', function () { - onDisconnect(); + sframeChan.on('EV_NETWORK_DISCONNECT', function (teamId) { + onDisconnect(teamId); + if (teamId && APP.teams[teamId]) { + APP.teams[teamId].offline = true; + } }); - sframeChan.on('EV_NETWORK_RECONNECT', function () { - onReconnect(); + sframeChan.on('EV_NETWORK_RECONNECT', function (teamId) { + onReconnect(teamId); + if (teamId && APP.teams[teamId]) { + APP.teams[teamId].offline = false; + } }); common.onLogout(function () { setEditable(false); }); }); diff --git a/www/teams/main.js b/www/teams/main.js index 044419dfc..a9a28267b 100644 --- a/www/teams/main.js +++ b/www/teams/main.js @@ -55,10 +55,10 @@ define([ sframeChan.event('EV_'+obj.data.ev, obj.data.data); } if (obj.data.ev === 'NETWORK_RECONNECT') { - sframeChan.event('EV_NETWORK_RECONNECT'); + sframeChan.event('EV_NETWORK_RECONNECT', obj.data.data); } if (obj.data.ev === 'NETWORK_DISCONNECT') { - sframeChan.event('EV_NETWORK_DISCONNECT'); + sframeChan.event('EV_NETWORK_DISCONNECT', obj.data.data); } }); diff --git a/www/whiteboard/inner.js b/www/whiteboard/inner.js index 44efd1421..30c43aef1 100644 --- a/www/whiteboard/inner.js +++ b/www/whiteboard/inner.js @@ -331,10 +331,11 @@ define([ APP.FM.handleFile(blob); }); }; + var MAX_IMAGE_SIZE = 1 * 1024 * 1024; // 1 MB + var maxSizeStr = Messages._getKey('formattedMB', [Util.bytesToMegabytes(MAX_IMAGE_SIZE)]); var addImageToCanvas = function (img) { - // 1 MB maximum - if (img.src && img.src.length > 1 * 1024 * 1024) { - UI.warn(Messages.upload_tooLargeBrief); + if (img.src && img.src.length > MAX_IMAGE_SIZE) { + UI.warn(Messages._getKey('upload_tooLargeBrief', [maxSizeStr])); return; } var w = img.width; @@ -356,8 +357,8 @@ define([ var file = e.target.files[0]; var reader = new FileReader(); // 1 MB maximum - if (file.size > 1 * 1024 * 1024) { - UI.warn(Messages.upload_tooLargeBrief); + if (file.size > MAX_IMAGE_SIZE) { + UI.warn(Messages._getKey('upload_tooLargeBrief', [maxSizeStr])); return; } reader.onload = function () {