diff --git a/.jshintignore b/.jshintignore index 45b4087f5..c403b39b3 100644 --- a/.jshintignore +++ b/.jshintignore @@ -5,7 +5,7 @@ www/common/tippy/ www/common/jquery-ui/ server.js -www/common/media-tag.js +www/common/old-media-tag.js www/scratch www/common/toolbar.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 598ff5c35..2b3b8c058 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,30 @@ +# Coati release (v2.2.0) + +## Goals + +For this release we wanted to continue our efforts towards improving CryptPad usability. We've also added a new Kanban application which was in its final stage for quite some time. + +## What's new + +### Features + +* We've added a new kanban application! + * You can create boards, add items to those boards and move items from one board to another. + * It includes almost all the features seen in the other apps: templates, password protection, history, read-only, etc. + * Kanban can be shared and used collaboratively. + * This new app was prototyped by @ldubost, and based on [jkanban](https://github.com/riktar/jkanban) by @riktar +* We've improved our tagging feature. + * When you want to add tags to a pad, you will see suggestions based on the tags you've already used + * There is a new *Tags* category in CryptDrive for logged in users. It shows all the tags you've used in your pads and their number of use. +* In the Poll application, the line where your cursor is located will be highlighted so that you can see easily which option you're looking at. + +### Bug fixes + +* We've fixed two interface bugs in the Share menu which made it difficult to change the access rights for the link (edit or read-only) in some cases. +* A bug introduced in the previous version prevented loading of the drive if it contained some content from an alpha version of CryptPad. +* Some parts of our UI were using CSS values not supported by all browsers. +* Some pads created more than one year ago were not loading properly. + # Badger release (v2.1.0) ## Goals diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index 8251147dd..7e8ee960d 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -1152,7 +1152,7 @@ define(function () { out.creation_newPadModalAdvanced = "Display the pad creation screen"; // Password prompt on the loadind screen - out.password_info = "The pad you're tyring to open is protected with a password. Enter the correct password to access its content."; + out.password_info = "The pad you're trying to open is protected with a password. Enter the correct password to access its content."; out.password_error = "Pad not found!
This error can be caused by two factors: either the password in invalid, or the pad has been deleted from the server."; out.password_placeholder = "Type the password here..."; out.password_submit = "Submit"; diff --git a/rpc.js b/rpc.js index c21bee11f..69d84b6dd 100644 --- a/rpc.js +++ b/rpc.js @@ -896,13 +896,13 @@ var removeOwnedBlob = function (Env, blobId, unsafeKey, cb) { Fs.unlink(blobPath, w(function (e) { if (e) { w.abort(); - return void cb(e); + return void cb(e.code); } })); }).nThen(function () { // Delete the proof of ownership Fs.unlink(ownPath, function (e) { - cb(e); + cb(e && e.code); }); }); }; @@ -1204,7 +1204,7 @@ var owned_upload_complete = function (Env, safeKey, id, cb) { return void cb(); } else { // it failed in an unexpected way. log it - WARN(e, 'ownedUploadComplete'); + WARN('ownedUploadComplete', e); return void cb(e.code); } }); @@ -1221,13 +1221,13 @@ var owned_upload_complete = function (Env, safeKey, id, cb) { Mkdirp(filePath, w(function (e /*, path */) { if (e) { // does not throw error if the directory already existed w.abort(); - return void cb(e); + return void cb(e.code); } })); Mkdirp(ownPath, w(function (e /*, path */) { if (e) { // does not throw error if the directory already existed w.abort(); - return void cb(e); + return void cb(e.code); } })); }).nThen(function (w) { @@ -1254,12 +1254,11 @@ var owned_upload_complete = function (Env, safeKey, id, cb) { // flow is dumb and I need to guard against this which will never happen /*:: if (typeof(oldPath) === 'object') { throw new Error('should never happen'); } */ - Fs.rename(oldPath /* XXX */, finalPath, w(function (e) { + Fs.rename(oldPath, finalPath, w(function (e) { if (e) { // Remove the ownership file - // XXX not needed if we have a cleanup script? Fs.unlink(finalOwnPath, function (e) { - WARN(e, 'Removing ownership file ownedUploadComplete'); + WARN('E_UNLINK_OWN_FILE', e); }); w.abort(); return void cb(e.code); diff --git a/www/assert/main.js b/www/assert/main.js index 657488b81..4ff862511 100644 --- a/www/assert/main.js +++ b/www/assert/main.js @@ -9,7 +9,8 @@ define([ '/common/common-thumbnail.js', '/common/wire.js', '/common/flat-dom.js', -], function ($, Hyperjson, Sortify, Drive, Test, Hash, Util, Thumb, Wire, Flat) { + '/common/media-tag.js', +], function ($, Hyperjson, Sortify, Drive, Test, Hash, Util, Thumb, Wire, Flat, MediaTag) { window.Hyperjson = Hyperjson; window.Sortify = Sortify; @@ -295,6 +296,26 @@ define([ !secret.hashData.present); }, "test support for ugly tracking query paramaters in url"); + assert(function (cb) { + try { + MediaTag(void 0).on('progress').on('decryption'); + return void cb(true); + } catch (e) { + console.error(e); + return void cb(false); + } + }, 'check that MediaTag does the right thing when passed no value'); + + assert(function (cb) { + try { + MediaTag(document.createElement('div')).on('progress').on('decryption'); + return void cb(true); + } catch (e) { + console.error(e); + return void cb(false); + } + }, 'check that MediaTag does the right thing when passed no value'); + assert(function (cb) { // TODO return cb(true); diff --git a/www/common/LessLoader.js b/www/common/LessLoader.js index 063b3d024..04fa68714 100644 --- a/www/common/LessLoader.js +++ b/www/common/LessLoader.js @@ -49,6 +49,7 @@ define([ if (ua[0].indexOf(':') === -1 && ua[0].indexOf('/') && parent) { ua[0] = parent.replace(/\/[^\/]*$/, '/') + ua[0]; } + ua[0] = ua[0].replace(/^\/\.\.\//, '/'); var out = ua.join('#'); //console.log(url + " --> " + out); return out; @@ -91,17 +92,32 @@ define([ }; var lessEngine; + var tempCache = { key: Math.random() }; var getLessEngine = function (cb) { if (lessEngine) { cb(lessEngine); } else { require(['/bower_components/less/dist/less.min.js'], function (Less) { + if (lessEngine) { return void cb(lessEngine); } lessEngine = Less; var doXHR = lessEngine.FileManager.prototype.doXHR; lessEngine.FileManager.prototype.doXHR = function (url, type, callback, errback) { url = fixURL(url); - //console.log("xhr: " + url); - return doXHR(url, type, callback, errback); + var cached = tempCache[url]; + if (cached && cached.res) { + var res = cached.res; + return void setTimeout(function () { callback(res[0], res[1]); }); + } + if (cached) { return void cached.queue.push(callback); } + cached = tempCache[url] = { queue: [ callback ], res: undefined }; + return doXHR(url, type, function (text, lastModified) { + cached.res = [ text, lastModified ]; + var queue = cached.queue; + cached.queue = []; + queue.forEach(function (f) { + setTimeout(function () { f(text, lastModified); }); + }); + }, errback); }; cb(lessEngine); }); diff --git a/www/common/common-interface.js b/www/common/common-interface.js index e65742d0f..86608e1b3 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -144,13 +144,15 @@ define([ }; dialog.frame = function (content) { - return h('div.alertify', { + return $(h('div.alertify', { tabindex: 1, }, [ h('div.dialog', [ h('div', content), ]) - ]); + ])).click(function (e) { + e.stopPropagation(); + })[0]; }; /** diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 3c1c9c7ab..04c9db22e 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -18,8 +18,10 @@ define([ var UIElements = {}; // Configure MediaTags to use our local viewer - if (MediaTag && MediaTag.PdfPlugin) { - MediaTag.PdfPlugin.viewer = '/common/pdfjs/web/viewer.html'; + if (MediaTag) { + MediaTag.setDefaultConfig('pdf', { + viewer: '/common/pdfjs/web/viewer.html' + }); } UIElements.updateTags = function (common, href) { @@ -626,7 +628,6 @@ define([ } } sframeChan.query('Q_SAVE_AS_TEMPLATE', { - title: title, toSave: toSave }, function () { UI.alert(Messages.templateSaved); @@ -1035,52 +1036,6 @@ define([ // Avatars - // Enable mediatags - $(window.document).on('decryption', function (e) { - var decrypted = e.originalEvent; - if (decrypted.callback) { - var cb = decrypted.callback; - cb(function (mediaObject) { - var root = mediaObject.element; - if (!root) { return; } - - if (mediaObject.type === 'image') { - $(root).data('blob', decrypted.blob); - } - - if (mediaObject.type !== 'download') { return; } - - var metadata = decrypted.metadata; - - var title = ''; - var size = 0; - if (metadata && metadata.name) { - title = metadata.name; - } - - if (decrypted.blob) { - size = decrypted.blob.size; - } - - var sizeMb = Util.bytesToMegabytes(size); - - var $btn = $(root).find('button'); - $btn.addClass('btn btn-success') - .attr('type', 'download') - .html(function () { - var text = Messages.download_mt_button + '
'; - if (title) { - text += '' + Util.fixHTML(title) + '
'; - } - if (size) { - text += '' + Messages._getKey('formattedMB', [sizeMb]) + ''; - } - return text; - }); - }); - } - }); - UIElements.displayMediatagImage = function (Common, $tag, cb) { if (!$tag.length || !$tag.is('media-tag')) { return void cb('NOT_MEDIATAG'); } var observer = new MutationObserver(function(mutations) { diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index b94d31f59..0a3724eba 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -449,6 +449,26 @@ define([ }).nThen(function () { Crypt.get(parsed.hash, function (err, val) { if (err) { throw new Error(err); } + try { + // Try to fix the title before importing the template + var parsed = JSON.parse(val); + var meta; + if (Array.isArray(parsed) && typeof(parsed[3]) === "object") { + meta = parsed[3].metadata; // pad + } else if (parsed.info) { + meta = parsed.info; // poll + } else { + meta = parsed.metadata; + } + if (typeof(meta) === "object") { + meta.defaultTitle = meta.title || meta.defaultTitle; + delete meta.users; + delete meta.title; + } + val = JSON.stringify(parsed); + } catch (e) { + console.log("Can't fix template title", e); + } Crypt.put(parsed2.hash, val, cb, optsPut); }, optsGet); }); diff --git a/www/common/media-tag.js b/www/common/media-tag.js index acdee91da..79f48e8ea 100644 --- a/www/common/media-tag.js +++ b/www/common/media-tag.js @@ -1 +1,428 @@ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.MediaTag=t():e.MediaTag=t()}(this,function(){return function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:r})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=63)}([function(e,t,n){"use strict";var r={IMAGE:"image",AUDIO:"audio",VIDEO:"video",PDF:"pdf",DASH:"dash",DOWNLOAD:"download",CRYPTO:"crypto",CLEAR_KEY:"clear-key",MEDIA_OBJECT:"media-object"};e.exports=r},function(e,t,n){"use strict";var r={MATCHER:"matcher",RENDERER:"renderer",FILTER:"filter",SANITIZER:"sanitizer"};e.exports=r},function(e,t,n){"use strict";function r(e){if(e instanceof Array){var t=[];return e.forEach(function(e){if(e.mediaObject)t.push(e.mediaObject);else{new c(e,r.processingEngine).mediaObjects.forEach(function(e){t.push(r.processingEngine.start(e))})}}),t}new c(e,r.processingEngine).mediaObjects.forEach(function(e){r.processingEngine.start(e)})}var o=n(31),i=n(33),u=n(34),a=n(35),c=n(36);r.pluginStore=r.pluginStore||new u,r.uriStore=r.uriStore||new a("../plugins"),r.processingEngine=r.processingEngine||new o(r.pluginStore),r.matchingEngine=r.matchingEngine||new i(r.pluginStore,r.uriStore),r.loadingEngine=null,e.exports=r},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function i(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}var u=function(){function e(e,t){for(var n=0;n=0&&f.mediaTypes.splice(t,1)},f.removeAllAllowedMediaTypes=function(e){e.forEach(function(e){f.removeAllowedMediaType(e)})},f.isAllowedMediaType=function(e){return f.mediaTypes.some(function(t){return t===e})},e.exports=f},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var o=function(){function e(e,t){for(var n=0;n0}}]),e}();e.exports=i},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function i(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}var u={PluginExists:function(e){function t(e){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,'Plugin with same "'+e.identifier+'" identifier found.'))}return i(t,e),t}(Error),TypeNotFound:function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,"Media Tag could not find the content type of an instance.}."))}return i(t,e),t}(Error),FilterExists:function(e){function t(e){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,'Filter with same "'+e.identifier+' identifier found."'))}return i(t,e),t}(Error),FetchFail:function(e){function t(e){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,'Could not fetch "'+e.url+'", received "'+e.status+": "+e.statusText+'".'))}return i(t,e),t}(Error),InvalidCryptoKey:function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,"Invalid cryptographic key."))}return i(t,e),t}(Error),InvalidCryptoLib:function(e){function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,"Invalid cryptographic algorithm name."))}return i(t,e),t}(Error),FailedCrypto:function(e){function t(e){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,"Failed to decrypt file"+(e&&e.message?" "+e.message:"")+"."))}return i(t,e),t}(Error)};e.exports=u},function(e,t,n){"use strict";var r=n(21),o=n(43),i=n(44),u=n(45),a=n(46),c=n(47),s=n(48),f=n(7),l=n(49),p=n(50);r.pluginStore.store(new o),r.pluginStore.store(new i),r.pluginStore.store(new u),r.pluginStore.store(new a),r.pluginStore.store(new c),r.pluginStore.store(new s),r.pluginStore.store(new f),r.pluginStore.store(new l),r.pluginStore.store(new p);var y=n(51),h=n(52);f.functionStore.store("salsa20poly1305",y),f.functionStore.store("cryptpad",h);var b=new s("

