diff --git a/changelog/unreleased/enhancement-internal-links b/changelog/unreleased/enhancement-internal-links new file mode 100644 index 00000000000..0fbd43ace71 --- /dev/null +++ b/changelog/unreleased/enhancement-internal-links @@ -0,0 +1,8 @@ +Enhancement: Resolve internal links + +Public links with the role "internal" can now be resolved. +Note: Internal links to shares can not be resolved as of now. This will follow in a subsequent PR. + +https://github.com/owncloud/web/pull/7405 +https://github.com/owncloud/web/issues/7304 +https://github.com/owncloud/web/issues/6844 diff --git a/changelog/unreleased/enhancement-private-links b/changelog/unreleased/enhancement-private-links new file mode 100644 index 00000000000..eccb15f9e1a --- /dev/null +++ b/changelog/unreleased/enhancement-private-links @@ -0,0 +1,7 @@ +Enhancement: Resolve private links + +Private links can now be resolved. +Note: Private links to shares in oCIS can not be resolved as of now. This will follow in a subsequent PR. + +https://github.com/owncloud/web/pull/7405 +https://github.com/owncloud/web/issues/7707 diff --git a/deployments/examples/ocis_web/config/ocis/proxy.yaml b/deployments/examples/ocis_web/config/ocis/proxy.yaml index e3244e00160..9e337df9b79 100644 --- a/deployments/examples/ocis_web/config/ocis/proxy.yaml +++ b/deployments/examples/ocis_web/config/ocis/proxy.yaml @@ -39,6 +39,10 @@ policies: endpoint: /ocs/v[12].php/config backend: http://localhost:9140 unprotected: true + - type: regex + endpoint: /ocs/v[12].php/apps/files_sharing/api/v1/tokeninfo/unprotected + backend: http://localhost:9140 + unprotected: true - endpoint: /ocs/ backend: http://localhost:9140 - type: query diff --git a/packages/web-app-files/src/components/SideBar/Shares/FileLinks.vue b/packages/web-app-files/src/components/SideBar/Shares/FileLinks.vue index f16375978ae..ad33e125311 100644 --- a/packages/web-app-files/src/components/SideBar/Shares/FileLinks.vue +++ b/packages/web-app-files/src/components/SideBar/Shares/FileLinks.vue @@ -19,7 +19,7 @@ /> - + @@ -183,7 +177,11 @@ import { basename } from 'path' import { DateTime } from 'luxon' import { mapActions, mapGetters } from 'vuex' import { createLocationSpaces } from '../../../../router' -import { LinkShareRoles } from 'web-client/src/helpers/share' +import { + linkRoleInternalFile, + linkRoleInternalFolder, + LinkShareRoles +} from 'web-client/src/helpers/share' import { defineComponent } from '@vue/runtime-core' import { formatDateFromDateTime, formatRelativeDateFromDateTime } from 'web-pkg/src/helpers' import { SpaceResource } from 'web-client/src/helpers' @@ -239,15 +237,15 @@ export default defineComponent({ }, computed: { ...mapGetters('runtime/spaces', ['spaces']), + currentLinkRole() { + return LinkShareRoles.getByBitmask(this.link.permissions, this.isFolderShare) + }, currentLinkRoleDescription() { - return LinkShareRoles.getByBitmask( - parseInt(this.link.permissions), - this.isFolderShare - ).description(false) + return this.currentLinkRole.description(false) }, currentLinkRoleLabel() { - return LinkShareRoles.getByBitmask(parseInt(this.link.permissions), this.isFolderShare).label + return this.currentLinkRole.label }, editOptions() { @@ -317,7 +315,7 @@ export default defineComponent({ }) } } - if (!this.isPasswordEnforced && !this.link.password) { + if (!this.isPasswordEnforced && !this.link.password && !this.isAliasLink) { result.push({ id: 'add-password', title: this.$gettext('Add password'), @@ -390,6 +388,10 @@ export default defineComponent({ passwortProtectionTooltip() { return this.$gettext('This link is password-protected') + }, + + isAliasLink() { + return [linkRoleInternalFolder, linkRoleInternalFile].includes(this.currentLinkRole) } }, watch: { diff --git a/packages/web-app-files/src/helpers/resources.ts b/packages/web-app-files/src/helpers/resources.ts index 66ac10c055f..667167e7cf6 100644 --- a/packages/web-app-files/src/helpers/resources.ts +++ b/packages/web-app-files/src/helpers/resources.ts @@ -357,8 +357,9 @@ export function buildSpaceShare(s, storageId): Share { function _buildLink(link): Share { let description = '' + const permissions = parseInt(link.permissions) - const role = LinkShareRoles.getByBitmask(parseInt(link.permissions), link.item_type === 'folder') + const role = LinkShareRoles.getByBitmask(permissions, link.item_type === 'folder') if (role) { description = role.label } @@ -382,7 +383,7 @@ function _buildLink(link): Share { token: link.token as string, url: link.url, path: link.path, - permissions: link.permissions, + permissions, description, quicklink, stime: link.stime, diff --git a/packages/web-app-files/src/index.js b/packages/web-app-files/src/index.js index f70bbb3bb2b..b1821ecb420 100644 --- a/packages/web-app-files/src/index.js +++ b/packages/web-app-files/src/index.js @@ -1,7 +1,6 @@ import App from './App.vue' import Favorites from './views/Favorites.vue' import FilesDrop from './views/FilesDrop.vue' -import PrivateLink from './views/PrivateLink.vue' import SharedWithMe from './views/shares/SharedWithMe.vue' import SharedWithOthers from './views/shares/SharedWithOthers.vue' import SharedViaLink from './views/shares/SharedViaLink.vue' @@ -94,7 +93,6 @@ export default { App, Favorites, FilesDrop, - PrivateLink, SearchResults, Shares: { SharedViaLink, diff --git a/packages/web-app-files/src/router/deprecated.ts b/packages/web-app-files/src/router/deprecated.ts index 35dc6ca7768..8f7e4289c14 100644 --- a/packages/web-app-files/src/router/deprecated.ts +++ b/packages/web-app-files/src/router/deprecated.ts @@ -2,7 +2,6 @@ import VueRouter, { RouteConfig, Route, Location, RouteMeta } from 'vue-router' import { createLocationSpaces } from './spaces' import { createLocationShares } from './shares' import { createLocationCommon } from './common' -import { createLocationOperations } from './operations' import { createLocationPublic } from './public' import { isLocationActive as isLocationActiveNoCompat } from './utils' import { createLocationTrash } from './trash' @@ -90,10 +89,7 @@ export const buildRoutes = (): RouteConfig[] => }, { path: '/private-link/:fileId', - meta: { - auth: false - }, - redirect: (to) => createLocationOperations('files-operations-resolver-private-link', to) + redirect: (to) => ({ name: 'resolvePrivateLink', params: { fileId: to.params.fileId } }) }, { path: '/public-link/:token', diff --git a/packages/web-app-files/src/router/index.ts b/packages/web-app-files/src/router/index.ts index 1be216a043d..25781c194ff 100644 --- a/packages/web-app-files/src/router/index.ts +++ b/packages/web-app-files/src/router/index.ts @@ -6,11 +6,6 @@ import { createLocationCommon } from './common' import { buildRoutes as buildDeprecatedRoutes, isLocationActive } from './deprecated' -import { - buildRoutes as buildOperationsRoutes, - createLocationOperations, - isLocationOperationsActive -} from './operations' import { buildRoutes as buildPublicRoutes, createLocationPublic, @@ -45,7 +40,6 @@ const buildRoutes = (components: RouteComponents): RouteConfig[] => [ ...buildSharesRoutes(components), ...buildPublicRoutes(components), ...buildSpacesRoutes(components), - ...buildOperationsRoutes(components), ...buildTrashRoutes(components), ...buildDeprecatedRoutes() ] @@ -54,9 +48,7 @@ export { createLocationCommon, createLocationShares, createLocationSpaces, - createLocationOperations, createLocationPublic, - isLocationOperationsActive, isLocationCommonActive, isLocationSharesActive, isLocationSpacesActive, diff --git a/packages/web-app-files/src/router/operations.ts b/packages/web-app-files/src/router/operations.ts deleted file mode 100644 index a1d8d54c609..00000000000 --- a/packages/web-app-files/src/router/operations.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { RouteComponents } from './router' -import { Location, RouteConfig } from 'vue-router' -import { $gettext, createLocation, isLocationActiveDirector } from './utils' - -type operationsTypes = 'files-operations-resolver-private-link' - -export const createLocationOperations = (name: operationsTypes, location = {}): Location => - createLocation(name, location) - -const locationResolverPrivateLink = createLocationOperations( - 'files-operations-resolver-private-link' -) - -export const isLocationOperationsActive = isLocationActiveDirector( - locationResolverPrivateLink -) - -export const buildRoutes = (components: RouteComponents): RouteConfig[] => [ - { - name: locationResolverPrivateLink.name, - path: '/ops/resolver/private-link/:fileId', - component: components.PrivateLink, - meta: { title: $gettext('Resolving private link') } - } -] diff --git a/packages/web-app-files/src/views/PrivateLink.vue b/packages/web-app-files/src/views/PrivateLink.vue deleted file mode 100644 index d5299b09456..00000000000 --- a/packages/web-app-files/src/views/PrivateLink.vue +++ /dev/null @@ -1,99 +0,0 @@ - - - - - diff --git a/packages/web-app-files/tests/unit/components/SideBar/Shares/FileLinks.spec.js b/packages/web-app-files/tests/unit/components/SideBar/Shares/FileLinks.spec.js index ba92293c83b..97b1a716675 100644 --- a/packages/web-app-files/tests/unit/components/SideBar/Shares/FileLinks.spec.js +++ b/packages/web-app-files/tests/unit/components/SideBar/Shares/FileLinks.spec.js @@ -31,7 +31,7 @@ const defaultLinksList = [ name: 'public link 1', url: 'some-link-1', path: '/file-1.txt', - permissions: '1' + permissions: 1 }, { id: '2', @@ -39,7 +39,7 @@ const defaultLinksList = [ name: 'public link 2', url: 'some-link-2', path: '/file-2.txt', - permissions: '1' + permissions: 1 } ] diff --git a/packages/web-app-files/tests/unit/components/SideBar/Shares/Links/DetailsAndEdit.spec.js b/packages/web-app-files/tests/unit/components/SideBar/Shares/Links/DetailsAndEdit.spec.js index e168d97aeff..f6e224574a8 100644 --- a/packages/web-app-files/tests/unit/components/SideBar/Shares/Links/DetailsAndEdit.spec.js +++ b/packages/web-app-files/tests/unit/components/SideBar/Shares/Links/DetailsAndEdit.spec.js @@ -19,7 +19,7 @@ const availableRoleOptions = LinkShareRoles.list(false, true, true) const exampleLink = { name: 'Example link', url: 'https://some-url.com/abc', - permissions: '1' + permissions: 1 } describe('DetailsAndEdit component', () => { diff --git a/packages/web-app-files/tests/unit/views/PrivateLink.spec.ts b/packages/web-app-files/tests/unit/views/PrivateLink.spec.ts deleted file mode 100644 index 5743a54fdc3..00000000000 --- a/packages/web-app-files/tests/unit/views/PrivateLink.spec.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { shallowMount } from '@vue/test-utils' -import { getRouter, getStore, localVue } from './views.setup' -import PrivateLink from '@files/src/views/PrivateLink.vue' -import fileFixtures from '../../../../../__fixtures__/files' - -localVue.prototype.$client.files = { - getPathForFileId: jest.fn(() => Promise.resolve('/lorem.txt')) -} - -const theme = { - general: { slogan: 'some-slogan' } -} - -const $route = { - params: { - fileId: '2147491323' - }, - meta: { - title: 'Resolving private link' - } -} - -const selectors = { - pageTitle: 'h1.oc-invisible-sr', - loader: '.oc-card-body', - errorTitle: '.oc-link-resolve-error-title' -} - -describe('PrivateLink view', () => { - it.todo('adapt tests, see comment in Favorites.spec.ts...') - // afterEach(() => { - // jest.clearAllMocks() - // }) - // - // describe('when the page has loaded successfully', () => { - // let wrapper - // beforeEach(() => { - // wrapper = getShallowWrapper() - // }) - // - // it('should display the page title', () => { - // expect(wrapper.find(selectors.pageTitle)).toMatchSnapshot() - // }) - // it('should resolve the provided file id to a path', () => { - // expect(wrapper.vm.$client.files.getPathForFileId).toHaveBeenCalledTimes(1) - // }) - // }) - // - // describe('when the view is still loading', () => { - // it('should display the loading text with the spinner', () => { - // const wrapper = getShallowWrapper(true) - // - // expect(wrapper.find(selectors.loader)).toMatchSnapshot() - // }) - // }) - // - // describe('when there was an error', () => { - // it('should display the error message', async () => { - // jest.spyOn(console, 'error').mockImplementation(() => {}) - // const wrapper = getShallowWrapper( - // false, - // jest.fn(() => Promise.reject(Error('some error'))) - // ) - // - // await new Promise((resolve) => { - // setTimeout(() => { - // expect(wrapper.find(selectors.errorTitle)).toMatchSnapshot() - // resolve() - // }, 1) - // }) - // }) - // }) - // - // describe('when the view has finished loading and there was no error', () => { - // it('should not display the loading text and the error message', () => { - // const wrapper = getShallowWrapper() - // - // expect(wrapper).toMatchSnapshot() - // }) - // }) -}) - -function getShallowWrapper(loading = false, getPathForFileIdMock = jest.fn()) { - return shallowMount(PrivateLink, { - localVue, - store: createStore(), - mocks: { - $route, - $router: getRouter({}), - $client: { - files: { - fileInfo: jest.fn().mockImplementation(() => Promise.resolve(fileFixtures['/'][4])), - getPathForFileId: getPathForFileIdMock - } - } - }, - data() { - return { - loading - } - } - }) -} - -function createStore() { - return getStore({ - slogan: theme.general.slogan, - user: { id: 1 } - }) -} diff --git a/packages/web-client/src/helpers/share/role.ts b/packages/web-client/src/helpers/share/role.ts index eacb954f069..2309dd81e58 100644 --- a/packages/web-client/src/helpers/share/role.ts +++ b/packages/web-client/src/helpers/share/role.ts @@ -364,9 +364,14 @@ export abstract class LinkShareRoles { linkRoleUploaderFolder ] - static list(isFolder: boolean, canEditFile = false, hasAliasLinks = false): ShareRole[] { + static list( + isFolder: boolean, + canEditFile = false, + hasAliasLinks = false, + hasPassword = false + ): ShareRole[] { return [ - ...(hasAliasLinks ? [linkRoleInternalFile, linkRoleInternalFolder] : []), + ...(hasAliasLinks && !hasPassword ? [linkRoleInternalFile, linkRoleInternalFolder] : []), ...this.all, ...(canEditFile ? [linkRoleEditorFile] : []) ].filter((r) => r.folder === isFolder) @@ -383,15 +388,17 @@ export abstract class LinkShareRoles { * @param isFolder * @param canEditFile * @param hasAliasLinks + * @param hasPassword */ static filterByBitmask( bitmask: number, isFolder: boolean, canEditFile = false, - hasAliasLinks = false + hasAliasLinks = false, + hasPassword = false ): ShareRole[] { return [ - ...(hasAliasLinks ? [linkRoleInternalFile, linkRoleInternalFolder] : []), + ...(hasAliasLinks && !hasPassword ? [linkRoleInternalFile, linkRoleInternalFolder] : []), ...this.all, ...(canEditFile ? [linkRoleEditorFile] : []) ].filter((r) => { diff --git a/packages/web-client/src/types.ts b/packages/web-client/src/types.ts index 7897b075c47..c61b0c62cfa 100644 --- a/packages/web-client/src/types.ts +++ b/packages/web-client/src/types.ts @@ -34,6 +34,8 @@ export type OwnCloudSdk = { shares: { getShare(...args): any getShares(...args): any + getProtectedTokenInfo(...args): any + getUnprotectedTokenInfo(...args): any } users: { getUser(...args): any diff --git a/packages/web-runtime/package.json b/packages/web-runtime/package.json index fbadaf36df3..c13482560de 100644 --- a/packages/web-runtime/package.json +++ b/packages/web-runtime/package.json @@ -25,7 +25,7 @@ "marked": "^4.0.12", "oidc-client-ts": "^2.0.5", "owncloud-design-system": "14.0.0-alpha.22", - "owncloud-sdk": "~3.0.0-alpha.15", + "owncloud-sdk": "~3.0.0-alpha.17", "p-queue": "^6.6.2", "popper-max-size-modifier": "^0.2.0", "portal-vue": "^2.1.7", diff --git a/packages/web-runtime/src/composables/tokenInfo/index.ts b/packages/web-runtime/src/composables/tokenInfo/index.ts new file mode 100644 index 00000000000..e04af314e61 --- /dev/null +++ b/packages/web-runtime/src/composables/tokenInfo/index.ts @@ -0,0 +1 @@ +export * from './useLoadTokenInfo' diff --git a/packages/web-runtime/src/composables/tokenInfo/useLoadTokenInfo.ts b/packages/web-runtime/src/composables/tokenInfo/useLoadTokenInfo.ts new file mode 100644 index 00000000000..ae3daae975f --- /dev/null +++ b/packages/web-runtime/src/composables/tokenInfo/useLoadTokenInfo.ts @@ -0,0 +1,31 @@ +import { unref } from '@vue/composition-api' +import { useTask } from 'vue-concurrency' +import { useClientService, useStore, useUserContext } from 'web-pkg/src/composables' + +export function useLoadTokenInfo(token) { + const { owncloudSdk } = useClientService() + const store = useStore() + const isUserContext = useUserContext({ store }) + + const loadTokenInfoTask = useTask(function* () { + let tokenInfo + + try { + if (unref(isUserContext)) { + tokenInfo = yield owncloudSdk.shares.getProtectedTokenInfo(token) + } else { + tokenInfo = yield owncloudSdk.shares.getUnprotectedTokenInfo(token) + } + } catch (e) { + // backend doesn't support the token info endpoint + return {} + } + return { + ...tokenInfo, + alias_link: tokenInfo.alias_link === 'true', + password_protected: tokenInfo.password_protected === 'true' + } + }) + + return { loadTokenInfoTask } +} diff --git a/packages/web-runtime/src/pages/resolveFileLink.vue b/packages/web-runtime/src/pages/resolveFileLink.vue new file mode 100644 index 00000000000..47a6ed0c7b1 --- /dev/null +++ b/packages/web-runtime/src/pages/resolveFileLink.vue @@ -0,0 +1,131 @@ + + + + + diff --git a/packages/web-runtime/src/pages/resolvePublicLink.vue b/packages/web-runtime/src/pages/resolvePublicLink.vue index 97ecea97472..eda944eeb24 100644 --- a/packages/web-runtime/src/pages/resolvePublicLink.vue +++ b/packages/web-runtime/src/pages/resolvePublicLink.vue @@ -2,7 +2,14 @@