Skip to content

Commit

Permalink
Merge pull request #5924 from owncloud/public-token-features
Browse files Browse the repository at this point in the history
[full-ci] feat(public-links): archive downloads in public links
  • Loading branch information
fschade authored Nov 8, 2021
2 parents 9dcce09 + 964415d commit 936a57b
Show file tree
Hide file tree
Showing 21 changed files with 262 additions and 93 deletions.
2 changes: 1 addition & 1 deletion .drone.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# The version of OCIS to use in pipelines that test against OCIS
OCIS_COMMITID=5aeb76f6c8aee574fdd2710f9e883efe0c8166f8
OCIS_COMMITID=25d6e4efd119049f290683d6d126185368058b4e
OCIS_BRANCH=master
10 changes: 10 additions & 0 deletions changelog/unreleased/enhancement-public-link-capabilities
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Enhancement: App provider and archiver on public links

We made the app provider and archiver services available on public links. As a prerequisite for this we needed to make backend capabilities available on public links, which will
be beneficial for all future extension development.

https://github.com/owncloud/web/pull/5924
https://github.com/owncloud/web/issues/5884
https://github.com/owncloud/ocis/issues/2479
https://github.com/owncloud/web/issues/2479
https://github.com/owncloud/web/issues/5901
63 changes: 36 additions & 27 deletions packages/web-app-external/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,24 @@ import { mapGetters } from 'vuex'
import ErrorScreen from './components/ErrorScreen.vue'
import LoadingScreen from './components/LoadingScreen.vue'
// FIXME: hacky, get rid asap, just a workaround
// same as packages/web-app-files/src/views/PublicFiles.vue
const unauthenticatedUserReady = async (router, store) => {
if (store.getters.userReady) {
return
}
const publicToken = router.currentRoute.query['public-token']
const publicLinkPassword = store.getters['Files/publicLinkPassword']
await store.dispatch('loadCapabilities', {
publicToken,
...(publicLinkPassword && { user: 'public', password: publicLinkPassword })
})
store.commit('SET_USER_READY', true)
}
export default {
name: 'ExternalApp',
Expand All @@ -48,6 +66,7 @@ export default {
}),
computed: {
...mapGetters(['getToken', 'capabilities', 'configuration']),
...mapGetters('Files', ['publicLinkPassword']),
pageTitle() {
const translated = this.$gettext('"%{appName}" app page')
Expand All @@ -69,37 +88,27 @@ export default {
}
},
async created() {
this.loading = true
// TODO: Enable externalApp usage on public routes below
// initialize headers()
await unauthenticatedUserReady(this.$router, this.$store)
// 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
// }
this.loading = true
if (!this.getToken) {
this.loading = false
this.loadingError = true
return
// build headers with respect to the actual auth situation
const { 'public-token': publicToken } = this.$route.query
const publicLinkPassword = this.publicLinkPassword
const accessToken = this.getToken
const headers = {
'X-Requested-With': 'XMLHttpRequest',
...(publicToken && { 'public-token': publicToken }),
...(publicLinkPassword && {
Authorization:
'Basic ' + Buffer.from(['public', publicLinkPassword].join(':')).toString('base64')
}),
...(accessToken && {
Authorization: 'Bearer ' + accessToken
})
}
const headers = new Headers()
headers.append('Authorization', 'Bearer ' + this.getToken)
headers.append('X-Requested-With', 'XMLHttpRequest')
// fetch iframe params for app and file
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
Expand Down
7 changes: 4 additions & 3 deletions packages/web-app-external/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ const routes = [
app: App
},
meta: {
title: $gettext('External app')
title: $gettext('External app'),
auth: false
}
}
]
Expand All @@ -30,7 +31,7 @@ export default {
routes,
store,
translations,
async ready({ store: runtimeStore }) {
await runtimeStore.dispatch('External/fetchMimeTypes')
userReady({ store }) {
store.dispatch('External/fetchMimeTypes')
}
}
11 changes: 8 additions & 3 deletions packages/web-app-external/src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,18 @@ const actions = {
throw new Error('Error fetching app provider MIME types')
}

const mimeTypes = await response.json()
commit('SET_MIME_TYPES', mimeTypes['mime-types'])
const { 'mime-types': mimeTypes } = await response.json()

commit('SET_MIME_TYPES', mimeTypes)
}
}

