Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Bugfix: Only open supported file types in web viewer apps

Viewer apps in the web UI, like the PDF viewer, could be opened for any file
by entering its address directly, even when the app does not support that file
type. The file was then loaded and shown anyway.

Viewer apps now check that the file type is supported before loading the file
and show an error message otherwise.

https://github.com/owncloud/ocis/pull/13465
1 change: 1 addition & 0 deletions web/packages/web-app-pdf-viewer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const appInfo = {
extensions: [
{
extension: 'pdf',
mimeType: 'application/pdf',
routeName: 'pdf-viewer'
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ import { HttpError } from '@ownclouders/web-client'
import { dirname } from 'path'
import { useFileActionsOpenWithApp } from '../../composables/actions/files/useFileActionsOpenWithApp'
import { UnsavedChangesModal } from '../Modals'
import { formatFileSize, getSharedDriveItem } from '../../helpers'
import { formatFileSize, getSharedDriveItem, isResourceMatchingFileType } from '../../helpers'
import toNumber from 'lodash-es/toNumber'
import { useAuthService } from '../../composables/authContext/useAuthService'

Expand Down Expand Up @@ -199,6 +199,12 @@ const {

const { applicationMeta } = useAppMeta({ applicationId: applicationId, appsStore })

const isResourceSupported = computed(() => {
return appsStore.fileExtensions
.filter(({ app }) => app === applicationId)
.some((fileExtension) => isResourceMatchingFileType(unref(resource), fileExtension))
})

const fileSizeLimit = computed(() => {
return unref(applicationMeta).meta?.fileSizeLimit
})
Expand Down Expand Up @@ -360,6 +366,15 @@ const loadFileTask = useTask(function* (signal) {
}

if (unref(hasProp('url'))) {
if (!unref(isResourceSupported)) {
loadingError.value = new Error(
$gettext('This file type is not supported by %{appName}.', {
appName: $gettext(unref(applicationMeta).name)
})
)
return
}

url.value = yield getUrlForResource(unref(space), unref(resource), {
...urlForResourceOptions,
signal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { storeToRefs } from 'pinia'
import { useEmbedMode } from '../../embedMode'
import { RouteRecordName } from 'vue-router'
import { isLocationActive } from '../../../router/utils'
import { isResourceMatchingFileExtension } from '../../../helpers/resource/fileExtension'

export const EDITOR_MODE_EDIT = 'edit'
export const EDITOR_MODE_CREATE = 'create'
Expand Down Expand Up @@ -165,19 +166,7 @@ export const useFileActions = () => {
return false
}

if (resources[0].extension && fileExtension.extension) {
return resources[0].extension.toLowerCase() === fileExtension.extension.toLowerCase()
}

if (resources[0].mimeType && fileExtension.mimeType) {
return (
resources[0].mimeType.toLowerCase() === fileExtension.mimeType.toLowerCase() ||
resources[0].mimeType.split('/')[0].toLowerCase() ===
fileExtension.mimeType.toLowerCase()
)
}

return false
return isResourceMatchingFileExtension(resources[0], fileExtension)
},
hasPriority: fileExtension.hasPriority,
class: `oc-files-actions-${kebabCase(appInfo.name).toLowerCase()}-trigger`
Expand Down
47 changes: 47 additions & 0 deletions web/packages/web-pkg/src/helpers/resource/fileExtension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Resource } from '@ownclouders/web-client'
import { ApplicationFileExtension } from '../../apps/types'

/**
* Checks whether a resource matches a registered application file extension,
* either by its file extension or by its mime type.
*/
export const isResourceMatchingFileExtension = (
resource: Resource,
fileExtension: ApplicationFileExtension
): boolean => {
if (!resource || !fileExtension) {
return false
}

if (resource.extension && fileExtension.extension) {
return resource.extension.toLowerCase() === fileExtension.extension.toLowerCase()
}

if (resource.mimeType && fileExtension.mimeType) {
return (
resource.mimeType.toLowerCase() === fileExtension.mimeType.toLowerCase() ||
resource.mimeType.split('/')[0].toLowerCase() === fileExtension.mimeType.toLowerCase()
)
}

return false
}

const normalizeMimeType = (mimeType: string) => mimeType.split(';')[0].trim().toLowerCase()

/**
* Strict check for apps that render the file content directly: the mime type reported by the
* server must match the registered one. The file extension is not taken into account because
* the name of a resource (e.g. a mounted share) can differ from the name of the underlying file,
* while the mime type always reflects the latter.
*/
export const isResourceMatchingFileType = (
resource: Resource,
fileExtension: ApplicationFileExtension
): boolean => {
if (!resource?.mimeType || !fileExtension?.mimeType) {
return false
}

return normalizeMimeType(resource.mimeType) === normalizeMimeType(fileExtension.mimeType)
}
1 change: 1 addition & 0 deletions web/packages/web-pkg/src/helpers/resource/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './conflictHandling'
export * from './filter'
export * from './icon'
export * from './renameResource'
export * from './fileExtension'
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { Resource } from '@ownclouders/web-client'
import { isResourceMatchingFileExtension, isResourceMatchingFileType } from '../../../../src'

const buildResource = (data: Partial<Resource>) =>
({ id: '1', path: '', webDavPath: '', spaceId: '1', ...data }) as Resource

describe('isResourceMatchingFileExtension', () => {
it('evaluates to false if resource or file extension is nullish', () => {
expect(isResourceMatchingFileExtension(null, { extension: 'pdf' })).toBe(false)
expect(isResourceMatchingFileExtension(buildResource({ extension: 'pdf' }), null)).toBe(false)
})

it.each([
{ resourceExtension: 'pdf', extension: 'pdf', expected: true },
{ resourceExtension: 'PDF', extension: 'pdf', expected: true },
{ resourceExtension: 'html', extension: 'pdf', expected: false },
{ resourceExtension: 'svg', extension: 'pdf', expected: false }
])(
'matches by extension: $resourceExtension against $extension',
({ resourceExtension, extension, expected }) => {
expect(
isResourceMatchingFileExtension(buildResource({ extension: resourceExtension }), {
extension
})
).toBe(expected)
}
)

it('uses the extension and ignores the mime type if both have an extension', () => {
expect(
isResourceMatchingFileExtension(
buildResource({ extension: 'html', mimeType: 'application/pdf' }),
{ extension: 'pdf', mimeType: 'application/pdf' }
)
).toBe(false)
})

it.each([
{ resourceMimeType: 'application/pdf', mimeType: 'application/pdf', expected: true },
{ resourceMimeType: 'image/png', mimeType: 'image', expected: true },
{ resourceMimeType: 'text/html', mimeType: 'application/pdf', expected: false }
])(
'matches by mime type: $resourceMimeType against $mimeType',
({ resourceMimeType, mimeType, expected }) => {
expect(
isResourceMatchingFileExtension(buildResource({ mimeType: resourceMimeType }), {
mimeType
})
).toBe(expected)
}
)

it('evaluates to false if nothing can be compared', () => {
expect(isResourceMatchingFileExtension(buildResource({}), { extension: 'pdf' })).toBe(false)
})
})

describe('isResourceMatchingFileType', () => {
const pdfFileExtension = { extension: 'pdf', mimeType: 'application/pdf' }

it('evaluates to false if resource or file extension is nullish', () => {
expect(isResourceMatchingFileType(null, pdfFileExtension)).toBe(false)
expect(isResourceMatchingFileType(buildResource({ mimeType: 'application/pdf' }), null)).toBe(
false
)
})

it.each([
{ mimeType: 'application/pdf', expected: true },
{ mimeType: 'APPLICATION/PDF', expected: true },
{ mimeType: 'application/pdf; charset=UTF-8', expected: true },
{ mimeType: 'text/html', expected: false },
{ mimeType: 'image/svg+xml', expected: false },
{ mimeType: undefined, expected: false }
])('matches by mime type: $mimeType', ({ mimeType, expected }) => {
expect(
isResourceMatchingFileType(buildResource({ extension: 'pdf', mimeType }), pdfFileExtension)
).toBe(expected)
})

it('evaluates to false if the name has a matching extension but the mime type differs', () => {
// e.g. a mounted share named "report.pdf" whose underlying file was renamed to "report.html"
expect(
isResourceMatchingFileType(
buildResource({ name: 'report.pdf', extension: 'pdf', mimeType: 'text/html' }),
pdfFileExtension
)
).toBe(false)
})

it('evaluates to false if the file extension has no mime type registered', () => {
expect(
isResourceMatchingFileType(buildResource({ extension: 'pdf', mimeType: 'application/pdf' }), {
extension: 'pdf'
})
).toBe(false)
})
})
Loading