Skip to content

Commit 8893dda

Browse files
author
Julien Veyssier
committed
manual backport of #2383, fix attachment link target generation, this also fixes cleanup when there are parenthesis in the file names
Signed-off-by: Julien Veyssier <[email protected]>
1 parent 2c0b293 commit 8893dda

File tree

6 files changed

+1601
-49
lines changed

6 files changed

+1601
-49
lines changed

cypress/integration/images.spec.js

Lines changed: 124 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020
*
2121
*/
2222

23-
2423
import { randHash } from '../utils/'
2524
import 'cypress-file-upload'
25+
2626
const randUser = randHash()
2727
const randUser2 = randHash()
2828
let currentUser = randUser
@@ -32,6 +32,16 @@ const ACTION_UPLOAD_LOCAL_FILE = 1
3232
const ACTION_INSERT_FROM_FILES = 2
3333
const ACTION_INSERT_FROM_LINK = 3
3434

35+
/**
36+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
37+
* @param str string
38+
*/
39+
function fixedEncodeURIComponent(str) {
40+
return encodeURIComponent(str).replace(/[!'()*]/g, (c) => {
41+
return '%' + c.charCodeAt(0).toString(16).toUpperCase()
42+
})
43+
}
44+
3545
/**
3646
* Open the image action menu and click one action
3747
*
@@ -45,50 +55,81 @@ const clickOnImageAction = (actionIndex, callback) => {
4555
.should('have.class', 'action-item--open')
4656

4757
// get the popover ID to be able to find the related DOM element
48-
cy.get('#viewer .action-item.submenu > div.v-popover > .trigger')
58+
return cy.get('#viewer .action-item.submenu > div.v-popover > .trigger')
4959
.should('have.attr', 'aria-describedby')
50-
.should('contain', 'popover_')
51-
.then((popoverId) => {
52-
cy.log('Click on the action entry')
53-
cy.get('div#' + popoverId)
54-
.should('have.class', 'open')
55-
.find('li:nth-child(' + actionIndex + ')').click()
60+
.should('contain', 'popover_')
61+
.then((popoverId) => {
62+
cy.log('Click on the action entry')
63+
cy.get('div#' + popoverId)
64+
.should('have.class', 'open')
65+
.find('li:nth-child(' + actionIndex + ')').click()
5666
// our job here is done
57-
callback(popoverId)
58-
})
67+
callback(popoverId)
68+
})
5969
}
6070

6171
/**
6272
* Check if an image is visible in the document
6373
*
64-
* @param documentId file ID of the current document
65-
* @param imageName file name to be checked
74+
* @param {number} documentId file ID of the current document
75+
* @param {string} imageName file name to be checked
76+
* @param {number} imageId file id
77+
* @param {number|undefined} index index of image in the document
6678
*/
67-
const checkImage = (documentId, imageName, imageId) => {
68-
cy.log('Check the image is visible and well formed')
69-
cy.get('#editor .ProseMirror div.image[data-src="text://image?imageFileName=' + imageName + '"]')
70-
.should('be.visible')
71-
.find('img')
72-
.should('have.attr', 'src')
73-
.should('contain', 'apps/text/image?documentId=' + documentId)
74-
.should('contain', 'imageFileName=' + imageName)
75-
// keep track that we have created this image in the attachment dir
76-
attachmentFileNameToId[imageName] = imageId
79+
const checkImage = (documentId, imageName, imageId, index) => {
80+
const encodedName = fixedEncodeURIComponent(imageName)
81+
82+
cy.log('Check the image is visible and well formed', { documentId, imageName, imageId, index, encodedName })
83+
return new Cypress.Promise((resolve, reject) => {
84+
cy.get('#editor [data-component="image-view"]')
85+
.filter('[data-src="text://image?imageFileName=' + encodedName + '"]')
86+
.find('.image__view') // wait for load finish
87+
.within(($el) => {
88+
// keep track that we have created this image in the attachment dir
89+
if (!attachmentFileNameToId[documentId]) {
90+
attachmentFileNameToId[documentId] = {}
91+
}
92+
93+
attachmentFileNameToId[documentId][imageName] = imageId
94+
95+
if (index > 0) {
96+
expect(imageName).include(`(${index + 1})`)
97+
}
98+
99+
cy.wrap($el)
100+
.should('be.visible')
101+
.find('img')
102+
.should('have.attr', 'src')
103+
.should('contain', 'apps/text/image?documentId=' + documentId)
104+
.should('contain', 'imageFileName=' + encodeURIComponent(imageName))
105+
106+
return cy.wrap($el)
107+
.find('.image__caption input')
108+
.should('be.visible')
109+
.should('have.value', imageName)
110+
111+
})
112+
.then(resolve, reject)
113+
})
77114
}
78115

79116
/**
80117
* Wait for the image insertion request to finish and check if the image is visible
81118
*
82-
* @param requestAlias Alias of the request we are waiting for
119+
* @param {string} requestAlias Alias of the request we are waiting for
120+
* @param {number|undefined} index of image
83121
*/
84-
const waitForRequestAndCheckImage = (requestAlias) => {
85-
cy.wait('@' + requestAlias).then((req) => {
86-
// the name of the created file on NC side is returned in the response
87-
const fileId = req.response.body.id
88-
const fileName = req.response.body.name
89-
const documentId = req.response.body.documentId
90-
checkImage(documentId, fileName, fileId)
91-
cy.screenshot()
122+
const waitForRequestAndCheckImage = (requestAlias, index) => {
123+
return new Cypress.Promise((resolve, reject) => {
124+
return cy.wait('@' + requestAlias).then((req) => {
125+
// the name of the created file on NC side is returned in the response
126+
const fileId = req.response.body.id
127+
const fileName = req.response.body.name
128+
const documentId = req.response.body.documentId
129+
130+
checkImage(documentId, fileName, fileId, index)
131+
.then(resolve, reject)
132+
})
92133
})
93134
}
94135

@@ -100,6 +141,7 @@ describe('Test all image insertion methods', () => {
100141

101142
// Upload test files to user's storage
102143
cy.uploadFile('test.md', 'text/markdown')
144+
cy.uploadFile('empty.md', 'text/markdown')
103145
cy.uploadFile('github.png', 'image/png')
104146

105147
cy.nextcloudCreateUser(randUser2, 'password')
@@ -155,22 +197,52 @@ describe('Test all image insertion methods', () => {
155197
})
156198
})
157199

200+
it('Upload images with the same name', () => {
201+
cy.uploadFile('empty.md', 'text/markdown')
202+
cy.openFile('empty.md')
203+
204+
const assertImage = index => {
205+
return clickOnImageAction(ACTION_UPLOAD_LOCAL_FILE, () => {
206+
const requestAlias = `uploadRequest${index}`
207+
cy.intercept({ method: 'POST', url: '**/upload' }).as(requestAlias)
208+
209+
cy.log('Upload the file through the input', { index })
210+
cy.get('.menubar input[type="file"]').attachFile('github.png')
211+
212+
return waitForRequestAndCheckImage(requestAlias, index)
213+
})
214+
}
215+
216+
cy.wrap([0, 1, 2])
217+
.each((index) => {
218+
return new Cypress.Promise((resolve, reject) => {
219+
assertImage(index).then(resolve, reject)
220+
})
221+
})
222+
.then(() => {
223+
return cy.get('#editor [data-component="image-view"]')
224+
.should('have.length', 3)
225+
})
226+
})
227+
158228
it('test if image files are in the attachment folder', () => {
159229
// check we stored the image names/ids
160-
cy.expect(Object.keys(attachmentFileNameToId)).to.have.lengthOf(2)
161230

162-
cy.get(`#fileList tr[data-file="test.md"]`, { timeout: 10000 })
231+
cy.get('#fileList tr[data-file="test.md"]', { timeout: 10000 })
163232
.should('have.attr', 'data-id')
164233
.then((documentId) => {
234+
const files = attachmentFileNameToId[documentId]
235+
236+
cy.expect(Object.keys(files)).to.have.lengthOf(2)
165237
cy.intercept({ method: 'PROPFIND', url: '**/.attachments.' + documentId }).as('chdir')
166238
cy.openFile('.attachments.' + documentId)
167239
cy.wait('@chdir')
168240
cy.screenshot()
169-
for (const name in attachmentFileNameToId) {
241+
for (const name in files) {
170242
cy.get(`#fileList tr[data-file="${name}"]`, { timeout: 10000 })
171243
.should('exist')
172244
.should('have.attr', 'data-id')
173-
.should('eq', String(attachmentFileNameToId[name]))
245+
.should('eq', String(files[name]))
174246
}
175247
})
176248
})
@@ -191,19 +263,20 @@ describe('Test all image insertion methods', () => {
191263
cy.openFile('subFolder')
192264
cy.wait('@chdir')
193265

194-
cy.get(`#fileList tr[data-file="test.md"]`, { timeout: 10000 })
266+
cy.get('#fileList tr[data-file="test.md"]', { timeout: 10000 })
195267
.should('exist')
196268
.should('have.attr', 'data-id')
197269
.then((documentId) => {
270+
const files = attachmentFileNameToId[documentId]
198271
cy.intercept({ method: 'PROPFIND', url: '**/.attachments.' + documentId }).as('chdir')
199272
cy.openFile('.attachments.' + documentId)
200273
cy.wait('@chdir')
201274
cy.screenshot()
202-
for (const name in attachmentFileNameToId) {
275+
for (const name in files) {
203276
cy.get(`#fileList tr[data-file="${name}"]`, { timeout: 10000 })
204277
.should('exist')
205278
.should('have.attr', 'data-id')
206-
.should('eq', String(attachmentFileNameToId[name]))
279+
.should('eq', String(files[name]))
207280
}
208281
})
209282
})
@@ -216,27 +289,29 @@ describe('Test all image insertion methods', () => {
216289
cy.reloadFileList()
217290
cy.wait('@reload2')
218291

219-
cy.get(`#fileList tr[data-file="testCopied.md"]`, { timeout: 10000 })
292+
cy.get('#fileList tr[data-file="testCopied.md"]', { timeout: 10000 })
220293
.should('exist')
221294
.should('have.attr', 'data-id')
222295
.then((documentId) => {
296+
const files = attachmentFileNameToId[documentId]
297+
223298
cy.intercept({ method: 'PROPFIND', url: '**/.attachments.' + documentId }).as('chdir')
224299
cy.openFile('.attachments.' + documentId)
225300
cy.wait('@chdir')
226301
cy.screenshot()
227-
for (const name in attachmentFileNameToId) {
302+
for (const name in files) {
228303
cy.get(`#fileList tr[data-file="${name}"]`, { timeout: 10000 })
229304
.should('exist')
230305
.should('have.attr', 'data-id')
231306
// these are new copied attachment files
232307
// so they should not have the same IDs than the ones created when uploading the images
233-
.should('not.eq', String(attachmentFileNameToId[name]))
308+
.should('not.eq', String(files[name]))
234309
}
235310
})
236311
})
237312

238313
it('test if attachment folder is deleted after having deleted a markdown file', () => {
239-
cy.get(`#fileList tr[data-file="testCopied.md"]`, { timeout: 10000 })
314+
cy.get('#fileList tr[data-file="testCopied.md"]', { timeout: 10000 })
240315
.should('exist')
241316
.should('have.attr', 'data-id')
242317
.then((documentId) => {
@@ -271,7 +346,7 @@ describe('Test all image insertion methods', () => {
271346
cy.wait('@showHidden')
272347

273348
// check the attachment folder is not there
274-
cy.get(`#fileList tr[data-file="test.md"]`, { timeout: 10000 })
349+
cy.get('#fileList tr[data-file="test.md"]', { timeout: 10000 })
275350
.should('exist')
276351
.should('have.attr', 'data-id')
277352
.then((documentId) => {
@@ -288,7 +363,7 @@ describe('Test all image insertion methods', () => {
288363
cy.reloadFileList()
289364
cy.wait('@reload')
290365

291-
cy.get(`#fileList tr[data-file="testMoved.md"]`, { timeout: 10000 })
366+
cy.get('#fileList tr[data-file="testMoved.md"]', { timeout: 10000 })
292367
.should('exist')
293368
.should('have.attr', 'data-id')
294369
.then((documentId) => {
@@ -304,21 +379,23 @@ describe('Test all image insertion methods', () => {
304379
cy.reloadFileList()
305380
cy.wait('@reload2')
306381

307-
cy.get(`#fileList tr[data-file="testCopied.md"]`, { timeout: 10000 })
382+
cy.get('#fileList tr[data-file="testCopied.md"]', { timeout: 10000 })
308383
.should('exist')
309384
.should('have.attr', 'data-id')
310385
.then((documentId) => {
386+
const files = attachmentFileNameToId[documentId]
387+
311388
cy.intercept({ method: 'PROPFIND', url: '**/.attachments.' + documentId }).as('chdir')
312389
cy.openFile('.attachments.' + documentId)
313390
cy.wait('@chdir')
314391
cy.screenshot()
315-
for (const name in attachmentFileNameToId) {
392+
for (const name in files) {
316393
cy.get(`#fileList tr[data-file="${name}"]`, { timeout: 10000 })
317394
.should('exist')
318395
.should('have.attr', 'data-id')
319396
// these are new copied attachment files
320397
// so they should not have the same IDs than the ones created when uploading the images
321-
.should('not.eq', String(attachmentFileNameToId[name]))
398+
.should('not.eq', String(files[name]))
322399
}
323400
})
324401
})

src/components/EditorWrapper.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -634,7 +634,12 @@ export default {
634634
})
635635
},
636636
insertAttachmentImage(name, fileId, position = null) {
637-
const src = 'text://image?imageFileName=' + encodeURIComponent(name)
637+
// inspired by the fixedEncodeURIComponent function suggested in
638+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
639+
const src = 'text://image?imageFileName='
640+
+ encodeURIComponent(name).replace(/[!'()*]/g, (c) => {
641+
return '%' + c.charCodeAt(0).toString(16).toUpperCase()
642+
})
638643
// simply get rid of brackets to make sure link text is valid
639644
// as it does not need to be unique and matching the real file name
640645
const alt = name.replaceAll(/[[\]]/g, '')

src/nodes/ImageView.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@
2222

2323
<template>
2424
<NodeViewWrapper>
25-
<div class="image" :class="{'icon-loading': !loaded}" :data-src="src">
25+
<div class="image"
26+
data-component="image-view"
27+
:class="{'icon-loading': !loaded}"
28+
:data-src="src">
2629
<div v-if="imageLoaded && isSupportedImage"
2730
v-click-outside="() => showIcons = false"
2831
class="image__view"

tests/.phpunit.result.cache

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"version":1,"defects":[],"times":{"OCA\\Text\\Tests\\TextTest::testDummy":0.001,"OCA\\Text\\Tests\\TextTest::testGetAttachmentNamesFromContent":0.002}}

tests/TextTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ public function testDummy() {
1515
public function testGetAttachmentNamesFromContent() {
1616
$contentNames = [
1717
'aaa.png',
18+
'aaa (2).png',
19+
'aaa 2).png',
20+
'aaa (2.png',
21+
'aaa ((2.png',
22+
'aaa 2)).png',
1823
'a[a]a.png',
1924
'a(a)a.png',
2025
'a](a.png',

0 commit comments

Comments
 (0)