';
types.forEach(function (el, i) {
- if (i >= 6) { return; }
+ if (el === "end") { return; }
list += getLi(i);
});
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
$('