import: Ajaxify pad import

This eliminates an inline script (good for Content Security Policy)
and improves the user experience.
pull/4779/head
Richard Hansen 2020-10-05 22:22:44 -04:00 committed by John McLear
parent fba55fa6cf
commit b711ff6acf
3 changed files with 76 additions and 67 deletions

View File

@ -258,21 +258,18 @@ const doImport = async (req, res, padId) => {
};
exports.doImport = async (req, res, padId) => {
let status = 'ok';
let httpStatus = 200;
let code = 0;
let message = 'ok';
let directDatabaseAccess;
try {
directDatabaseAccess = await doImport(req, res, padId);
} catch (err) {
if (!(err instanceof ImportError) || !err.status) throw err;
status = err.status;
const known = err instanceof ImportError && err.status;
if (!known) logger.error(`Internal error during import: ${err.stack || err}`);
httpStatus = known ? 400 : 500;
code = known ? 1 : 2;
message = known ? err.status : 'internalError';
}
// close the connection
res.send([
'<script>',
"document.addEventListener('DOMContentLoaded', () => {",
' window.parent.padimpexp.handleFrameCall(',
` ${JSON.stringify(directDatabaseAccess)}, ${JSON.stringify(status)});`,
'});',
'</script>',
].join('\n'));
res.status(httpStatus).json({code, message, data: {directDatabaseAccess}});
};

View File

