Skip to content

Commit dc15f7d

Browse files
feat(web-app-external): handle Collabora UI_InsertGraphic and UI_InsertFile postMessages
Add frontend handling for Collabora's remote file insertion and document comparison features. When oCIS sets EnableInsertRemoteFile and EnableInsertRemoteImage in the WOPI CheckFileInfo response, Collabora shows new menu items that send UI_InsertGraphic and UI_InsertFile postMessages to the parent window. - Add Host_PostmessageReady handshake: reply to App_LoadingStatus with Host_PostmessageReady so Collabora accepts Action postMessages. - Handle UI_InsertGraphic: open a file picker modal filtered to image MIME types, resolve the selected file to a signed WebDAV download URL, and send Action_InsertGraphic back to the Collabora iframe. - Handle UI_InsertFile: read callback and mimeTypeFilter from the Collabora message, open the file picker accordingly, and send back the appropriate Action (Action_InsertMultimedia or Action_CompareDocuments). - Create InsertRemoteFileModal.vue: new modal component that embeds the oCIS file browser in embed mode, resolves the picked file to a download URL via clientService.webdav.getFileUrl(), and calls back with { filename, url }. - Replace catchClickMicrosoftEdit with a unified handleAppMessage listener that handles all app iframe postMessages (UI_Edit, App_LoadingStatus, UI_InsertGraphic, UI_InsertFile). Companion server-side PR: owncloud/ocis#12192 Signed-off-by: Pedro Pinto Silva <pedro.silva@collabora.com>
1 parent 0be51f3 commit dc15f7d

3 files changed

Lines changed: 263 additions & 10 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Enhancement: Handle Collabora insert remote file and image postMessages
2+
3+
Handle UI_InsertGraphic and UI_InsertFile postMessages from Collabora Online,
4+
enabling remote image insertion, multimedia insertion, and document comparison
5+
features. Opens a file picker modal, resolves the selected file to a download
6+
URL, and sends the result back to Collabora. Also adds the Host_PostmessageReady
7+
handshake required by Collabora before it accepts Action postMessages.
8+
9+
Requires Collabora Online >= 24.04.10 for multimedia insertion, >= 25.04.9.1
10+
for document comparison. Companion server-side PR: owncloud/ocis#12192.
11+
12+
https://github.com/owncloud/web/pull/13658

packages/web-app-external/src/App.vue

Lines changed: 110 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<template>
22
<iframe
33
v-if="appUrl && method === 'GET'"
4+
ref="appIframeRef"
45
:src="appUrl"
56
class="oc-width-1-1 oc-height-1-1"
67
:title="iFrameTitle"
@@ -15,6 +16,7 @@
1516
</div>
1617
</form>
1718
<iframe
19+
ref="appIframeRef"
1820
name="app-iframe"
1921
:src="appUrl"
2022
class="oc-width-1-1 oc-height-1-1"
@@ -27,7 +29,18 @@
2729

