From c89e3618deb5b49ed3af6025f1802f14102ea0c7 Mon Sep 17 00:00:00 2001 From: yuuhikaze Date: Wed, 1 Apr 2026 18:13:22 -0500 Subject: [PATCH] feat: add intent parameter to distinguish file preview from download - Add 'intent' parameter support to getFileUrl() with 'preview' or 'download' values - Update all viewer apps to pass intent: 'preview': - Preview app (images, videos, audio) - PDF viewer - EPUB reader - Text editor - Add intent: 'download' to download composable - Include intent in preview service thumbnail URLs - Modify addIntentToUrl() to append intent as query parameter - Pass X-OC-Intent header for inline file access This enables the audit service to distinguish between file views and downloads for compliance tracking, with intent metadata propagating through HTTP headers and query parameters to the backend services. Co-Authored-By: Claude Haiku 4.5 --- packages/web-app-epub-reader/src/index.ts | 3 +++ packages/web-app-pdf-viewer/src/index.ts | 3 ++- packages/web-app-preview/src/index.ts | 3 ++- packages/web-app-text-editor/src/index.ts | 5 +++- packages/web-client/src/webdav/getFileUrl.ts | 23 +++++++++++++++++-- .../composables/download/useDownloadFile.ts | 3 ++- .../src/services/preview/previewService.ts | 1 + 7 files changed, 35 insertions(+), 6 deletions(-) diff --git a/packages/web-app-epub-reader/src/index.ts b/packages/web-app-epub-reader/src/index.ts index af1f625308..ba223b1121 100644 --- a/packages/web-app-epub-reader/src/index.ts +++ b/packages/web-app-epub-reader/src/index.ts @@ -18,6 +18,9 @@ export default defineWebApplication({ applicationId: appId, fileContentOptions: { responseType: 'blob' + }, + urlForResourceOptions: { + intent: 'preview' } }) }, diff --git a/packages/web-app-pdf-viewer/src/index.ts b/packages/web-app-pdf-viewer/src/index.ts index 9a5ad6cc66..f16736152c 100644 --- a/packages/web-app-pdf-viewer/src/index.ts +++ b/packages/web-app-pdf-viewer/src/index.ts @@ -13,7 +13,8 @@ const routes = [ component: AppWrapperRoute(PdfViewer, { applicationId: 'pdf-viewer', urlForResourceOptions: { - disposition: 'inline' + disposition: 'inline', + intent: 'preview' } }), name: 'pdf-viewer', diff --git a/packages/web-app-preview/src/index.ts b/packages/web-app-preview/src/index.ts index 55cab66bcb..6b3d11c0fe 100644 --- a/packages/web-app-preview/src/index.ts +++ b/packages/web-app-preview/src/index.ts @@ -21,7 +21,8 @@ export default defineWebApplication({ component: AppWrapperRoute(App, { applicationId: appId, urlForResourceOptions: { - disposition: 'inline' + disposition: 'inline', + intent: 'preview' } }), name: 'media', diff --git a/packages/web-app-text-editor/src/index.ts b/packages/web-app-text-editor/src/index.ts index 12ad36ad56..da804467dc 100644 --- a/packages/web-app-text-editor/src/index.ts +++ b/packages/web-app-text-editor/src/index.ts @@ -93,7 +93,10 @@ export default defineWebApplication({ { path: '/:driveAliasAndItem(.*)?', component: AppWrapperRoute(TextEditor, { - applicationId: appId + applicationId: appId, + fileContentOptions: { + intent: 'preview' + } }), name: 'text-editor', meta: { diff --git a/packages/web-client/src/webdav/getFileUrl.ts b/packages/web-client/src/webdav/getFileUrl.ts index e4f1d04a22..1ee7aad5e5 100644 --- a/packages/web-client/src/webdav/getFileUrl.ts +++ b/packages/web-client/src/webdav/getFileUrl.ts @@ -7,6 +7,19 @@ import { ocs } from '../ocs' import { GetFileInfoFactory } from './getFileInfo' import { DavProperty } from './constants' +/** + * Add intent query parameter to URL for audit logging + * @param url The original URL + * @param intent The access intent ('preview' or 'download') + * @returns URL with intent parameter added + */ +const addIntentToUrl = (url: string, intent: 'preview' | 'download'): string => { + if (!url) return url + const urlObj = new URL(url, window.location.origin) + urlObj.searchParams.set('intent', intent) + return urlObj.toString() +} + export const GetFileUrlFactory = ( dav: DAV, getFileContentsFactory: ReturnType, @@ -21,11 +34,13 @@ export const GetFileUrlFactory = ( disposition = 'attachment', version = null, username = '', + intent = 'download', ...opts }: { disposition?: 'inline' | 'attachment' version?: string username?: string + intent?: 'preview' | 'download' } & DAVRequestOptions ): Promise { // FIXME: re-introduce query parameters @@ -40,6 +55,10 @@ export const GetFileUrlFactory = ( if (disposition === 'inline') { const response = await getFileContentsFactory.getFileContents(space, resource, { responseType: 'blob', + headers: { + 'X-OC-Intent': intent, + ...opts.headers + }, ...opts }) return URL.createObjectURL(response.body) @@ -56,13 +75,13 @@ export const GetFileUrlFactory = ( } if (resource.downloadURL) { - return resource.downloadURL + return addIntentToUrl(resource.downloadURL, intent) } const { downloadURL } = await getFileInfoFactory.getFileInfo(space, resource, { davProperties: [DavProperty.DownloadURL] }) - return downloadURL + return addIntentToUrl(downloadURL, intent) }, revokeUrl: (url: string) => { if (url && url.startsWith('blob:')) { diff --git a/packages/web-pkg/src/composables/download/useDownloadFile.ts b/packages/web-pkg/src/composables/download/useDownloadFile.ts index 539750fc6f..67e4d87ddd 100644 --- a/packages/web-pkg/src/composables/download/useDownloadFile.ts +++ b/packages/web-pkg/src/composables/download/useDownloadFile.ts @@ -28,7 +28,8 @@ export const useDownloadFile = (options?: DownloadFileOptions) => { } const url = await clientService.webdav.getFileUrl(space, file, { version, - username: userStore.user?.onPremisesSamAccountName + username: userStore.user?.onPremisesSamAccountName, + intent: 'download' }) triggerDownloadWithFilename(url, file.name) } catch (e) { diff --git a/packages/web-pkg/src/services/preview/previewService.ts b/packages/web-pkg/src/services/preview/previewService.ts index b2993f731b..f18c590e6c 100644 --- a/packages/web-pkg/src/services/preview/previewService.ts +++ b/packages/web-pkg/src/services/preview/previewService.ts @@ -101,6 +101,7 @@ export class PreviewService { scalingup: options.scalingup || 0, preview: Object.hasOwnProperty.call(options, 'preview') ? options.preview : 1, a: Object.hasOwnProperty.call(options, 'a') ? options.a : 1, + intent: 'preview', ...(options.processor && { processor: options.processor }), ...(options.etag && { c: options.etag.replaceAll('"', '') }), ...(options.dimensions && options.dimensions[0] && { x: options.dimensions[0] }),