diff --git a/app/server/lib/DocApi.ts b/app/server/lib/DocApi.ts index d6ff0702ff..35e6ec6ac0 100644 --- a/app/server/lib/DocApi.ts +++ b/app/server/lib/DocApi.ts @@ -530,11 +530,11 @@ export class DocWorkerApi { })); // Returns the status of any current / pending attachment transfers - this._app.get('/api/docs/:docId/attachments/transferStatus', isOwner, withDoc(async (activeDoc, req, res) => { + this._app.get('/api/docs/:docId/attachments/transferStatus', canView, withDoc(async (activeDoc, req, res) => { res.json(await activeDoc.attachmentTransferStatus()); })); - this._app.get('/api/docs/:docId/attachments/store', isOwner, + this._app.get('/api/docs/:docId/attachments/store', canView, withDoc(async (activeDoc, req, res) => { const storeId = await activeDoc.getAttachmentStore(); res.json({ diff --git a/app/server/lib/uploads.ts b/app/server/lib/uploads.ts index 08c3585207..1f187459dc 100644 --- a/app/server/lib/uploads.ts +++ b/app/server/lib/uploads.ts @@ -1,7 +1,7 @@ import {ApiError} from 'app/common/ApiError'; import {InactivityTimer} from 'app/common/InactivityTimer'; import {FetchUrlOptions, FileUploadResult, UPLOAD_URL_PATH, UploadResult} from 'app/common/uploads'; -import {getUrlFromPrefix} from 'app/common/UserAPI'; +import {DocAttachmentsLocation, getUrlFromPrefix} from 'app/common/UserAPI'; import {getAuthorizedUserId, getTransitiveHeaders, getUserId, isSingleUserMode, RequestWithLogin} from 'app/server/lib/Authorizer'; import {expressWrap} from 'app/server/lib/expressWrap'; @@ -426,9 +426,31 @@ export async function fetchDoc( // The backend needs to be well configured for this to work. const { selfPrefix, docWorker } = await getDocWorkerInfoOrSelfPrefix(docId, docWorkerMap, server.getTag()); const docWorkerUrl = docWorker ? docWorker.internalUrl : getUrlFromPrefix(server.getHomeInternalUrl(), selfPrefix); + const apiBaseUrl = docWorkerUrl.replace(/\/*$/, '/'); + + // Documents with external attachments can't be copied right now. Check status and alert the user. + // Copying as a template is fine, as no attachments will be copied. + if (!template) { + const transferStatusResponse = await fetch( + new URL(`/api/docs/${docId}/attachments/transferStatus`, apiBaseUrl).href, + { + headers: { + ...headers, + 'Content-Type': 'application/json', + } + } + ); + if (!transferStatusResponse.ok) { + throw new ApiError(await transferStatusResponse.text(), transferStatusResponse.status); + } + const attachmentsLocation: DocAttachmentsLocation = (await transferStatusResponse.json()).locationSummary; + if (attachmentsLocation !== 'internal' && attachmentsLocation !== 'none') { + throw new ApiError("Cannot copy a document with external attachments", 400); + } + } + // Download the document, in full or as a template. - const url = new URL(`api/docs/${docId}/download?template=${Number(template)}`, - docWorkerUrl.replace(/\/*$/, '/')); + const url = new URL(`api/docs/${docId}/download?template=${Number(template)}`, apiBaseUrl); return _fetchURL(url.href, accessId, {headers}); } diff --git a/test/server/lib/DocApi.ts b/test/server/lib/DocApi.ts index e72b1abf13..06622b75d5 100644 --- a/test/server/lib/DocApi.ts +++ b/test/server/lib/DocApi.ts @@ -2433,23 +2433,32 @@ function testDocApi(settings: { }); + async function addAttachmentsToDoc(docId: string, attachments: {name: string, contents: string}[], + user: AxiosRequestConfig = chimpy) { + const formData = new FormData(); + for (const attachment of attachments) { + formData.append('upload', attachment.contents, attachment.name); + } + const resp = await axios.post(`${serverUrl}/api/docs/${docId}/attachments`, formData, + defaultsDeep({headers: formData.getHeaders()}, user)); + assert.equal(resp.status, 200); + assert.equal(resp.data.length, attachments.length); + return resp; + } + describe('attachments', function () { it("POST /docs/{did}/attachments adds attachments", async function () { - let formData = new FormData(); - formData.append('upload', 'foobar', "hello.doc"); - formData.append('upload', '123456', "world.jpg"); - let resp = await axios.post(`${serverUrl}/api/docs/${docIds.TestDoc}/attachments`, formData, - defaultsDeep({headers: formData.getHeaders()}, chimpy)); - assert.equal(resp.status, 200); - assert.deepEqual(resp.data, [1, 2]); + const uploadResp = await addAttachmentsToDoc(docIds.TestDoc, [ + { name: 'hello.doc', contents: 'foobar' }, + { name: 'world.jpg', contents: '123456' }, + ], chimpy); + assert.deepEqual(uploadResp.data, [1, 2]); // Another upload gets the next number. - formData = new FormData(); - formData.append('upload', 'abcdef', "hello.png"); - resp = await axios.post(`${serverUrl}/api/docs/${docIds.TestDoc}/attachments`, formData, - defaultsDeep({headers: formData.getHeaders()}, chimpy)); - assert.equal(resp.status, 200); - assert.deepEqual(resp.data, [3]); + const upload2Resp = await addAttachmentsToDoc(docIds.TestDoc, [ + { name: 'hello.png', contents: 'abcdef' }, + ], chimpy); + assert.deepEqual(upload2Resp.data, [3]); }); it("GET /docs/{did}/attachments lists attachment metadata", async function () { @@ -2753,13 +2762,11 @@ function testDocApi(settings: { docId = await userApi.newDoc({name: 'TestDocExternalAttachments'}, wid); docUrl = `${serverUrl}/api/docs/${docId}`; - const formData = new FormData(); - formData.append('upload', 'foobar', "hello.doc"); - formData.append('upload', '123456', "world.jpg"); - formData.append('upload', 'foobar', "hello2.doc"); - const resp = await axios.post(`${docUrl}/attachments`, formData, - defaultsDeep({headers: formData.getHeaders()}, chimpy)); - assert.equal(resp.status, 200); + const resp = await addAttachmentsToDoc(docId, [ + { name: 'hello.doc', contents: 'foobar' }, + { name: 'world.jpg', contents: '123456' }, + { name: 'hello2.doc', contents: 'foobar' } + ], chimpy); assert.deepEqual(resp.data, [1, 2, 3]); }); @@ -2804,6 +2811,23 @@ function testDocApi(settings: { locationSummary: "internal", }); }); + + it("POST /docs/{did}/copy fails when the document has external attachments", async function () { + const worker1 = await userApi.getWorkerAPI(docId); + await assert.isRejected(worker1.copyDoc(docId, undefined, 'copy'), /status 400/); + }); + + it("POST /docs/{did} with sourceDocId fails to copy a document with external attachments", async function () { + const chimpyWs = await userApi.newWorkspace({name: "Chimpy's Workspace"}, ORG_NAME); + const resp = await axios.post(`${serverUrl}/api/docs`, { + sourceDocumentId: docId, + documentName: 'copy of TestDocExternalAttachments', + asTemplate: false, + workspaceId: chimpyWs + }, chimpy); + assert.equal(resp.status, 400); + assert.match(resp.data.error, /external attachments/); + }); }); });