diff --git a/www/common/common-constants.js b/www/common/common-constants.js index 986e115e2..ac51dfbca 100644 --- a/www/common/common-constants.js +++ b/www/common/common-constants.js @@ -7,6 +7,7 @@ define(function () { fileHashKey: 'FS_hash', // sessionStorage newPadPathKey: "newPadPath", + newPadFileData: "newPadFileData", // Store displayNameKey: 'cryptpad.username', oldStorageKey: 'CryptPad_RECENTPADS', diff --git a/www/common/common-thumbnail.js b/www/common/common-thumbnail.js index a6e5d5128..9d52f130b 100644 --- a/www/common/common-thumbnail.js +++ b/www/common/common-thumbnail.js @@ -15,6 +15,7 @@ define([ }; var supportedTypes = [ + 'text/plain', 'image/png', 'image/jpeg', 'image/jpg', @@ -23,7 +24,12 @@ define([ 'application/pdf' ]; - Thumb.isSupportedType = function (type) { + Thumb.isSupportedType = function (file) { + if (!file) { return false; } + var type = file.type; + if (Util.isPlainTextFile(file.type, file.name)) { + type = "text/plain"; + } return supportedTypes.some(function (t) { return type.indexOf(t) !== -1; }); @@ -164,6 +170,26 @@ define([ }); }); }; + Thumb.fromPlainTextBlob = function (blob, cb) { + var canvas = document.createElement("canvas"); + canvas.width = canvas.height = Thumb.dimension; + var reader = new FileReader(); + reader.addEventListener('loadend', function (e) { + var content = e.srcElement.result; + var lines = content.split("\n"); + var canvasContext = canvas.getContext("2d"); + var fontSize = 4; + canvas.height = (lines.length) * (fontSize + 1); + canvasContext.font = fontSize + 'px monospace'; + lines.forEach(function (text, i) { + + canvasContext.fillText(text, 5, i * (fontSize + 1)); + }); + var D = getResizedDimensions(canvas, "txt"); + Thumb.fromCanvas(canvas, D, cb); + }); + reader.readAsText(blob); + }; Thumb.fromBlob = function (blob, cb) { if (blob.type.indexOf('video/') !== -1) { return void Thumb.fromVideoBlob(blob, cb); @@ -171,6 +197,9 @@ define([ if (blob.type.indexOf('application/pdf') !== -1) { return void Thumb.fromPdfBlob(blob, cb); } + if (Util.isPlainTextFile(blob.type, blob.name)) { + return void Thumb.fromPlainTextBlob(blob, cb); + } Thumb.fromImageBlob(blob, cb); }; diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 9aa313779..f3f2f5e6c 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -2301,7 +2301,10 @@ define([ if (!common.isLoggedIn()) { return void cb(); } var sframeChan = common.getSframeChannel(); var metadataMgr = common.getMetadataMgr(); + var privateData = metadataMgr.getPrivateData(); var type = metadataMgr.getMetadataLazy().type; + var fromFileData = privateData.fromFileData; + var $body = $('body'); var $creationContainer = $('
', { id: 'cp-creation-container' }).appendTo($body); @@ -2313,7 +2316,8 @@ define([ // Title //var colorClass = 'cp-icon-color-'+type; //$creation.append(h('h2.cp-creation-title', Messages.newButtonTitle)); - $creation.append(h('h3.cp-creation-title', Messages['button_new'+type])); + var newPadH3Title = Messages['button_new' + type]; + $creation.append(h('h3.cp-creation-title', newPadH3Title)); //$creation.append(h('h2.cp-creation-title.'+colorClass, Messages.newButtonTitle)); // Deleted pad warning @@ -2323,7 +2327,7 @@ define([ )); } - var origin = common.getMetadataMgr().getPrivateData().origin; + var origin = privateData.origin; var createHelper = function (href, text) { var q = h('a.cp-creation-help.fa.fa-question-circle', { title: text, @@ -2480,7 +2484,26 @@ define([ }); if (i < TEMPLATES_DISPLAYED) { $(left).addClass('hidden'); } }; - redraw(0); + if (fromFileData) { + var todo = function (thumbnail) { + allData = [{ + name: fromFileData.title, + id: 0, + thumbnail: thumbnail, + icon: h('span.cptools.cptools-file'), + }]; + redraw(0); + }; + todo(); + sframeChan.query("Q_GET_FILE_THUMBNAIL", null, function (err, res) { + if (err || (res && res.error)) { return; } + todo(res.data); + }); + } + else { + redraw(0); + } + // Change template selection when Tab is pressed next = function (revert) { diff --git a/www/common/common-util.js b/www/common/common-util.js index 03c9e321b..262de76e9 100644 --- a/www/common/common-util.js +++ b/www/common/common-util.js @@ -325,6 +325,30 @@ define([], function () { return div.innerText; }; + // return an object containing {name, ext} + // or {} if the name could not be parsed + Util.parseFilename = function (filename) { + if (!filename || !filename.trim()) { return {}; } + var parsedName = /^(\.?.+?)(\.[^.]+)?$/.exec(filename) || []; + return { + name: parsedName[1], + ext: parsedName[2], + }; + }; + + // Tell if a file is plain text from its metadata={title, fileType} + Util.isPlainTextFile = function (type, name) { + // does its type begins with "text/" + if (type && type.indexOf("text/") === 0) { return true; } + // no type and no file extension -> let's guess it's plain text + var parsedName = Util.parseFilename(name); + if (!type && name && !parsedName.ext) { return true; } + // other exceptions + if (type === 'application/x-javascript') { return true; } + if (type === 'application/xml') { return true; } + return false; + }; + return Util; }); }(self)); diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index a22f4d57e..a2a2fe7a9 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -571,6 +571,66 @@ define([ }); }; + common.useFile = function (Crypt, cb, optsPut) { + var data = common.fromFileData; + var parsed = Hash.parsePadUrl(data.href); + var parsed2 = Hash.parsePadUrl(window.location.href); + var hash = parsed.hash; + var name = data.title; + var secret = Hash.getSecrets('file', hash, data.password); + var src = Hash.getBlobPathFromHex(secret.channel); + var key = secret.keys && secret.keys.cryptKey; + + var u8; + var res; + var mode; + var val; + Nthen(function(waitFor) { + Util.fetch(src, waitFor(function (err, _u8) { + if (err) { return void waitFor.abort(); } + u8 = _u8; + })); + }).nThen(function (waitFor) { + require(["/file/file-crypto.js"], waitFor(function (FileCrypto) { + FileCrypto.decrypt(u8, key, waitFor(function (err, _res) { + if (err || !_res.content) { return void waitFor.abort(); } + res = _res; + })); + })); + }).nThen(function (waitFor) { + var ext = Util.parseFilename(data.title).ext; + if (!ext) { + mode = "text"; + return; + } + require(["/common/modes.js"], waitFor(function (Modes) { + Modes.list.some(function (fType) { + if (fType.ext === ext) { + mode = fType.mode; + return true; + } + }); + })); + }).nThen(function (waitFor) { + var reader = new FileReader(); + reader.addEventListener('loadend', waitFor(function (e) { + val = { + content: e.srcElement.result, + highlightMode: mode, + metadata: { + defaultTitle: name, + title: name, + type: "code", + }, + }; + })); + reader.readAsText(res.content); + }).nThen(function () { + Crypt.put(parsed2.hash, JSON.stringify(val), cb, optsPut); + }); + + }; + // Forget button common.moveToTrash = function (cb, href) { href = href || window.location.href; @@ -1274,6 +1334,12 @@ define([ messenger: rdyCfg.messenger, // Boolean driveEvents: rdyCfg.driveEvents // Boolean }; + // if a pad is created from a file + if (sessionStorage[Constants.newPadFileData]) { + common.fromFileData = JSON.parse(sessionStorage[Constants.newPadFileData]); + delete sessionStorage[Constants.newPadFileData]; + } + if (sessionStorage[Constants.newPadPathKey]) { common.initialPath = sessionStorage[Constants.newPadPathKey]; delete sessionStorage[Constants.newPadPathKey]; diff --git a/www/common/sframe-common-file.js b/www/common/sframe-common-file.js index 32f031010..e2e2cd46c 100644 --- a/www/common/sframe-common-file.js +++ b/www/common/sframe-common-file.js @@ -367,7 +367,7 @@ define([ blobToArrayBuffer(file, function (e, buffer) { if (e) { console.error(e); } file_arraybuffer = buffer; - if (!Thumb.isSupportedType(file.type)) { return getName(); } + if (!Thumb.isSupportedType(file)) { return getName(); } // make a resized thumbnail from the image.. Thumb.fromBlob(file, function (e, thumb64) { if (e) { console.error(e); } diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 104bdaa12..ba778a331 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -319,6 +319,9 @@ define([ channel: secret.channel, enableSF: localStorage.CryptPad_SF === "1", // TODO to remove when enabled by default devMode: localStorage.CryptPad_dev === "1", + fromFileData: Cryptpad.fromFileData ? { + title: Cryptpad.fromFileData.title + } : undefined, }; if (window.CryptPad_newSharedFolder) { additionalPriv.newSharedFolder = window.CryptPad_newSharedFolder; @@ -359,6 +362,8 @@ define([ sframeChan.event("EV_NEW_VERSION"); }); + + // Put in the following function the RPC queries that should also work in filepicker var addCommonRpc = function (sframeChan) { sframeChan.on('Q_ANON_RPC_MESSAGE', function (data, cb) { @@ -811,6 +816,22 @@ define([ }); }); + sframeChan.on('Q_GET_FILE_THUMBNAIL', function (data, cb) { + if (!Cryptpad.fromFileData || !Cryptpad.fromFileData.href) { + return void cb({ + error: "EINVAL", + }); + } + var key = getKey(Cryptpad.fromFileData.href, Cryptpad.fromFileData.channel); + Utils.LocalStore.getThumbnail(key, function (e, data) { + if (data === "EMPTY") { data = null; } + cb({ + error: e, + data: data + }); + }); + }); + sframeChan.on('EV_GOTO_URL', function (url) { if (url) { window.location.href = url; @@ -1097,11 +1118,11 @@ define([ })); } }).nThen(function () { + var cryptputCfg = $.extend(true, {}, rtConfig, {password: password}); if (data.template) { // Pass rtConfig to useTemplate because Cryptput will create the file and // we need to have the owners and expiration time in the first line on the // server - var cryptputCfg = $.extend(true, {}, rtConfig, {password: password}); Cryptpad.useTemplate({ href: data.template }, Cryptget, function () { @@ -1110,6 +1131,14 @@ define([ }, cryptputCfg); return; } + // if we open a new code from a file + if (Cryptpad.fromFileData) { + Cryptpad.useFile(Cryptget, function () { + startRealtime(); + cb(); + }, cryptputCfg); + return; + } // Start realtime outside the iframe and callback startRealtime(rtConfig); cb(); diff --git a/www/drive/inner.js b/www/drive/inner.js index 4292e5a7f..bdf74112a 100644 --- a/www/drive/inner.js +++ b/www/drive/inner.js @@ -82,6 +82,7 @@ define([ var faCollapseAll = 'fa-minus-square-o'; var faShared = 'fa-shhare-alt'; var faReadOnly = 'fa-eye'; + var faOpenInCode = 'cptools-code'; var faRename = 'fa-pencil'; var faColor = 'cptools-palette'; var faTrash = 'fa-trash'; @@ -343,6 +344,10 @@ define([ 'tabindex': '-1', 'data-icon': faReadOnly, }, Messages.fc_open_ro)), + h('li', h('a.cp-app-drive-context-openincode.dropdown-item', { + 'tabindex': '-1', + 'data-icon': faOpenInCode, + }, Messages.fc_openInCode)), $separator.clone()[0], h('li', h('a.cp-app-drive-context-expandall.dropdown-item', { 'tabindex': '-1', @@ -1095,6 +1100,10 @@ define([ // We can only open parent in virtual categories hide.push('openparent'); } + if (!$element.is('.cp-border-color-file')) { + //hide.push('download'); + hide.push('openincode'); + } if ($element.is('.cp-app-drive-element-file')) { // No folder in files hide.push('color'); @@ -1104,6 +1113,11 @@ define([ } else if ($element.is('.cp-app-drive-element-noreadonly')) { hide.push('openro'); // Remove open 'view' mode } + // if it's not a plain text file + var metadata = manager.getFileData(manager.find(path)); + if (!metadata || !Util.isPlainTextFile(metadata.fileType, metadata.title)) { + hide.push('openincode'); + } } else if ($element.is('.cp-app-drive-element-sharedf')) { if (containsFolder) { // More than 1 folder selected: cannot create a new subfolder @@ -1113,6 +1127,7 @@ define([ } containsFolder = true; hide.push('openro'); + hide.push('openincode'); hide.push('hashtag'); hide.push('delete'); //hide.push('deleteowned'); @@ -1125,6 +1140,7 @@ define([ } containsFolder = true; hide.push('openro'); + hide.push('openincode'); hide.push('properties'); hide.push('share'); hide.push('hashtag'); @@ -1159,6 +1175,7 @@ define([ hide.push('hashtag'); hide.push('download'); hide.push('share'); + hide.push('openincode'); // can't because of race condition } if (containsFolder && paths.length > 1) { // Cannot open multiple folders @@ -1175,7 +1192,7 @@ define([ show = ['newfolder', 'newsharedfolder', 'newdoc']; break; case 'tree': - show = ['open', 'openro', 'expandall', 'collapseall', 'color', 'download', 'share', 'rename', 'delete', 'deleteowned', 'removesf', 'properties', 'hashtag']; + show = ['open', 'openro', 'openincode', 'expandall', 'collapseall', 'color', 'download', 'share', 'rename', 'delete', 'deleteowned', 'removesf', 'properties', 'hashtag']; break; case 'default': show = ['open', 'openro', 'share', 'openparent', 'delete', 'deleteowned', 'properties', 'hashtag']; @@ -3677,6 +3694,25 @@ define([ openFile(null, href); }); } + else if ($this.hasClass('cp-app-drive-context-openincode')) { + if (paths.length !== 1) { return; } + var p = paths[0]; + el = manager.find(p.path); + var metadata = manager.getFileData(el); + var simpleData = { + title: metadata.filename || metadata.title, + href: metadata.href, + password: metadata.password, + channel: metadata.channel, + }; + nThen(function (waitFor) { + common.sessionStorage.put(Constants.newPadFileData, JSON.stringify(simpleData), waitFor()); + common.sessionStorage.put(Constants.newPadPathKey, currentPath, waitFor()); + }).nThen(function () { + common.openURL('/code/'); + }); + } + else if ($this.hasClass('cp-app-drive-context-expandall') || $this.hasClass('cp-app-drive-context-collapseall')) { if (paths.length !== 1) { return; }