MediaTag cannot find a plugin able to renderer your content

","Download");r.processingEngine.setDefaultPlugin(b),r.CryptoFilter=f;var d=["image/png","image/jpeg","image/jpg","image/gif","audio/mp3","audio/ogg","audio/wav","audio/webm","video/mp4","video/ogg","video/webm","application/pdf","application/dash+xml","download"];r.CryptoFilter.setAllowedMediaTypes(d);var v=n(53),g=(n(14),n(0),new v);r.PdfPlugin=a,r.PdfPlugin.viewer="/pdfjs/web/viewer.html",r.processingEngine.configure(g),e.exports=r},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function i(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}var u=n(1),a=n(5),c=n(6),s=function(e){function t(e){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e,u.SANITIZER,a.EVERY))}return i(t,e),t}(c);e.exports=s},function(e,t,n){"use strict";var r=n(22),o=n(23),i=n(24),u=n(25),a=n(26),c=n(27),s=n(28),f=n(29),l=n(30),p=n(2);p.pluginStore.store(new r),p.pluginStore.store(new o),p.pluginStore.store(new i),p.pluginStore.store(new u),p.pluginStore.store(new a),p.pluginStore.store(new c),p.pluginStore.store(new s),p.pluginStore.store(new f),p.pluginStore.store(new l),e.exports=p},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function i(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}var u=function(){function e(e,t){for(var n=0;n=e.STACK_LIMIT)throw console.error(this.snapshots[n]),new Error("Plugin stack size exceed");if(this.snapshots[n].length>=e.SNAPSHOT_LIMIT)throw console.error(this.snapshots[n]),new Error("Plugin snapshots size exceed");var r=0;if(this.stacks[n].forEach(function(e){e.type===a.RENDERER&&r++}),r>1)throw console.error(this.snapshots[n]),new Error("More of one renderer in the stack");if(0===this.stacks[n].length&&!this.stats[n][a.RENDERER]){if(!this.defaultPlugin)throw new Error("No default plugin assignated");this.stacks[n].unshift(this.defaultPlugin)}}},{key:"return",value:function(e){var t=e.getId(),n=this.unstack(e);if(n){try{this.stats[t]||(this.stats[t]={}),this.stats[t][n.type]?this.stats[t][n.type]+=1:this.stats[t][n.type]=1}catch(e){console.error(e,this.snapshots[t])}0===this.stacks[t].length&&n.type===a.RENDERER?this.run(e):n.type!==a.SANITIZER&&this.fill(e),this.snapshot(e),this.check(e),this.run(e)}}},{key:"process",value:function(e){var t=e.getId(),n=this.stacks[t].length,r=this.stacks[t][n-1];if(!r)throw console.log(this.stacks),new Error("Impossible to run a undefined plugin");r.process(e)}},{key:"isStacked",value:function(e,t){var n=e.getId();return!(!this.stacks[n]||!this.stacks[n].includes(t))}},{key:"setDefaultPlugin",value:function(e){this.defaultPlugin=e}}]),e}();f.STACK_LIMIT=100,f.SNAPSHOT_LIMIT=100,e.exports=f},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var o=function(){function e(e,t){for(var n=0;n0&&r(e.mediaObjects[t-1],e)}},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var o=function(){function e(e,t){for(var n=0;n1?t[0]:window.location.protocol}},{key:"hostname",value:function(e){var t=e.getAttribute("src").split("://");return t.length>1?t[1].split("/")[0]:window.location.hostname}},{key:"source",value:function(e){return e.getAttribute("src")}},{key:"schemes",value:function(e){return/\w+:/.exec(e.getAttribute("src"))}},{key:"sources",value:function(e){var t=e.getAttribute("sources")||e.getAttribute("srcs");return t?JSON.parse(t):null}},{key:"actions",value:function(e){var t=e.getAttribute("actions");return t?JSON.parse(t):null}},{key:"parse",value:function(t){return{protocol:e.protocol(t),hostname:e.hostname(t),src:e.source(t),type:e.type(t),extension:e.extension(t),mime:e.mime(t),sources:e.sources(t),actions:e.actions(t)}}}]),e}();e.exports=i},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function i(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}var u=function(){function e(e,t){for(var n=0;n1;){if("number"!=typeof e[t])throw new Error("E_UNSAFE_TYPE");if(e[t]>255)throw new Error("E_OUT_OF_BOUNDS");if(255!==e[t])return void e[t]++;if(e[t]=0,0===t)throw new Error("E_NONCE_TOO_LARGE")}}},{key:"encodePrefix",value:function(e){return[65280,255].map(function(t,n){return(e&t)>>8*(1-n)})}},{key:"decodePrefix",value:function(e){return e[0]<<8|e[1]}},{key:"joinChunks",value:function(e){return new Blob(e)}},{key:"slice",value:function(e){return Array.prototype.slice.call(e)}},{key:"getRandomKeyStr",value:function(){var e=window.nacl,t=e.randomBytes(18);return e.util.encodeBase64(t)}},{key:"getKeyFromStr",value:function(e){return window.nacl.util.decodeBase64(e)}},{key:"encrypt",value:function(){}},{key:"decrypt",value:function(t,n,r){var o=window.nacl,i=function(e){var n=new Event("decryptionProgress");n.percent=e/t.length*100,window.document.dispatchEvent(n)},u=e.createNonce(),a=0,c=t.subarray(0,2),s=e.decodePrefix(c),f={metadata:void 0},l=new Uint8Array(t.subarray(2,2+s)),p=o.secretbox.open(l,u,n);e.increment(u);try{f.metadata=JSON.parse(o.util.encodeUTF8(p))}catch(e){return r("E_METADATA_DECRYPTION")}if(!f.metadata)return r("NO_METADATA");var y=function(r){setTimeout(function(){var c=131088*a+2+s,f=c+131088;a++;var l=new Uint8Array(t.subarray(c,f)),p=o.secretbox.open(l,u,n);if(e.increment(u),!p)return void r("DECRYPTION_FAILURE");i(Math.min(f,t.length)),r(void 0,p)})},h=[];!function n(){y(function(o,i){return o?setTimeout(function(){r(o)}):i?2+s+131088*a<=t.length?(h.push(i),n()):(h.push(i),f.content=e.joinChunks(h),r(void 0,f)):void r("UNEXPECTED_ENDING")})}()}}]),e}(),l=function(){function e(){r(this,e)}return u(e,null,[{key:"getArrayBuffer",value:function(e){return fetch(e).then(function(e){if(e.ok)return e.arrayBuffer();throw new a.FetchFails}).then(function(e){return e})}},{key:"createUrl",value:function(e){return window.URL.createObjectURL(e)}},{key:"getBlobUrl",value:function(e,t){return window.URL.createObjectURL(new Blob([e],{type:t}))}},{key:"getDataUrl",value:function(e,t){return"data:"+t+";base64,"+window.nacl.util.encodeBase64(e)}}]),e}();e.exports=i},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var o=function(){function e(e,t){for(var n=0;n&"']/g, function (x) { + return ({ "<": "<", ">": ">", "&": "&", '"': """, "'": "'" })[x]; + }); + }; + + + // Default config, can be overriden per media-tag call + var config = { + allowed: [ + 'image/png', + 'image/jpeg', + 'image/jpg', + 'image/gif', + 'audio/mp3', + 'audio/ogg', + 'audio/wav', + 'audio/webm', + 'video/mp4', + 'video/ogg', + 'video/webm', + 'application/pdf', + //'application/dash+xml', // FIXME? + 'download' + ], + pdf: {}, + download: { + text: "Download" + }, + Plugins: { + image: function (metadata, url, content, cfg, cb) { + var img = document.createElement('img'); + img.setAttribute('src', url); + img.blob = content; + cb(void 0, img); + }, + video: function (metadata, url, content, cfg, cb) { + var video = document.createElement('video'); + video.setAttribute('src', url); + video.setAttribute('controls', true); + cb(void 0, video); + }, + audio: function (metadata, url, content, cfg, cb) { + var audio = document.createElement('audio'); + audio.setAttribute('src', url); + audio.setAttribute('controls', true); + cb(void 0, audio); + }, + pdf: function (metadata, url, content, cfg, cb) { + var iframe = document.createElement('iframe'); + if (cfg.pdf.viewer) { // PDFJS + var viewerUrl = cfg.pdf.viewer + '?file=' + url; + iframe.src = viewerUrl + '#' + window.encodeURIComponent(metadata.name); + return void cb (void 0, iframe); + } + iframe.src = url + '#' + window.encodeURIComponent(metadata.name); + return void cb (void 0, iframe); + }, + download: function (metadata, url, content, cfg, cb) { + var btn = document.createElement('button'); + btn.setAttribute('class', 'btn btn-success'); + btn.innerHTML = cfg.download.text + '
' + + (metadata.name ? '' + fixHTML(metadata.name) + '' : ''); + btn.addEventListener('click', function () { + saveFile(content, url, metadata.name); + }); + cb(void 0, btn); + } + } + }; + + + // Download a blob from href + var download = function (src, cb) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', src, true); + xhr.responseType = 'arraybuffer'; + + xhr.onload = function () { + // Error? + if (/^4/.test('' + this.status)) { return void cb("XHR_ERROR " + this.status); } + + var arrayBuffer = xhr.response; + if (arrayBuffer) { cb(null, new Uint8Array(arrayBuffer)); } + }; + + xhr.send(null); + }; + + // Decryption tools + var Decrypt = { + // Create a nonce + createNonce: function () { + var n = new Uint8Array(24); + for (var i = 0; i < 24; i++) { n[i] = 0; } + return n; + }, + + // Increment a nonce + increment: function (N) { + var l = N.length; + while (l-- > 1) { + /* .jshint probably suspects this is unsafe because we lack types + but as long as this is only used on nonces, it should be safe */ + if (N[l] !== 255) { return void N[l]++; } // jshint ignore:line + + // you don't need to worry about this running out. + // you'd need a REAAAALLY big file + if (l === 0) { throw new Error('E_NONCE_TOO_LARGE'); } + + N[l] = 0; + } + }, + + decodePrefix: function (A) { + return (A[0] << 8) | A[1]; + }, + joinChunks: function (chunks) { + return new Blob(chunks); + }, + + // Convert a Uint8Array into Array. + slice: function (u8) { + return Array.prototype.slice.call(u8); + }, + + // Gets the key from the key string. + getKeyFromStr: function (str) { + return window.nacl.util.decodeBase64(str); + } + }; + + // Decrypts a Uint8Array with the given key. + var decrypt = function (u8, strKey, done, progressCb) { + var Nacl = window.nacl; + + var progress = function (offset) { + progressCb((offset / u8.length) * 100); + }; + + var key = Decrypt.getKeyFromStr(strKey); + var nonce = Decrypt.createNonce(); + var i = 0; + var prefix = u8.subarray(0, 2); + var metadataLength = Decrypt.decodePrefix(prefix); + + var res = { metadata: undefined }; + + // Get metadata + var metaBox = new Uint8Array(u8.subarray(2, 2 + metadataLength)); + var metaChunk = Nacl.secretbox.open(metaBox, nonce, key); + + Decrypt.increment(nonce); + + try { res.metadata = JSON.parse(Nacl.util.encodeUTF8(metaChunk)); } + catch (e) { return void done('E_METADATA_DECRYPTION'); } + + if (!res.metadata) { return void done('NO_METADATA'); } + + var takeChunk = function (cb) { + setTimeout(function () { + var start = i * cypherChunkLength + 2 + metadataLength; + var end = start + cypherChunkLength; + i++; + + // Get the chunk + var box = new Uint8Array(u8.subarray(start, end)); + + // Decrypt the chunk + var plaintext = Nacl.secretbox.open(box, nonce, key); + Decrypt.increment(nonce); + + if (!plaintext) { return void cb('DECRYPTION_FAILURE'); } + + progress(Math.min(end, u8.length)); + + cb(void 0, plaintext); + }); + }; + + var chunks = []; + + // decrypt file contents + var again = function () { + takeChunk(function (e, plaintext) { + if (e) { return setTimeout(function () { done(e); }); } + + if (plaintext) { + if (i * cypherChunkLength < u8.length) { // not done + chunks.push(plaintext); + return again(); + } + + chunks.push(plaintext); + res.content = Decrypt.joinChunks(chunks); + return void done(void 0, res); + } + done('UNEXPECTED_ENDING'); + }); + }; + again(); + }; + + // Get type + var getType = function (mediaObject, metadata, cfg) { + var mime = metadata.type; + var s = metadata.type.split('/'); + var type = s[0]; + var extension = s[1]; + + mediaObject.name = metadata.name; + if (mime && cfg.allowed.indexOf(mime) !== -1) { + mediaObject.type = type; + mediaObject.extension = extension; + mediaObject.mime = mime; + return type; + } else if (cfg.allowed.indexOf('download') !== -1) { + mediaObject.type = type; + mediaObject.extension = extension; + mediaObject.mime = mime; + return 'download'; + } else { + return; + } + }; + + // Copy attributes + var copyAttributes = function (origin, dest) { + Object.keys(origin.attributes).forEach(function (i) { + if (!/^data-attr/.test(origin.attributes[i].name)) { return; } + var name = origin.attributes[i].name.slice(10); + var value = origin.attributes[i].value; + dest.setAttribute(name, value); + }); + }; + + // Process + var process = function (mediaObject, decrypted, cfg, cb) { + var metadata = decrypted.metadata; + var blob = decrypted.content; + + var mediaType = getType(mediaObject, metadata, cfg); + + if (mediaType === 'application') { + mediaType = mediaObject.extension; + } + + if (!mediaType || !cfg.Plugins[mediaType]) { + return void cb('NO_PLUGIN_FOUND'); + } + + // Get blob URL + var url = decrypted.url; + if (!url && window.URL) { + url = decrypted.url = window.URL.createObjectURL(new Blob([blob], { + type: metadata.type + })); + } + + cfg.Plugins[mediaType](metadata, url, blob, cfg, function (err, el) { + if (err || !el) { return void cb(err || 'ERR_MEDIATAG_DISPLAY'); } + copyAttributes(mediaObject.tag, el); + mediaObject.tag.innerHTML = ''; + mediaObject.tag.appendChild(el); + cb(); + }); + }; + + var addMissingConfig = function (base, target) { + Object.keys(target).forEach(function (k) { + if (!target[k]) { return; } + // Target is an object, fix it recursively + if (typeof target[k] === "object" && !Array.isArray(target[k])) { + // Sub-object + if (base[k] && (typeof base[k] !== "object" || Array.isArray(base[k]))) { return; } + else if (base[k]) { addMissingConfig(base[k], target[k]); } + else { + base[k] = {}; + addMissingConfig(base[k], target[k]); + } + } + // Target is array or immutable, copy the value if it's missing + if (!base[k]) { + base[k] = Array.isArray(target[k]) ? JSON.parse(JSON.stringify(target[k])) + : target[k]; + } + }); + }; + + // Initialize a media-tag + var init = function (el, cfg) { + cfg = cfg || {}; + + addMissingConfig(cfg, config); + + // Handle jQuery elements + if (typeof(el) === "object" && el.jQuery) { el = el[0]; } + + // Abort smoothly if the element is not a media-tag + if (!el || el.nodeName !== "MEDIA-TAG") { + console.error("Not a media-tag!"); + return { + on: function () { return this; } + }; + } + + var handlers = cfg.handlers || { + 'progress': [], + 'complete': [], + 'error': [] + }; + + var mediaObject = el._mediaObject = { + handlers: handlers, + tag: el + }; + + var emit = function (ev, data) { + // Check if the event name is valid + if (Object.keys(handlers).indexOf(ev) === -1) { + return void console.error("Invalid mediatag event"); + } + + // Call the handlers + handlers[ev].forEach(function (h) { + // Make sure a bad handler won't break the media-tag script + try { + h(data); + } catch (err) { + console.error(err); + } + }); + }; + + mediaObject.on = function (ev, handler) { + // Check if the event name is valid + if (Object.keys(handlers).indexOf(ev) === -1) { + console.error("Invalid mediatag event"); + return mediaObject; + } + // Check if the handler is valid + if (typeof (handler) !== "function") { + console.error("Handler is not a function!"); + return mediaObject; + } + // Add the handler + handlers[ev].push(handler); + return mediaObject; + }; + + var src = el.getAttribute('src'); + var strKey = el.getAttribute('data-crypto-key'); + if (/^cryptpad:/.test(strKey)) { + strKey = strKey.slice(9); + } + var uid = [src, strKey].join(''); + + // End media-tag rendering: display the tag and emit the event + var end = function (decrypted) { + process(mediaObject, decrypted, cfg, function (err) { + if (err) { return void emit('error', err); } + emit('complete', decrypted); + }); + }; + + // If we have the blob in our cache, don't download & decrypt it again, just display + if (cache[uid]) { + end(cache[uid]); + return mediaObject; + } + + // Download the encrypted blob + download(src, function (err, u8Encrypted) { + if (err) { + return void emit('error', err); + } + // Decrypt the blob + decrypt(u8Encrypted, strKey, function (errDecryption, u8Decrypted) { + if (errDecryption) { + return void emit('error', errDecryption); + } + // Cache and display the decrypted blob + cache[uid] = u8Decrypted; + end(u8Decrypted); + }, function (progress) { + emit('progress', { + progress: progress + }); + }); + }); + + return mediaObject; + }; + + // Add the cache as a property of MediaTag + cache = init.__Cryptpad_Cache = {}; + + init.setDefaultConfig = function (key, value) { + config[key] = value; + }; + + return init; +})); diff --git a/www/common/outer/userObject.js b/www/common/outer/userObject.js index 8b2270797..d43375ef2 100644 --- a/www/common/outer/userObject.js +++ b/www/common/outer/userObject.js @@ -111,7 +111,7 @@ define([ // RPC may not be responding // Send a report that can be handled manually console.error(obj.error); - Feedback.send('ERROR_DELETING_OWNED_PAD=' + channelId, true); + Feedback.send('ERROR_DELETING_OWNED_PAD=' + channelId + '|' + obj.error, true); } }); } diff --git a/www/common/sframe-common-title.js b/www/common/sframe-common-title.js index 317944993..0a838ef34 100644 --- a/www/common/sframe-common-title.js +++ b/www/common/sframe-common-title.js @@ -42,6 +42,9 @@ define([ exp.updateTitle = function (newTitle, cb) { cb = cb || $.noop; if (newTitle === exp.title) { return void cb(); } + if (newTitle === exp.defaultTitle) { + newTitle = ""; + } metadataMgr.updateTitle(newTitle); titleUpdated = cb; }; @@ -51,7 +54,9 @@ define([ if ($title) { $title.find('span.cp-toolbar-title-value').text(md.title || md.defaultTitle); $title.find('input').val(md.title || md.defaultTitle); + $title.find('input').prop('placeholder', md.defaultTitle); } + exp.defaultTitle = md.defaultTitle; exp.title = md.title; }); metadataMgr.onTitleChange(function (title) { diff --git a/www/file/inner.js b/www/file/inner.js index 78d162f5e..1e2a4f3e1 100644 --- a/www/file/inner.js +++ b/www/file/inner.js @@ -37,6 +37,9 @@ 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'); @@ -130,32 +133,17 @@ define([ $mt.attr('data-crypto-key', 'cryptpad:'+cryptKey); var rightsideDisplayed = false; - $(window.document).on('decryption', function (e) { - /* FIXME - we're listening for decryption events and assuming that only - the main media-tag exists. In practice there is also your avatar - and there could be other things in the future, so we should - figure out a generic way target media-tag decryption events. - */ - var decrypted = e.originalEvent; - if (decrypted.callback) { - decrypted.callback(); - } + MediaTag($mt[0]).on('complete', function (decrypted) { $dlview.show(); $dlform.hide(); var $dlButton = $dlview.find('media-tag button'); if (ev) { $dlButton.click(); } - $dlButton.addClass('btn btn-success'); - var text = Messages.download_mt_button + '
'; - text += '' + Util.fixHTML(title) + '
'; - text += '' + Messages._getKey('formattedMB', [sizeMb]) + ''; - $dlButton.html(text); if (!rightsideDisplayed) { toolbar.$rightside .append(common.createButton('export', true, {}, function () { - saveAs(decrypted.blob, decrypted.metadata.name); + saveAs(decrypted.content, decrypted.metadata.name); })); rightsideDisplayed = true; } @@ -178,42 +166,12 @@ define([ } else { cb(); } - }) - .on('decryptionError', function (e) { - var error = e.originalEvent; - //UI.alert(error.message); - cb(error.message); - }) - .on('decryptionProgress', function (e) { - var progress = e.originalEvent; - var p = progress.percent +'%'; + }).on('progress', function (data) { + var p = data.progress +'%'; $progress.width(p); + }).on('error', function (err) { + console.error(err); }); - - /** - * Allowed mime types that have to be set for a rendering after a decryption. - * - * @type {Array} - */ - var allowedMediaTypes = [ - 'image/png', - 'image/jpeg', - 'image/jpg', - 'image/gif', - 'audio/mp3', - 'audio/ogg', - 'audio/wav', - 'audio/webm', - 'video/mp4', - 'video/ogg', - 'video/webm', - 'application/pdf', - 'application/dash+xml', - 'download' - ]; - MediaTag.CryptoFilter.setAllowedMediaTypes(allowedMediaTypes); - - MediaTag($mt[0]); }; var todoBigFile = function (sizeMb) { diff --git a/www/filepicker/app-filepicker.less b/www/filepicker/app-filepicker.less index 952a4d857..5a80661cf 100644 --- a/www/filepicker/app-filepicker.less +++ b/www/filepicker/app-filepicker.less @@ -4,11 +4,15 @@ @import (once) '../../customize/src/less2/include/fileupload.less'; @import (once) '../../customize/src/less2/include/alertify.less'; @import (once) '../../customize/src/less2/include/tippy.less'; +@import (once) '../../customize/src/less2/include/checkmark.less'; +@import (once) '../../customize/src/less2/include/password-input.less'; .iconColors_main(); .fileupload_main(); .alertify_main(); .tippy_main(); +.checkmark_main(20px); +.password_main(); #cp-filepicker-dialog { display: none; diff --git a/www/mediatag/media-tag.js b/www/mediatag/media-tag.js index f240e0941..9ae19d3a8 100644 --- a/www/mediatag/media-tag.js +++ b/www/mediatag/media-tag.js @@ -4,7 +4,6 @@ else { this[name] = definition(); } }('MediaTag', function() { var cache; - var PARANOIA = true; var cypherChunkLength = 131088; // Save a blob on the file system @@ -23,6 +22,14 @@ } }; + var fixHTML = function (str) { + if (!str) { return ''; } + return str.replace(/[<>&"']/g, function (x) { + return ({ "<": "<", ">": ">", "&": "&", '"': """, "'": "'" })[x]; + }); + }; + + // Default config, can be overriden per media-tag call var config = { allowed: [ @@ -49,6 +56,7 @@ image: function (metadata, url, content, cfg, cb) { var img = document.createElement('img'); img.setAttribute('src', url); + img.blob = content; cb(void 0, img); }, video: function (metadata, url, content, cfg, cb) { @@ -75,7 +83,8 @@ }, download: function (metadata, url, content, cfg, cb) { var btn = document.createElement('button'); - btn.innerHTML = cfg.download.text; + btn.innerHTML = cfg.download.text + '
' + + metadata.name ? '' + fixHTML(metadata.name) + '' : ''; btn.addEventListener('click', function () { saveFile(content, url, metadata.name); }); @@ -106,38 +115,24 @@ var Decrypt = { // Create a nonce createNonce: function () { - if (!Array.prototype.fill) { - // IE support - var arr = []; - for (var i = 0; i < 24; i++) { arr[i] = 0; } - return new Uint8Array(arr); - } - return new Uint8Array(new Array(24).fill(0)); + var n = new Uint8Array(24); + for (var i = 0; i < 24; i++) { n[i] = 0; } + return n; }, // Increment a nonce - // FIXME: remove throw? increment: function (N) { var l = N.length; while (l-- > 1) { - if (PARANOIA) { - if (typeof(N[l]) !== 'number') { - throw new Error('E_UNSAFE_TYPE'); - } - if (N[l] > 255) { - throw new Error('E_OUT_OF_BOUNDS'); - } - } /* .jshint probably suspects this is unsafe because we lack types but as long as this is only used on nonces, it should be safe */ if (N[l] !== 255) { return void N[l]++; } // jshint ignore:line - N[l] = 0; // you don't need to worry about this running out. // you'd need a REAAAALLY big file - if (l === 0) { - throw new Error('E_NONCE_TOO_LARGE'); - } + if (l === 0) { throw new Error('E_NONCE_TOO_LARGE'); } + + N[l] = 0; } }, @@ -153,13 +148,6 @@ return Array.prototype.slice.call(u8); }, - // Gets the random key string. - getRandomKeyStr: function () { - var Nacl = window.nacl; - var rdm = Nacl.randomBytes(18); - return Nacl.util.encodeBase64(rdm); - }, - // Gets the key from the key string. getKeyFromStr: function (str) { return window.nacl.util.decodeBase64(str); @@ -287,7 +275,7 @@ // Get blob URL var url = decrypted.url; - if (!url) { + if (!url && window.URL) { url = decrypted.url = window.URL.createObjectURL(new Blob([blob], { type: metadata.type })); diff --git a/www/pad/inner.js b/www/pad/inner.js index b8199bc61..865be9ef8 100644 --- a/www/pad/inner.js +++ b/www/pad/inner.js @@ -601,8 +601,8 @@ define([ var $clone = $(inner).clone(); nThen(function (waitFor) { $(inner).find('media-tag').each(function (i, el) { - if (!$(el).data('blob')) { return; } - Util.blobToImage($(el).data('blob'), waitFor(function (imgSrc) { + if (!$(el).data('blob') || !el.blob) { return; } + Util.blobToImage(el.blob || $(el).data('blob'), waitFor(function (imgSrc) { $clone.find('media-tag[src="' + $(el).attr('src') + '"] img') .attr('src', imgSrc); $clone.find('media-tag').parent() diff --git a/www/profile/inner.js b/www/profile/inner.js index 40642507a..76c279ce0 100644 --- a/www/profile/inner.js +++ b/www/profile/inner.js @@ -43,16 +43,6 @@ define([ _onRefresh: [] }; - // Decryption event for avatar mediatag (TODO not needed anymore?) - $(window.document).on('decryption', function (e) { - var decrypted = e.originalEvent; - if (decrypted.callback) { decrypted.callback(); } - }) - .on('decryptionError', function (e) { - var error = e.originalEvent; - UI.alert(error.message); - }); - $(window).click(function () { $('.cp-dropdown-content').hide(); });