@ -23,9 +23,9 @@
*/
const padimpexp = (() => {
// /// import
let currentImportTimer = null;
let pad;
// /// import
const addImportFrames = () => {
$('#import .importframe').remove();
const iframe = $('<iframe>')
@ -42,35 +42,38 @@ const padimpexp = (() => {
$('#importmessagefail').fadeOut('fast');
};
const fileInputSubmit = () => {
const fileInputSubmit = (e) => {
e.preventDefault();
$('#importmessagefail').fadeOut('fast');
if (!window.confirm(html10n.get('pad.impexp.confirmimport'))) return false;
currentImportTimer = window.setTimeout(() => {
if (!currentImportTimer) return;
currentImportTimer = null;
importErrorMessage('Request timed out.');
importDone();
}, 25000); // time out after some number of seconds
if (!window.confirm(html10n.get('pad.impexp.confirmimport'))) return;
$('#importsubmitinput').attr({disabled: true}).val(html10n.get('pad.impexp.importing'));
window.setTimeout(() => $('#importfileinput').attr({disabled: true}), 0);
$('#importarrow').stop(true, true).hide();
$('#importstatusball').show();
return true;
};
const importDone = () => {
$('#importsubmitinput').removeAttr('disabled').val(html10n.get('pad.impexp.importbutton'));
window.setTimeout(() => $('#importfileinput').removeAttr('disabled'), 0);
$('#importstatusball').hide();
importClearTimeout();
addImportFrames();
};
const importClearTimeout = () => {
if (currentImportTimer) {
window.clearTimeout(currentImportTimer);
currentImportTimer = null;
}
(async () => {
const {code, message, data: {directDatabaseAccess} = {}} = await $.ajax({
url: `${window.location.href.split('?')[0].split('#')[0]}/import`,
method: 'POST',
data: new FormData(this),
processData: false,
contentType: false,
dataType: 'json',
timeout: 25000,
}).catch((err) => {
if (err.responseJSON) return err.responseJSON;
return {code: 2, message: 'Unknown import error'};
});
if (code !== 0) {
importErrorMessage(message);
} else {
$('#import_export').removeClass('popup-show');
if (directDatabaseAccess) pad.switchToPad(clientVars.padId);
}
$('#importsubmitinput').removeAttr('disabled').val(html10n.get('pad.impexp.importbutton'));
window.setTimeout(() => $('#importfileinput').removeAttr('disabled'), 0);
$('#importstatusball').hide();
addImportFrames();
})();
};
const importErrorMessage = (status) => {
@ -118,7 +121,6 @@ const padimpexp = (() => {
}
// ///
let pad = undefined;
const self = {
init: (_pad) => {
pad = _pad;
@ -126,9 +128,6 @@ const padimpexp = (() => {
// get /p/padname
// if /p/ isn't available due to a rewrite we use the clientVars padId
const padRootPath = /.*\/p\/[^/]+/.exec(document.location.pathname) || clientVars.padId;
// get http://example.com/p/padname without Params
const dl = document.location;
const padRootUrl = `${dl.protocol}//${dl.host}${dl.pathname}`;
// i10l buttom import
$('#importsubmitinput').val(html10n.get('pad.impexp.importbutton'));
@ -141,9 +140,6 @@ const padimpexp = (() => {
$('#exportetherpada').attr('href', `${padRootPath}/export/etherpad`);
$('#exportplaina').attr('href', `${padRootPath}/export/txt`);
// activate action to import in the form
$('#importform').attr('action', `${padRootUrl}/import`);
// hide stuff thats not avaible if abiword/soffice is disabled
if (clientVars.exportAvailable === 'no') {
$('#exportworda').remove();
@ -170,21 +166,6 @@ const padimpexp = (() => {
$('#importform').unbind('submit').submit(fileInputSubmit);
$('.disabledexport').click(cantExport);
},
handleFrameCall: (directDatabaseAccess, status) => {
if (status !== 'ok') {
importErrorMessage(status);
} else {
$('#import_export').removeClass('popup-show');
}
if (directDatabaseAccess) {
// Switch to the pad without redrawing the page
pad.switchToPad(clientVars.padId);
$('#import_export').removeClass('popup-show');
}
importDone();
},
disable: () => {
$('#impexp-disabled-clickcatcher').show();
$('#import').css('opacity', 0.5);

View File

@ -127,6 +127,8 @@ describe(__filename, function () {
describe('Import/Export tests requiring AbiWord/LibreOffice', function () {
this.timeout(60000);
before(async function () {
if ((!settings.abiword || settings.abiword.indexOf('/') === -1) &&
(!settings.soffice || settings.soffice.indexOf('/') === -1)) {
@ -141,7 +143,12 @@ describe(__filename, function () {
await agent.post(`/p/${testPadId}/import`)
.attach('file', wordDoc, {filename: '/test.doc', contentType: 'application/msword'})
.expect(200)
.expect(/FrameCall\('undefined', 'ok'\);/);
.expect('Content-Type', /json/)
.expect((res) => assert.deepEqual(res.body, {
code: 0,
message: 'ok',
data: {directDatabaseAccess: false},
}));
});
it('exports DOC', async function () {
@ -161,7 +168,12 @@ describe(__filename, function () {
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
})
.expect(200)
.expect(/FrameCall\('undefined', 'ok'\);/);
.expect('Content-Type', /json/)
.expect((res) => assert.deepEqual(res.body, {
code: 0,
message: 'ok',
data: {directDatabaseAccess: false},
}));
});
it('exports DOC from imported DOCX', async function () {
@ -177,7 +189,12 @@ describe(__filename, function () {
await agent.post(`/p/${testPadId}/import`)
.attach('file', pdfDoc, {filename: '/test.pdf', contentType: 'application/pdf'})
.expect(200)
.expect(/FrameCall\('undefined', 'ok'\);/);
.expect('Content-Type', /json/)
.expect((res) => assert.deepEqual(res.body, {
code: 0,
message: 'ok',
data: {directDatabaseAccess: false},
}));
});
it('exports PDF', async function () {
@ -193,7 +210,12 @@ describe(__filename, function () {
await agent.post(`/p/${testPadId}/import`)
.attach('file', odtDoc, {filename: '/test.odt', contentType: 'application/odt'})
.expect(200)
.expect(/FrameCall\('undefined', 'ok'\);/);
.expect('Content-Type', /json/)
.expect((res) => assert.deepEqual(res.body, {
code: 0,
message: 'ok',
data: {directDatabaseAccess: false},
}));
});
it('exports ODT', async function () {
@ -213,7 +235,12 @@ describe(__filename, function () {
contentType: 'application/etherpad',
})
.expect(200)
.expect(/FrameCall\('true', 'ok'\);/);
.expect('Content-Type', /json/)
.expect((res) => assert.deepEqual(res.body, {
code: 0,
message: 'ok',
data: {directDatabaseAccess: true},
}));
});
it('exports Etherpad', async function () {
@ -237,8 +264,12 @@ describe(__filename, function () {
settings.allowUnknownFileEnds = false;
await agent.post(`/p/${testPadId}/import`)
.attach('file', padText, {filename: '/test.xasdasdxx', contentType: 'weirdness/jobby'})
.expect(200)
.expect((res) => assert.doesNotMatch(res.text, /FrameCall\('undefined', 'ok'\);/));
.expect(400)
.expect('Content-Type', /json/)
.expect((res) => {
assert.equal(res.body.code, 1);
assert.equal(res.body.message, 'uploadFailed');
});
});
describe('Import authorization checks', function () {