Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Get mimeIconUrl for media attachments without a session
In order to have non-image attachments rendered in editors without
a session, `AttachmentResolver.resolve()` should return a candidate with
the mimeType icon as url as last resort - because all endpoints from
Text `AttachmentController` require a session.

This fixes rendering of non-image attachments in RichTextReader.

Fixes: #2919

Signed-off-by: Jonas <[email protected]>
  • Loading branch information
mejo- committed Mar 16, 2023
commit 0b0d268dcfccc1bcbb209b959978f730bac3df33
19 changes: 11 additions & 8 deletions src/nodes/ImageView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -279,28 +279,30 @@ export default {
},
methods: {
async init() {
const candidates = this.$attachmentResolver.resolve(this.src)
const candidates = await this.$attachmentResolver.resolve(this.src)
return this.load(candidates)
},
async load(candidates) {
const [candidate, ...fallbacks] = candidates
return this.loadImage(candidate.url, candidate.type, candidate.name).catch((e) => {
return this.loadImage(candidate.url, candidate.type, candidate.name, candidate.metadata).catch((e) => {
if (fallbacks.length > 0) {
return this.load(fallbacks)
// TODO if fallback works, rewrite the url with correct document ID
}
return Promise.reject(e)
})
},
async loadImage(imageUrl, attachmentType, name = null) {
async loadImage(imageUrl, attachmentType, name = null, metadata = null) {
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = async () => {
this.imageUrl = imageUrl
this.imageLoaded = true
this.loaded = true
this.attachmentType = attachmentType
if (attachmentType === this.$attachmentResolver.ATTACHMENT_TYPE_MEDIA) {
if (attachmentType === this.$attachmentResolver.ATTACHMENT_TYPE_MEDIA && metadata) {
this.attachmentMetadata = metadata
} else if (attachmentType === this.$attachmentResolver.ATTACHMENT_TYPE_MEDIA) {
await this.loadMediaMetadata(name)
}
resolve(imageUrl)
Expand Down Expand Up @@ -339,20 +341,21 @@ export default {
onLoaded() {
this.loaded = true
},
handleImageClick(src) {
async handleImageClick(src) {
const imageViews = Array.from(document.querySelectorAll('figure[data-component="image-view"].image-view'))
let basename, relativePath

imageViews.forEach(imgv => {
for (const imgv of imageViews) {
relativePath = imgv.getAttribute('data-src')
basename = relativePath.split('/').slice(-1).join()
const { url: source } = this.$attachmentResolver.resolve(relativePath, true).shift()
const response = await this.$attachmentResolver.resolve(relativePath, true)
const { url: source } = response.shift()
this.embeddedImagesList.push({
source,
basename,
relativePath,
})
})
}
this.imageIndex = this.embeddedImagesList.findIndex(image => image.relativePath === src)
this.showImageModal = true
},
Expand Down
42 changes: 38 additions & 4 deletions src/services/AttachmentResolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

import { generateUrl, generateRemoteUrl } from '@nextcloud/router'
import pathNormalize from 'path-normalize'
import axios from '@nextcloud/axios'
import { formatFileSize } from '@nextcloud/files'

import { logger } from '../helpers/logger.js'

Expand Down Expand Up @@ -53,7 +55,7 @@ export default class AttachmentResolver {
*
* Currently returns either one or two urls.
*/
resolve(src, preferRawImage = false) {
async resolve(src, preferRawImage = false) {
if (this.#session && src.startsWith('text://')) {
const imageFileName = getQueryVariable(src, 'imageFileName')
return [{
Expand All @@ -62,6 +64,7 @@ export default class AttachmentResolver {
}]
}

// Has session and URL points to attachment from current document
if (this.#session && src.startsWith(`.attachments.${this.#session?.documentId}/`)) {
const imageFileName = decodeURIComponent(src.replace(`.attachments.${this.#session?.documentId}/`, '').split('?')[0])
return [
Expand Down Expand Up @@ -91,11 +94,11 @@ export default class AttachmentResolver {
}]
}

// if it starts with '.attachments.1234/'
if (src.match(/^\.attachments\.\d+\//)) {
// Has session and URL points to attachment from a (different) text document
if (this.#session && src.match(/^\.attachments\.\d+\//)) {
const imageFileName = this.#relativePath(src)
.replace(/\.attachments\.\d+\//, '')
// try the webdav url and attachment API if it fails
// Try webdav URL, use attachment API as fallback
return [
{
type: this.ATTACHMENT_TYPE_IMAGE,
Expand All @@ -113,6 +116,26 @@ export default class AttachmentResolver {
]
}

// Doesn't have session and URL points to attachment from (current or different) text document
if (!this.#session && src.match(/^\.attachments\.\d+\//)) {
const imageFileName = this.#relativePath(src)
.replace(/\.attachments\.\d+\//, '')
const { mimeType, size } = await this.getMetadata(this.#davUrl(src))
// Without session, use webdav URL for images and mimetype icon for media attachments
return [
{
type: this.ATTACHMENT_TYPE_IMAGE,
url: this.#davUrl(src),
},
{
type: this.ATTACHMENT_TYPE_MEDIA,
url: this.getMimeUrl(mimeType),
metadata: { size },
name: imageFileName,
},
]
}

return [{
type: this.ATTACHMENT_TYPE_IMAGE,
url: this.#davUrl(src),
Expand Down Expand Up @@ -249,6 +272,17 @@ export default class AttachmentResolver {
return pathNormalize(f)
}

async getMetadata(src) {
const headResponse = await axios.head(src)
const mimeType = headResponse.headers['content-type']
const size = formatFileSize(headResponse.headers['content-length'])
return { mimeType, size }
}

getMimeUrl(mimeType) {
return mimeType ? OC.MimeType.getIconUrl(mimeType) : null
}

}

/**
Expand Down
59 changes: 31 additions & 28 deletions src/tests/services/AttachmentResolver.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,41 +19,41 @@ describe('Image resolver', () => {
expect(resolver).toBeInstanceOf(AttachmentResolver)
})

it('handles text:// urls via Text API', () => {
it('handles text:// urls via Text API', async () => {
const src = 'text://image?imageFileName=group%20pic.jpg'
const resolver = new AttachmentResolver({ session })
const [candidate] = resolver.resolve(src)
const [candidate] = await resolver.resolve(src)
expect(candidate.type).toBe('image')
expect(candidate.url).toBe('/nc-webroot/apps/text/image?documentId=4173&sessionId=456&sessionToken=mySessionToken&imageFileName=group%20pic.jpg&preferRawImage=0')
})

it('handles text:// urls with token via Text API', () => {
it('handles text:// urls with token via Text API', async () => {
const src = 'text://image?imageFileName=group%20pic.jpg'
const resolver = new AttachmentResolver({
session,
shareToken: 'myShareToken',
})
const [candidate] = resolver.resolve(src)
const [candidate] = await resolver.resolve(src)
expect(candidate.type).toBe('image')
expect(candidate.url).toBe('/nc-webroot/apps/text/image?documentId=4173&sessionId=456&sessionToken=mySessionToken&imageFileName=group%20pic.jpg&shareToken=myShareToken&preferRawImage=0')
})

it('uses user auth over token auth', () => {
it('uses user auth over token auth', async () => {
const src = 'text://image?imageFileName=group%20pic.jpg'
const resolver = new AttachmentResolver({
session,
user,
shareToken: 'myShareToken',
})
const [candidate] = resolver.resolve(src)
const [candidate] = await resolver.resolve(src)
expect(candidate.type).toBe('image')
expect(candidate.url).not.toContain('myShareToken')
})

it('handles .attachments urls via Text API', () => {
it('handles .attachments urls to own fileId via Text API', async () => {
const src = `.attachments.${session.documentId}/group%20pic.jpg`
const resolver = new AttachmentResolver({ session })
const [candidate, fallbackCandidate] = resolver.resolve(src)
const [candidate, fallbackCandidate] = await resolver.resolve(src)
expect(candidate.type).toBe('image')
expect(candidate.url).toBe('/nc-webroot/apps/text/image?documentId=4173&sessionId=456&sessionToken=mySessionToken&imageFileName=group%20pic.jpg&preferRawImage=0')
expect(fallbackCandidate.type).toBe('media')
Expand All @@ -63,13 +63,13 @@ describe('Image resolver', () => {
expect(metadataUrl).toBe('/nc-webroot/apps/text/mediaMetadata?documentId=4173&sessionId=456&sessionToken=mySessionToken&mediaFileName=group%20pic.jpg')
})

it('handles .attachments urls with token via Text API', () => {
it('handles .attachments urls to own fileId with token via Text API', async () => {
const src = `.attachments.${session.documentId}/group%20pic.jpg`
const resolver = new AttachmentResolver({
session,
shareToken,
})
const [candidate, fallbackCandidate] = resolver.resolve(src)
const [candidate, fallbackCandidate] = await resolver.resolve(src)
expect(candidate.type).toBe('image')
expect(candidate.url).toBe('/nc-webroot/apps/text/image?documentId=4173&sessionId=456&sessionToken=mySessionToken&imageFileName=group%20pic.jpg&shareToken=myShareToken&preferRawImage=0')
expect(fallbackCandidate.type).toBe('media')
Expand All @@ -79,7 +79,7 @@ describe('Image resolver', () => {
expect(metadataUrl).toBe('/nc-webroot/apps/text/mediaMetadata?documentId=4173&sessionId=456&sessionToken=mySessionToken&mediaFileName=group%20pic.jpg&shareToken=myShareToken')
})

it('leaves urls unchanged if they can be loaded directly', () => {
it('leaves urls unchanged if they can be loaded directly', async () => {
const sources = [
'http://nextcloud.com/pic.jpg',
'https://nextcloud.com/pic.jpg',
Expand All @@ -89,42 +89,42 @@ describe('Image resolver', () => {
]
const resolver = new AttachmentResolver({ })
for (const src of sources) {
const [candidate] = resolver.resolve(src)
const [candidate] = await resolver.resolve(src)
expect(candidate.type).toBe('image')
expect(candidate.url).toBe(src)
}
})

it('uses fileId for preview', () => {
it('uses fileId for preview', async () => {
const src = '/Media/photo.jpeg?fileId=7#mimetype=image%2Fjpeg&hasPreview=true'
const resolver = new AttachmentResolver({ user })
const [candidate] = resolver.resolve(src)
const [candidate] = await resolver.resolve(src)
expect(candidate.type).toBe('image')
expect(candidate.url).toContain('/core/preview?fileId=7')
})

it('uses .png endpoint if no fileId is given', () => {
it('uses .png endpoint if no fileId is given', async () => {
const src = '/Media/photo.jpeg?mimetype=image%2Fjpeg&hasPreview=true'
const resolver = new AttachmentResolver({ user })
const [candidate] = resolver.resolve(src)
const [candidate] = await resolver.resolve(src)
expect(candidate.type).toBe('image')
expect(candidate.url).toBe('/nc-webroot/core/preview.png?file=%2FMedia%2Fphoto.jpeg&x=1024&y=1024&a=true')
})

it('retrieves public preview by path', () => {
it('retrieves public preview by path', async () => {
const src = '/Media/photo.jpeg?fileId=7#mimetype=image%2Fjpeg&hasPreview=true'
const resolver = new AttachmentResolver({
shareToken: 'SHARE_TOKEN'
})
const [candidate] = resolver.resolve(src)
const [candidate] = await resolver.resolve(src)
expect(candidate.type).toBe('image')
expect(candidate.url).toBe('/nc-webroot/apps/files_sharing/publicpreview/SHARE_TOKEN?file=%2FMedia%2Fphoto.jpeg&x=1024&y=1024&a=true')
})

it('handles old .attachments urls via webdav with text API fallback', () => {
it('handles .attachments urls to different fileId via webdav with text API fallback', async () => {
const src = `.attachments.${session.documentId + 1}/group%20pic.jpg`
const resolver = new AttachmentResolver({ session, user, currentDirectory })
const [candidate, fallbackCandidate, secondFallback] = resolver.resolve(src)
const [candidate, fallbackCandidate, secondFallback] = await resolver.resolve(src)
expect(candidate.type).toBe('image')
expect(candidate.url).toBe('http://localhost/nc-webroot/remote.php/dav/files/user-uid/parentDir/.attachments.4174/group%20pic.jpg')
expect(fallbackCandidate.type).toBe('image')
Expand All @@ -138,38 +138,41 @@ describe('Image resolver', () => {

describe('missing session', () => {

it('resolves text:// urls as authenticated dav', () => {
it('resolves text:// urls as authenticated dav', async () => {
const src = 'text://image?imageFileName=group%20pic.jpg'
const resolver = new AttachmentResolver({
fileId: 4173,
user,
currentDirectory,
})
const [candidate] = resolver.resolve(src)
const [candidate] = await resolver.resolve(src)
expect(candidate.type).toBe('image')
expect(candidate.url).toBe('http://localhost/nc-webroot/remote.php/dav/files/user-uid/parentDir/.attachments.4173/group%20pic.jpg')
})

it('resolves text:// urls as share token download', () => {
it('resolves text:// urls as share token download', async () => {
const src = 'text://image?imageFileName=group%20pic.jpg'
const resolver = new AttachmentResolver({
fileId,
shareToken,
currentDirectory,
})
const [candidate] = resolver.resolve(src)
const [candidate] = await resolver.resolve(src)
expect(candidate.type).toBe('image')
expect(candidate.url).toBe('/nc-webroot/s/myShareToken/download?path=%2FparentDir%2F.attachments.4173&files=group%20pic.jpg')
})

it('handles .attachments urls for other fileIds via webdav with webdav fallback', () => {
it('handles .attachments urls via webdav with mimetype URL fallback', async () => {
const src = `.attachments.${session.documentId + 1}/group%20pic.jpg`
const resolver = new AttachmentResolver({ user, currentDirectory, fileId })
const [candidate, fallbackCandidate] = resolver.resolve(src)

jest.spyOn(resolver, 'getMetadata').mockReturnValue({mimetype: 'application/pdf', size: '1 KB'})
jest.spyOn(resolver, 'getMimeUrl').mockReturnValue('/nc-webroot/apps/theming/img/core/filetypes/application-pdf.svg')
const [candidate, fallbackCandidate] = await resolver.resolve(src)
expect(candidate.type).toBe('image')
expect(candidate.url).toBe('http://localhost/nc-webroot/remote.php/dav/files/user-uid/parentDir/.attachments.4174/group%20pic.jpg')
expect(fallbackCandidate.type).toBe('image')
expect(fallbackCandidate.url).toBe('http://localhost/nc-webroot/remote.php/dav/files/user-uid/parentDir/.attachments.4173/group%20pic.jpg')
expect(fallbackCandidate.type).toBe('media')
expect(fallbackCandidate.url).toBe('/nc-webroot/apps/theming/img/core/filetypes/application-pdf.svg')
})

})
Expand Down