diff --git a/static/js/contentCollection.js b/static/js/contentCollection.js
index 36a555b..d4726e8 100644
--- a/static/js/contentCollection.js
+++ b/static/js/contentCollection.js
@@ -1,11 +1,13 @@
'use strict';
+const {_isValid, uploadFile} = require('ep_image_upload/static/js/toolbar');
+
// When an image is detected give it a lineAttribute
// of Image with the URL to the iamge
exports.collectContentImage = (hookName, {node, state: {lineAttributes}, tname}) => {
if (tname === 'div' || tname === 'p') delete lineAttributes.img;
if (tname !== 'img') return;
- lineAttributes.img =
+ const imageData =
// Client-side. This will also be used for server-side HTML imports once jsdom adds support
// for HTMLImageElement.currentSrc.
node.currentSrc ||
@@ -13,6 +15,33 @@ exports.collectContentImage = (hookName, {node, state: {lineAttributes}, tname})
node.src ||
// Server-side HTML imports using cheerio (Etherpad <= v1.8.14).
(node.attribs && node.attribs.src);
+
+ if (typeof window !== 'undefined' && clientVars.ep_image_upload.storageType === 'local') {
+ if (/^http/.test(imageData)) {
+ // an uploaded image is copied, place a copy in the desired line
+ lineAttributes.img = imageData;
+ return;
+ }
+
+ const padeditor = require('ep_etherpad-lite/static/js/pad_editor').padeditor;
+
+ const match = imageData.match(/data:([^;]+);base64,(.*)/);
+ if (!match || !match[1] || !match[2]) return;
+
+ // decode from internal base64 rep
+ const decodedData = Uint8Array.from(window.atob(match[2]), (c) => c.charCodeAt(0));
+
+ // check if size is within limits and mime type is supported
+ const extension = _isValid({size: decodedData.length, type: match[1]});
+ if (!extension) return;
+
+ const blob = new Blob([decodedData], {type: match[1]});
+
+ // image.* is a temporary name not used on the server
+ uploadFile(padeditor, blob, `image.${extension}`);
+ } else {
+ lineAttributes.img = imageData;
+ }
};
exports.collectContentPre = (name, context) => {
diff --git a/static/js/toolbar.js b/static/js/toolbar.js
index 5b95f92..35a57c9 100644
--- a/static/js/toolbar.js
+++ b/static/js/toolbar.js
@@ -16,6 +16,7 @@ const _handleNewLines = (ace) => {
const _isValid = (file) => {
const mimedb = clientVars.ep_image_upload.mimeTypes;
const mimeType = mimedb[file.type];
+ const extension = mimeType ? mimeType.extensions[0] : null;
let validMime = null;
if (clientVars.ep_image_upload && clientVars.ep_image_upload.fileTypes) {
validMime = false;
@@ -44,9 +45,56 @@ const _isValid = (file) => {
return false;
}
- return true;
+ return extension;
};
+exports._isValid = _isValid;
+const uploadFile = (context, file, filename) => {
+ const formData = new FormData();
+
+ // add assoc key values, this will be posts values
+ formData.append('file', file, filename ? filename : file.name);
+ $('#imageUploadModalLoader').addClass('popup-show');
+ $.ajax({
+ type: 'POST',
+ url: `${clientVars.padId}/pluginfw/ep_image_upload/upload`,
+ xhr: () => {
+ const myXhr = $.ajaxSettings.xhr();
+
+ return myXhr;
+ },
+ success: (data) => {
+ $('#imageUploadModalLoader').removeClass('popup-show');
+ context.ace.callWithAce((ace) => {
+ const imageLineNr = _handleNewLines(ace);
+ ace.ace_addImage(imageLineNr, data);
+ ace.ace_doReturnKey();
+ }, 'img', true);
+ },
+ error: (error) => {
+ let errorResponse;
+ try {
+ errorResponse = JSON.parse(error.responseText.trim());
+ if (errorResponse.type) {
+ errorResponse.message = window._(`ep_image_upload.error.${errorResponse.type}`);
+ }
+ } catch (err) {
+ errorResponse = {message: error.responseText};
+ }
+
+ $('#imageUploadModalLoader').removeClass('popup-show');
+ $('#imageUploadModalError .error').html(errorResponse.message);
+ $('#imageUploadModalError').addClass('popup-show');
+ },
+ async: true,
+ data: formData,
+ cache: false,
+ contentType: false,
+ processData: false,
+ timeout: 60000,
+ });
+};
+exports.uploadFile = uploadFile;
exports.postToolbarInit = (hook, context) => {
const toolbar = context.toolbar;
@@ -83,49 +131,7 @@ exports.postToolbarInit = (hook, context) => {
}, 'img', true);
};
} else {
- const formData = new FormData();
-
- // add assoc key values, this will be posts values
- formData.append('file', file, file.name);
- $('#imageUploadModalLoader').addClass('popup-show');
- $.ajax({
- type: 'POST',
- url: `${clientVars.padId}/pluginfw/ep_image_upload/upload`,
- xhr: () => {
- const myXhr = $.ajaxSettings.xhr();
-
- return myXhr;
- },
- success: (data) => {
- $('#imageUploadModalLoader').removeClass('popup-show');
- context.ace.callWithAce((ace) => {
- const imageLineNr = _handleNewLines(ace);
- ace.ace_addImage(imageLineNr, data);
- ace.ace_doReturnKey();
- }, 'img', true);
- },
- error: (error) => {
- let errorResponse;
- try {
- errorResponse = JSON.parse(error.responseText.trim());
- if (errorResponse.type) {
- errorResponse.message = window._(`ep_image_upload.error.${errorResponse.type}`);
- }
- } catch (err) {
- errorResponse = {message: error.responseText};
- }
-
- $('#imageUploadModalLoader').removeClass('popup-show');
- $('#imageUploadModalError .error').html(errorResponse.message);
- $('#imageUploadModalError').addClass('popup-show');
- },
- async: true,
- data: formData,
- cache: false,
- contentType: false,
- processData: false,
- timeout: 60000,
- });
+ uploadFile(context, file);
}
});
$(document).find('body').find('#imageInput').trigger('click');
diff --git a/static/tests/frontend/specs/insert.js b/static/tests/frontend/specs/insert.js
index b1eb8c6..633970f 100644
--- a/static/tests/frontend/specs/insert.js
+++ b/static/tests/frontend/specs/insert.js
@@ -1,20 +1,98 @@
'use strict';
+/**
+ * @param {number} line the line of the image
+ * @returns {string} embedded image data
+ */
+const fetchImageFromLine = async (line) => {
+ let imageUrl;
+ const inner$ = helper.padInner$;
+
+ // wait for img tag to appear
+ await helper.waitForPromise(() => {
+ const image = inner$(`div:eq(${line})`)[0].querySelector('img');
+ if (image) {
+ imageUrl = image.getAttribute('src');
+ if (/^http/.test(imageUrl)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }, 2000);
+
+ // get image
+ let embeddedImage;
+ $.ajax({
+ type: 'GET',
+ url: imageUrl,
+ success: (data) => {
+ embeddedImage = data;
+ },
+ error: (error) => {
+ throw error;
+ },
+ cache: false,
+ processData: false,
+ dataType: 'text',
+ });
+
+ // size of uploadSVG blob
+ await helper.waitForPromise(() => embeddedImage && embeddedImage.length === 214690);
+
+ return embeddedImage;
+};
+
describe('Image Upload', function () {
+ let storageType;
+
// create a new pad before each test run
- beforeEach(function (cb) {
- helper.newPad(cb);
+ beforeEach(async function () {
+ await helper.aNewPad();
+ storageType = helper.padChrome$.window.clientVars.ep_image_upload &&
+ helper.padChrome$.window.clientVars.ep_image_upload.storageType;
this.timeout(60000);
});
it('Puts an image in the pad and ensure it isnt removed', async function () {
this.timeout(10000);
const inner$ = helper.padInner$;
- inner$('div:eq(2)').html('hello world');
+
+ await helper.edit('hello world\n', 3);
+
+ // clear the first line
+ inner$('div').first().html('
');
+
+ // wait for the edit to be accepted
+ await helper.waitForPromise(() => helper.commits.length === 2);
+
+ // in case of copy&paste the image is placed at the cursor position
+ // let's put the cursor onto the first line
+ const firstLine = inner$('div:eq(0)');
+ helper.selectLines(firstLine, firstLine, 1, 1);
+
inner$('div').first().html(`
`);
- await helper.waitForPromise(() => inner$('div').first().html().indexOf(uploadSVG) !== -1, 1000);
- await helper.waitForPromise(
- () => inner$('div:eq(2)').text().indexOf('hello world') !== -1, 1000);
+
+ await helper.waitForPromise(() => helper.commits.length === 3);
+
+ if (storageType === 'base64') {
+ await helper.waitForPromise(() => inner$('div:eq(0)').html().indexOf(uploadSVG) !== -1, 1000);
+ await helper.waitForPromise(
+ () => inner$('div:eq(2)').text().indexOf('hello world') !== -1, 1000);
+ } else {
+ const image = await fetchImageFromLine(0);
+ const uploadedSVGData = uploadSVG.match(/^data:([^;]+);base64,(.*)/);
+
+ expect(window.atob(uploadedSVGData[2])).to.be(image);
+
+ // ensure the image is actually displayed
+ const height = window.getComputedStyle(inner$('div:eq(0)')[0].querySelector('img')).height;
+ expect(parseInt(height)).to.be.above(200);
+
+ // uploadFile calls `ace.ace_doReturnKey()` so there is an additional newline
+ await helper.waitForPromise(
+ () => inner$('div:eq(3)').text().indexOf('hello world') !== -1, 1000);
+ }
});
it('Puts an image in the pad and next line is not modified', async function () {
@@ -22,15 +100,39 @@ describe('Image Upload', function () {
const inner$ = helper.padInner$;
// puts hello world on second line
- inner$('div:eq(1)').html('hello world');
+ await helper.edit('hello world\n', 2);
+
await helper.waitForPromise(() => inner$('div:eq(1)').text() === 'hello world', 1000);
+ // in case of copy&paste the image is placed at the cursor position
+ // let's put the cursor onto the first line
+ const firstLine = inner$('div:eq(0)');
+ helper.selectLines(firstLine, firstLine, 1, 1);
+
// puts image on first line
inner$('div').first().html(`
`);
- await helper.waitForPromise(() => inner$('div').first().html().indexOf(uploadSVG) !== -1, 1000);
- await helper.waitForPromise(
- () => inner$('div:eq(1)').text().indexOf('hello world') !== -1, 1000);
+ // wait for the edit to be accepted
+ await helper.waitForPromise(() => helper.commits.length === 2);
+
+ if (storageType === 'base64') {
+ await helper.waitForPromise(() => inner$('div:eq(0)').html().indexOf(uploadSVG) !== -1, 1000);
+ await helper.waitForPromise(
+ () => inner$('div:eq(1)').text().indexOf('hello world') !== -1, 1000);
+ } else {
+ const image = await fetchImageFromLine(0);
+ const uploadedSVGData = uploadSVG.match(/^data:([^;]+);base64,(.*)/);
+
+ expect(window.atob(uploadedSVGData[2])).to.be(image);
+
+ // ensure the image is actually displayed
+ const height = window.getComputedStyle(inner$('div:eq(0)')[0].querySelector('img')).height;
+ expect(parseInt(height)).to.be.above(200);
+
+ // uploadFile calls `ace.ace_doReturnKey()` so there is an additional newline
+ await helper.waitForPromise(
+ () => inner$('div:eq(2)').text().indexOf('hello world') !== -1, 1000);
+ }
});
});