const getters = {
getMimeTypes: (state: typeof State): { [key: string]: string } => {
mimeTypes: (
state: typeof State
): {
[key: string]: string
} => {
return state.mimeTypes
}
}
Expand Down
13 changes: 9 additions & 4 deletions packages/web-app-external/tests/unit/app.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ const componentStubs = {
}

const $route = {
query: {
'public-token': 'a-token'
},
params: {
app: 'exampleApp',
file_id: '2147491323'
Expand All @@ -30,6 +33,7 @@ const storeOptions = {
configuration: jest.fn(() => ({
server: 'http://example.com/'
})),
userReady: () => true,
capabilities: jest.fn(() => ({
files: {
app_providers: [
Expand All @@ -46,7 +50,7 @@ const storeOptions = {
External: {
namespaced: true,
getters: {
getMimeTypes: jest.fn()
mimeTypes: jest.fn()
},
actions: {
fetchMimeTypes: jest.fn()
Expand Down Expand Up @@ -84,7 +88,7 @@ describe('The app provider extension', () => {
fetchMock.resetMocks()
})

it('should show a loading spinner while loading', () => {
it('should show a loading spinner while loading', async () => {
global.fetch = jest.fn(() =>
setTimeout(() => {
Promise.resolve({
Expand All @@ -94,19 +98,21 @@ describe('The app provider extension', () => {
}, 500)
)
const wrapper = createShallowMountWrapper()

await wrapper.vm.$nextTick()
expect(wrapper).toMatchSnapshot()
})
it('should show a meaningful message if an error occurs during loading', async () => {
fetchMock.mockReject(new Error('fake error message'))
const wrapper = createShallowMountWrapper()
await wrapper.vm.$nextTick()
await wrapper.vm.$nextTick()
expect(wrapper).toMatchSnapshot()
})
it('should fail for unauthenticated users', async () => {
fetchMock.mockResponseOnce({ status: 401 })
const wrapper = createShallowMountWrapper()
await wrapper.vm.$nextTick()
await wrapper.vm.$nextTick()
expect(wrapper).toMatchSnapshot()
})
it('should be able to load an iFrame via get', async () => {
Expand All @@ -131,7 +137,6 @@ describe('The app provider extension', () => {
json: () => providerSuccessResponsePost
})
)

const wrapper = createShallowMountWrapper()
await wrapper.vm.$nextTick()
await wrapper.vm.$nextTick()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ import MixinRoutes from '../../../mixins/routes'
import MixinDeleteResources from '../../../mixins/deleteResources'
import { cloneStateObject } from '../../../helpers/store'
import { canBeMoved } from '../../../helpers/permissions'
import { checkRoute } from '../../../helpers/route'
import { checkRoute, isPublicFilesRoute } from '../../../helpers/route'
import { shareStatus } from '../../../helpers/shareStatus'
import { triggerShareAction } from '../../../helpers/share/triggerShareAction'
import PQueue from 'p-queue'
Expand Down Expand Up @@ -129,7 +129,7 @@ export default {
canDownloadSingleFile() {
if (
!checkRoute(['files-personal', 'files-favorites', 'files-public-list'], this.$route.name)
!checkRoute(['files-personal', 'files-public-list', 'files-favorites'], this.$route.name)
) {
return false
}
Expand All @@ -146,7 +146,9 @@ export default {
},
canDownloadAsArchive() {
if (!checkRoute(['files-personal', 'files-favorites'], this.$route.name)) {
if (
!checkRoute(['files-personal', 'files-public-list', 'files-favorites'], this.$route.name)
) {
return false
}
Expand Down Expand Up @@ -363,12 +365,16 @@ export default {
await this.downloadFile(this.selectedFiles[0])
return
}
await this.downloadAsArchive()
},
async downloadAsArchive() {
await triggerDownloadAsArchive({
fileIds: this.selectedFiles.map((r) => r.fileId)
fileIds: this.selectedFiles.map((r) => r.fileId),
...(isPublicFilesRoute(this.$route) && {
publicToken: this.$route.params.item.split('/')[0]
})
}).catch((e) => {
console.error(e)
this.showMessage({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export default {
computed: {
...mapGetters('Files', ['highlightedFile', 'currentFolder']),
appList() {
return this.$_fileActions_loadApps(this.highlightedFile) || []
},
actions() {
return this.$_fileActions_getAllAvailableActions(this.highlightedFile)
}
Expand Down
21 changes: 17 additions & 4 deletions packages/web-app-files/src/helpers/download/downloadAsArchive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,45 @@ import {
ClientService,
clientService as defaultClientService
} from '../../services'

import { major } from 'semver'
import { RuntimeError } from 'web-runtime/src/container/error'

interface TriggerDownloadAsArchiveOptions {
fileIds: string[]
archiverService?: ArchiverService
clientService?: ClientService
publicToken?: string
}

export const triggerDownloadAsArchive = async (
options: TriggerDownloadAsArchiveOptions
): Promise<void> => {
const archiverService = options.archiverService || defaultArchiverService
const clientService = options.clientService || defaultClientService

if (!isDownloadAsArchiveAvailable(archiverService)) {
throw new RuntimeError('no archiver capability available')
}

if (options.fileIds.length === 0) {
throw new RuntimeError('requested archive with empty list of resources')
}

const majorVersion = major(archiverService.capability.version)
if (majorVersion === 2) {
const queryParams = [...options.fileIds.map((id) => `id=${id}`)]
const archiverUrl = archiverService.url + '?' + queryParams.join('&')
window.location = await clientService.owncloudSdk.signUrl(archiverUrl)
if (majorVersion !== 2) {
return
}

const queryParams = [
options.publicToken ? `public-token=${options.publicToken}` : '',
...options.fileIds.map((id) => `id=${id}`)
].filter(Boolean)
const archiverUrl = archiverService.url + '?' + queryParams.join('&')

window.location = options.publicToken
? (archiverUrl as any)
: await clientService.owncloudSdk.signUrl(archiverUrl)
}

export const isDownloadAsArchiveAvailable = (
Expand Down
14 changes: 7 additions & 7 deletions packages/web-app-files/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,19 +90,19 @@ export default {
navItems,
quickActions,
translations,
ready({ router: runtimeRouter, store: runtimeStore }) {
Registry.filterSearch = new FilterSearch(runtimeStore, runtimeRouter)
Registry.sdkSearch = new SDKSearch(runtimeStore, runtimeRouter)
ready({ router, store }) {
Registry.filterSearch = new FilterSearch(store, router)
Registry.sdkSearch = new SDKSearch(store, router)

// when discussing the boot process of applications we need to implement a
// registry that does not rely on call order, aka first register "on" and only after emit.
bus.publish('app.search.register.provider', Registry.filterSearch)
bus.publish('app.search.register.provider', Registry.sdkSearch)

// initialize services
},
userReady({ store }) {
archiverService.initialize(
runtimeStore.getters.configuration.server,
get(runtimeStore, 'getters.capabilities.files.archivers', [])
store.getters.configuration.server || window.location.origin,
get(store, 'getters.capabilities.files.archivers', [])
)
}
}
15 changes: 12 additions & 3 deletions packages/web-app-files/src/mixins/actions/downloadFolder.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { isFavoritesRoute, isPersonalRoute } from '../../helpers/route'
import { checkRoute, isPublicFilesRoute } from '../../helpers/route'

import {
isDownloadAsArchiveAvailable,
triggerDownloadAsArchive
Expand All @@ -15,7 +16,12 @@ export default {
return this.$gettext('Download folder')
},
isEnabled: ({ resource }) => {
if (!isPersonalRoute(this.$route) && !isFavoritesRoute(this.$route)) {
if (
!checkRoute(
['files-personal', 'files-public-list', 'files-favorites'],
this.$route.name
)
) {
return false
}
if (!resource.isFolder) {
Expand All @@ -36,7 +42,10 @@ export default {
methods: {
async $_downloadFolder_trigger(resource) {
await triggerDownloadAsArchive({
fileIds: [resource.fileId]
fileIds: [resource.fileId],
...(isPublicFilesRoute(this.$route) && {
publicToken: this.$route.params.item.split('/')[0]
})
}).catch((e) => {
console.error(e)
this.showMessage({
Expand Down
Loading

0 comments on commit 936a57b

Please sign in to comment.