Skip to content

Commit

Permalink
Add external app for appProvider iFrames
Browse files Browse the repository at this point in the history
  • Loading branch information
pascalwengerter committed Oct 1, 2021
1 parent 57daa52 commit 7980e48
Show file tree
Hide file tree
Showing 18 changed files with 534 additions and 2 deletions.
6 changes: 6 additions & 0 deletions changelog/unreleased/enhancement-external-app
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Enhancement: Add wrapper app for external apps

We have added a `external` app that can render apps
coming from the oCIS AppProvider via iFrame.

https://github.com/owncloud/web/pull/5805
4 changes: 3 additions & 1 deletion config/config.json.sample-ocis
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
"scope": "openid profile email"
},
"apps": [
"external",
"files",
"media-viewer",
"search",
"media-viewer"
"external"
],
"external_apps": [
{
Expand Down
8 changes: 7 additions & 1 deletion dev/docker/ocis.web.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@
"options": {
"hideSearchBar": true
},
"apps": ["files", "media-viewer", "markdown-editor", "search"],
"apps": [
"files",
"markdown-editor",
"media-viewer",
"search",
"external"
],
"external_apps": [
{
"id": "settings",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"author": "ownCloud",
"workspaces": [
"packages/web-app-draw-io",
"packages/web-app-external",
"packages/web-app-files",
"packages/web-app-markdown-editor",
"packages/web-app-media-viewer",
Expand Down
10 changes: 10 additions & 0 deletions packages/web-app-external/l10n/.tx/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[main]
host = https://www.transifex.com

[owncloud-web.external]
file_filter = locale/<lang>/LC_MESSAGES/app.po
minimum_perc = 0
source_file = template.pot
source_lang = en
type = PO

1 change: 1 addition & 0 deletions packages/web-app-external/l10n/translations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"de":{"Close":"Schließen","Download":"Herunterladen"}}
10 changes: 10 additions & 0 deletions packages/web-app-external/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "external",
"version": "0.0.0",
"description": "ownCloud web integration of the reva app provider",
"license": "AGPL-3.0",
"devDependencies": {
"vue": "^2.6.10",
"vuex": "3.6.2"
}
}
137 changes: 137 additions & 0 deletions packages/web-app-external/src/App.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<template>
<main
class="uk-height-viewport"
:class="{
'uk-flex uk-flex-center uk-flex-middle': loading || loadingError
}"
>
<h1 class="oc-invisible-sr" v-text="pageTitle" />
<loading-screen v-if="loading" />
<error-screen v-else-if="loadingError" />
<iframe
v-if="appUrl && method === 'GET'"
:src="appUrl"
class="uk-width-1-1 uk-height-viewport"
:title="iFrameTitle"
/>
<div v-if="appUrl && method === 'POST' && formParameters">
<form :action="appUrl" target="app-iframe" method="post">
<input ref="subm" type="submit" :value="formParameters" class="oc-hidden" />
<div v-for="(item, key, index) in formParameters" :key="index">
<input :name="key" :value="item" type="hidden" />
</div>
</form>
<iframe name="app-iframe" class="uk-width-1-1 uk-height-viewport" :title="iFrameTitle" />
</div>
</main>
</template>

<script>
import { mapGetters } from 'vuex'
import ErrorScreen from './components/ErrorScreen.vue'
import LoadingScreen from './components/LoadingScreen.vue'
export default {
name: 'ExternalApp',
components: {
ErrorScreen,
LoadingScreen
},
data: () => ({
loading: false,
loadingError: false,
appUrl: '',
method: '',
formParameters: {}
}),
computed: {
...mapGetters(['getToken', 'capabilities', 'configuration']),
pageTitle() {
const translated = this.$gettext('"%{appName}" app page')
return this.$gettextInterpolate(translated, {
appName: this.appName
})
},
iFrameTitle() {
const translated = this.$gettext('"%{appName}" app content area')
return this.$gettextInterpolate(translated, {
appName: this.appName
})
},
appName() {
return this.$route.params.app
},
fileId() {
return this.$route.params.file_id + '=='
}
},
async created() {
this.loading = true
// TODO: Enable externalApp usage on public routes below
// initialize headers()
// if (this.isPublicRoute) {
// // send auth header here if public route
// // if password exists send it via basicauth public:password
// // headers.append('public-token', 'uUCPJghnVUspjxe')
// // const password = this.publicLinkPassword
// // if (password) {
// // headers.append( Authorization: 'Basic ' + Buffer.from('public:' + password).toString('base64') }
// // }
// } else {
// - check for token
// - abort if falsy
// - build headers as below
// }
if (!this.getToken) {
this.loading = false
this.loadingError = true
return
}
const headers = new Headers()
headers.append('Authorization', 'Bearer ' + this.getToken)
headers.append('X-Requested-With', 'XMLHttpRequest')
const configUrl = this.configuration.server
const appOpenUrl = this.capabilities.files.app_providers[0].open_url.replace('/app', 'app')
const url = configUrl + appOpenUrl + '?file_id=' + this.fileId + '&app_name=' + this.appName
const response = await fetch(url, {
method: 'POST',
headers
})
if (response.status !== 200) {
this.loading = false
this.loadingError = true
console.error('Error fetching app information', response.status, response.message)
return
}
const data = await response.json()
if (!data.app_url || !data.method) {
this.loading = false
this.loadingError = true
console.error('Error in app server response')
return
}
this.appUrl = data.app_url
this.method = data.method
if (data.form_parameters) this.formParameters = data.form_parameters
if (this.method === 'POST' && this.formParameters) {
this.$nextTick(() => this.$refs.subm.click())
}
this.loading = false
}
}
</script>
6 changes: 6 additions & 0 deletions packages/web-app-external/src/components/ErrorScreen.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<template>
<div class="uk-text-center">
<oc-icon size="xxlarge" name="warning" />
<p v-translate>Error when loading the application</p>
</div>
</template>
6 changes: 6 additions & 0 deletions packages/web-app-external/src/components/LoadingScreen.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<template>
<div class="uk-text-center">
<oc-spinner size="xlarge" />
<p v-translate class="oc-invisible">Loading app</p>
</div>
</template>
40 changes: 40 additions & 0 deletions packages/web-app-external/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import translations from '../l10n/translations'

import App from './App.vue'
import store from './store'

const appInfo = {
name: 'External',
id: 'external'
}

const routes = [
{
name: 'apps',
path: '/:app/:file_id',
components: {
app: App
}
}
]

async function fetchAvailableMimeTypes() {
const vueStore = window.Vue.$store
if (!vueStore.getters.capabilities.files.app_providers[0]?.enabled) {
return
}
const serverUrl = vueStore.getters.configuration.server
const appList = vueStore.getters.capabilities.files.app_providers[0].apps_url
const url = serverUrl + appList.replace('/app', 'app')
await vueStore.dispatch('External/fetchMimeTypes', url)
}

export default {
appInfo,
routes,
store,
translations,
async mounted() {
await fetchAvailableMimeTypes()
}
}
37 changes: 37 additions & 0 deletions packages/web-app-external/src/store/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const state = {
mimeTypes: {}
}

const actions = {
async fetchMimeTypes(context, url: string): Promise<void> {
const response = await fetch(url)

if (!response.ok) {
throw new Error('Error fetching app provider MIME types')
}

const mimeTypes = await response.json()

context.commit('SET_MIME_TYPES', mimeTypes['mime-types'])
}
}

const getters = {
getMimeTypes: state => {
return state.mimeTypes
}
}

const mutations = {
SET_MIME_TYPES(state, mimeTypes): void {
state.mimeTypes = mimeTypes
}
}

export default {
namespaced: true,
state,
actions,
mutations,
getters
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`The app provider extension should be able to load an iFrame via get 1`] = `
<main class="uk-height-viewport">
<h1 class="oc-invisible-sr">"exampleApp" app page</h1>
<!----> <iframe src="https://example.test/d12ab86/loe009157-MzBw" title="&quot;exampleApp&quot; app content area" class="uk-width-1-1 uk-height-viewport"></iframe>
<!---->
</main>
`;

exports[`The app provider extension should be able to load an iFrame via post 1`] = `
<main class="uk-height-viewport">
<h1 class="oc-invisible-sr">"exampleApp" app page</h1>
<!---->
<!---->
<div>
<form action="https://example.test/d12ab86/loe009157-MzBw" target="app-iframe" method="post"><input type="submit" class="oc-hidden" value="[object Object]">
<div><input name="access_token" type="hidden" value="asdfsadfsadf"></div>
<div><input name="access_token_ttl" type="hidden" value="123456"></div>
</form> <iframe name="app-iframe" title="&quot;exampleApp&quot; app content area" class="uk-width-1-1 uk-height-viewport"></iframe>
</div>
</main>
`;
exports[`The app provider extension should fail for unauthenticated users 1`] = `
<main class="uk-height-viewport uk-flex uk-flex-center uk-flex-middle">
<h1 class="oc-invisible-sr">"exampleApp" app page</h1>
<errorscreen-stub></errorscreen-stub>
<!---->
<!---->
</main>
`;
exports[`The app provider extension should show a loading spinner while loading 1`] = `
<main class="uk-height-viewport uk-flex uk-flex-center uk-flex-middle">
<h1 class="oc-invisible-sr">"exampleApp" app page</h1>
<loadingscreen-stub></loadingscreen-stub>
<!---->
<!---->
</main>
`;
exports[`The app provider extension should show a meaningful message if an error occurs during loading 1`] = `
<main class="uk-height-viewport uk-flex uk-flex-center uk-flex-middle">
<h1 class="oc-invisible-sr">"exampleApp" app page</h1>
<errorscreen-stub></errorscreen-stub>
<!---->
<!---->
</main>
`;
Loading

0 comments on commit 7980e48

Please sign in to comment.