2830
<script lang="ts" setup>
2931
import { stringify } from 'qs'
30-
import { computed, inject, unref, nextTick, ref, watch, VNodeRef, onMounted, type Ref } from 'vue'
32+
import {
33+
computed,
34+
inject,
35+
unref,
36+
nextTick,
37+
ref,
38+
watch,
39+
VNodeRef,
40+
onMounted,
41+
onBeforeUnmount,
42+
type Ref
43+
} from 'vue'
3144
import { useTask } from 'vue-concurrency'
3245
import { useGettext } from 'vue3-gettext'
3346
@@ -38,12 +51,15 @@ import {
3851
useCapabilityStore,
3952
useConfigStore,
4053
useMessages,
54+
useModals,
4155
useRequest,
4256
useAppProviderService,
4357
useRoute,
58+
useFolderLink,
4459
queryItemAsString,
4560
useRouteQuery
4661
} from '@ownclouders/web-pkg'
62+
import InsertRemoteFileModal from './components/InsertRemoteFileModal.vue'
4763
import {
4864
isProjectSpaceResource,
4965
isPublicSpaceResource,
@@ -72,6 +88,8 @@ const configStore = useConfigStore()
7288
const route = useRoute()
7389
const appProviderService = useAppProviderService()
7490
const { makeRequest } = useRequest()
91+
const { dispatchModal } = useModals()
92+
const { getParentFolderLink } = useFolderLink()
7593
7694
const viewModeQuery = useRouteQuery('view_mode')
7795
const isMobileWidth =
@@ -97,6 +115,7 @@ const appUrl = ref()
97115
const formParameters = ref({})
98116
const method = ref()
99117
const subm: VNodeRef = ref()
118+
const appIframeRef = ref<HTMLIFrameElement>()
100119
101120
const iFrameTitle = computed(() => {
102121
return $gettext('"%{appName}" app content area', {
@@ -179,20 +198,101 @@ const determineOpenAsPreview = (appName: string) => {
179198
return openAsPreview === true || (Array.isArray(openAsPreview) && openAsPreview.includes(appName))
180199
}
181200
182-
// switch to write mode when edit is clicked
183-
const catchClickMicrosoftEdit = (event: MessageEvent) => {
201+
const IMAGE_MIME_TYPES = [
202+
'image/png',
203+
'image/jpeg',
204+
'image/gif',
205+
'image/svg+xml',
206+
'image/bmp',
207+
'image/webp'
208+
]
209+
210+
const postMessageToApp = (messageId: string, values?: Record<string, unknown>) => {
211+
const iframe = unref(appIframeRef)
212+
if (!iframe?.contentWindow) {
213+
return
214+
}
215+
iframe.contentWindow.postMessage(JSON.stringify({ MessageId: messageId, Values: values }), '*')
216+
}
217+
218+
const openInsertFileModal = ({
219+
title,
220+
fileTypes,
221+
responseMessageId
222+
}: {
223+
title: string
224+
fileTypes?: string[]
225+
responseMessageId: string
226+
}) => {
227+
const parentFolderLink = getParentFolderLink(props.resource)
228+
dispatchModal({
229+
elementClass: 'insert-remote-file-modal',
230+
title,
231+
customComponent: InsertRemoteFileModal,
232+
hideActions: true,
233+
customComponentAttrs: () => ({
234+
parentFolderLink,
235+
fileTypes,
236+
onSelect: ({ filename, url }: { filename: string; url: string }) => {
237+
postMessageToApp(responseMessageId, { filename, url })
238+
}
239+
}),
240+
focusTrapInitial: false
241+
})
242+
}
243+
244+
const handleAppMessage = (event: MessageEvent) => {
184245
try {
185-
if (JSON.parse(event.data)?.MessageId === 'UI_Edit') {
186-
loadAppUrl.perform('write')
246+
const msg = JSON.parse(event.data)
247+
if (!msg?.MessageId) {
248+
return
249+
}
250+
251+
switch (msg.MessageId) {
252+
// Collabora requires this handshake before it accepts any Action_* messages.
253+
// Without it, all postMessages are rejected with "PostMessage ignored: not ready."
254+
case 'App_LoadingStatus':
255+
if (msg.Values?.Status === 'Document_Loaded') {
256+
postMessageToApp('Host_PostmessageReady')
257+
}
258+
break
259+
260+
case 'UI_Edit':
261+
if (determineOpenAsPreview(unref(appName))) {
262+
loadAppUrl.perform('write')
263+
}
264+
break
265+
266+
case 'UI_InsertGraphic':
267+
openInsertFileModal({
268+
title: $gettext('Insert image'),
269+
fileTypes: IMAGE_MIME_TYPES,
270+
responseMessageId: 'Action_InsertGraphic'
271+
})
272+
break
273+
274+
case 'UI_InsertFile': {
275+
const callback = msg.Values?.callback
276+
const mimeTypeFilter = msg.Values?.mimeTypeFilter
277+
openInsertFileModal({
278+
title:
279+
callback === 'Action_CompareDocuments'
280+
? $gettext('Compare document')
281+
: $gettext('Insert multimedia'),
282+
fileTypes: mimeTypeFilter,
283+
responseMessageId: callback
284+
})
285+
break
286+
}
187287
}
188288
} catch {}
189289
}
290+
190291
onMounted(() => {
191-
if (determineOpenAsPreview(unref(appName))) {
192-
window.addEventListener('message', catchClickMicrosoftEdit)
193-
} else {
194-
window.removeEventListener('message', catchClickMicrosoftEdit)
195-
}
292+
window.addEventListener('message', handleAppMessage)
293+
})
294+
onBeforeUnmount(() => {
295+
window.removeEventListener('message', handleAppMessage)
196296
})
197297
198298
watch(
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
<template>
2+
<div class="oc-height-1-1" tabindex="0">
3+
<app-loading-spinner v-if="isLoading" />
4+
<iframe
5+
v-show="!isLoading"
6+
ref="iframeRef"
7+
class="oc-width-1-1 oc-height-1-1"
8+
:title="iframeTitle"
9+
:src="iframeSrc"
10+
tabindex="0"
11+
@load="onLoad"
12+
></iframe>
13+
</div>
14+
</template>
15+
16+
<!--
17+
File picker modal for Collabora's remote insert features (UI_InsertGraphic / UI_InsertFile).
18+
Opens the oCIS file browser in embed mode, resolves the picked file to a download URL
19+
(with URL signing if available), and calls onSelect with { filename, url }.
20+
The caller sends the result back to the Collabora iframe as an Action_* postMessage.
21+
-->
22+
<script lang="ts" setup>
23+
import { onBeforeUnmount, onMounted, ref, unref } from 'vue'
24+
import {
25+
Modal,
26+
useClientService,
27+
useGetMatchingSpace,
28+
useMessages,
29+
useModals,
30+
useRouter,
31+
useThemeStore,
32+
useCapabilityStore,
33+
useUserStore,
34+
embedModeFilePickMessageData
35+
} from '@ownclouders/web-pkg'
36+
import { RouteLocationRaw } from 'vue-router'
37+
import { AppLoadingSpinner } from '@ownclouders/web-pkg'
38+
import { useGettext } from 'vue3-gettext'
39+
40+
interface Props {
41+
modal: Modal
42+
parentFolderLink: RouteLocationRaw
43+
fileTypes?: string[]
44+
onSelect: (result: { filename: string; url: string }) => void
45+
}
46+
const { modal, parentFolderLink, fileTypes, onSelect } = defineProps<Props>()
47+
const iframeRef = ref<HTMLIFrameElement>()
48+
const isLoading = ref(true)
49+
const router = useRouter()
50+
const themeStore = useThemeStore()
51+
const clientService = useClientService()
52+
const capabilityStore = useCapabilityStore()
53+
const userStore = useUserStore()
54+
const { removeModal } = useModals()
55+
const { showErrorMessage } = useMessages()
56+
const { getMatchingSpace } = useGetMatchingSpace()
57+
const { $gettext } = useGettext()
58+
59+
const parentFolderRoute = router.resolve(parentFolderLink)
60+
const iframeTitle = themeStore.currentTheme.common?.name
61+
const iframeUrl = new URL(parentFolderRoute.href, window.location.origin)
62+
iframeUrl.searchParams.append('hide-logo', 'true')
63+
iframeUrl.searchParams.append('embed', 'true')
64+
iframeUrl.searchParams.append('embed-target', 'file')
65+
iframeUrl.searchParams.append('embed-delegate-authentication', 'false')
66+
if (fileTypes?.length) {
67+
iframeUrl.searchParams.append('embed-file-types', fileTypes.join(','))
68+
}
69+
70+
const iframeSrc = iframeUrl.href
71+
72+
const onLoad = () => {
73+
isLoading.value = false
74+
unref(iframeRef).contentWindow.focus()
75+
}
76+
77+
const onFilePick = async ({ data }: MessageEvent) => {
78+
if (data.name !== 'owncloud-embed:file-pick') {
79+
return
80+
}
81+
82+
const { resource }: embedModeFilePickMessageData = data.data
83+
const space = getMatchingSpace(resource)
84+
85+
try {
86+
// Resolve to a signed WebDAV URL so the Collabora server can download the file
87+
const url = await clientService.webdav.getFileUrl(space, resource, {
88+
isUrlSigningEnabled: capabilityStore.supportUrlSigning,
89+
username: userStore.user?.onPremisesSamAccountName
90+
})
91+
onSelect({ filename: resource.name, url })
92+
} catch (e) {
93+
console.error('Failed to resolve download URL for remote file insert', e)
94+
showErrorMessage({
95+
title: $gettext('Failed to get file URL'),
96+
errors: [e]
97+
})
98+
}
99+
100+
removeModal(modal.id)
101+
}
102+
103+
const onCancel = ({ data }: MessageEvent) => {
104+
if (data.name !== 'owncloud-embed:cancel') {
105+
return
106+
}
107+
108+
removeModal(modal.id)
109+
}
110+
111+
onMounted(() => {
112+
window.addEventListener('message', onFilePick)
113+
window.addEventListener('message', onCancel)
114+
})
115+
116+
onBeforeUnmount(() => {
117+
window.removeEventListener('message', onFilePick)
118+
window.removeEventListener('message', onCancel)
119+
})
120+
</script>
121+
122+
<style lang="scss">
123+
.oc-modal.insert-remote-file-modal {
124+
max-width: 80dvw;
125+
border: none;
126+
overflow: hidden;
127+
128+
.oc-modal-title {
129+
display: none;
130+
}
131+
132+
.oc-modal-body {
133+
padding: 0;
134+
135+
&-message {
136+
height: 60dvh;
137+
margin: 0;
138+
}
139+
}
140+
}
141+
</style>

0 commit comments

Comments
 (0)