diff --git a/changelog/unreleased/enhancement-history-mode b/changelog/unreleased/enhancement-history-mode
new file mode 100644
index 00000000000..4d4055291d4
--- /dev/null
+++ b/changelog/unreleased/enhancement-history-mode
@@ -0,0 +1,8 @@
+Enhancement: Option to enable Vue history mode
+
+We've added the option to use vue's history mode. All configuration is done automatically by the system.
+To enable it, add a `` header tag to `index.html`, `oidc-callback.html` and `oidc-silent-redirect.html`.
+Adding `` is not needed for ocis.
+
+https://github.com/owncloud/web/issues/6363
+https://github.com/owncloud/web/issues/6277
diff --git a/changelog/unreleased/enhancement-sidecar-mode b/changelog/unreleased/enhancement-sidecar-mode
new file mode 100644
index 00000000000..4d7f41cc335
--- /dev/null
+++ b/changelog/unreleased/enhancement-sidecar-mode
@@ -0,0 +1,6 @@
+Enhancement: Run web as oc10 sidecar
+
+We've added the option to run web in oc10 sidecar mode.
+Copy `config/config.json.sample-oc10` to `config/config.json`, run `yarn server` and then `docker compose up oc10`.
+
+https://github.com/owncloud/web/issues/6363
diff --git a/config/config.json.sample-oc10 b/config/config.json.sample-oc10
index 1a7bb458b3e..12987676929 100644
--- a/config/config.json.sample-oc10
+++ b/config/config.json.sample-oc10
@@ -3,7 +3,7 @@
"theme": "https://localhost:9100/themes/owncloud/theme.json",
"version": "0.1.0",
"auth": {
- "clientId": "",
+ "clientId": "UmCVsEIxdWmssxa6uVRRPC3txYBVN4qqxooJbsPhuuoPmHk9Pt9Oy68N4ZaKXUYy",
"url": "http://localhost:8080/index.php/apps/oauth2/api/v1/token",
"authUrl": "http://localhost:8080/index.php/apps/oauth2/authorize"
},
diff --git a/dev/docker/oc10.Dockerfile b/dev/docker/oc10.Dockerfile
new file mode 100644
index 00000000000..d5bb584d1e3
--- /dev/null
+++ b/dev/docker/oc10.Dockerfile
@@ -0,0 +1,8 @@
+ARG OC10_IMAGE
+FROM ${OC10_IMAGE}
+
+RUN apt -qqy update \
+ && apt -qqy --no-install-recommends install \
+ bash \
+ && rm -rf /var/lib/apt/lists/* \
+ && apt -qyy clean
diff --git a/dev/docker/oc10.entrypoint.sh b/dev/docker/oc10.entrypoint.sh
index 469a88574b3..8f897df99cc 100755
--- a/dev/docker/oc10.entrypoint.sh
+++ b/dev/docker/oc10.entrypoint.sh
@@ -24,6 +24,13 @@ then
M8W5mo3wQV3VHWYsaYpWhkr8dwa949i4GljCkedHhl7GWqmHMkxSeJgK2PcS0jt5 \
sqvPYXK94tMsEEVOYORxg8Ufesi2kC4WpJJSYb0Kj1DSAYl6u2XvJZjc3VcitjDv \
http://host.docker.internal:8080/index.php/apps/web/oidc-callback.html
+ occ oauth2:add-client \
+ web-sidecar \
+ UmCVsEIxdWmssxa6uVRRPC3txYBVN4qqxooJbsPhuuoPmHk9Pt9Oy68N4ZaKXUYy \
+ HW1fo6lbtgEERBQufBouJ4HID2QaDfngvIdc2vjDUE46qKB4JRG1YDir41LliReC \
+ http://localhost:9100/oidc-callback.html
+ occ config:system:set trusted_domains 0 --value="localhost"
+ occ config:system:set cors.allowed-domains 0 --value="http://localhost:9100"
fi
if [ -d /var/www/owncloud/apps/web/ ]
diff --git a/docker-compose.yml b/docker-compose.yml
index e755ddf182b..6657f78fa90 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -22,7 +22,11 @@ services:
- host.docker.internal:${DOCKER_HOST:-host-gateway}
oc10:
- image: ${OC10_IMAGE:-owncloud/server:latest}
+ build:
+ dockerfile: oc10.Dockerfile
+ context: ./dev/docker
+ args:
+ OC10_IMAGE: ${OC10_IMAGE:-owncloud/server:latest}
container_name: web_oc10
ports:
- 8080:8080
diff --git a/packages/web-container/oidc-callback.html b/packages/web-container/oidc-callback.html
index 06aa86652d4..ee4ee43141f 100644
--- a/packages/web-container/oidc-callback.html
+++ b/packages/web-container/oidc-callback.html
@@ -1,10 +1,13 @@
+
+
diff --git a/packages/web-container/oidc-silent-redirect.html b/packages/web-container/oidc-silent-redirect.html
index 8c83b021f96..fad53492f8d 100644
--- a/packages/web-container/oidc-silent-redirect.html
+++ b/packages/web-container/oidc-silent-redirect.html
@@ -1,10 +1,13 @@
+
+
diff --git a/packages/web-runtime/src/defaults/index.ts b/packages/web-runtime/src/defaults/index.ts
index cd785b1a2a9..3aa89859f1a 100644
--- a/packages/web-runtime/src/defaults/index.ts
+++ b/packages/web-runtime/src/defaults/index.ts
@@ -8,7 +8,6 @@ import { createStore } from 'vuex-extensions'
import Vuex from 'vuex'
export { default as Vue } from './vue'
export { default as DesignSystem } from 'owncloud-design-system'
-export { default as Router } from '../router'
export const store = createStore(Vuex.Store, { ...Store })
export const pages = { success: App, failure: missingOrInvalidConfigPage }
diff --git a/packages/web-runtime/src/index.ts b/packages/web-runtime/src/index.ts
index a753e17a530..89e8c7477db 100644
--- a/packages/web-runtime/src/index.ts
+++ b/packages/web-runtime/src/index.ts
@@ -4,9 +4,11 @@ import {
translations,
supportedLanguages,
store,
- Router as router,
Vue
} from './defaults'
+
+import { router } from './router'
+
import {
requestConfiguration,
announceApplications,
diff --git a/packages/web-runtime/src/router/index.js b/packages/web-runtime/src/router/index.js
index 3cb6223242d..1c6dc4c9f8e 100644
--- a/packages/web-runtime/src/router/index.js
+++ b/packages/web-runtime/src/router/index.js
@@ -9,66 +9,120 @@ import Account from '../pages/account.vue'
Vue.use(Router)
+// type: patch
+// temporary patch till we have upgraded web to the latest vue router which make this obsolete
+// this takes care that routes like 'foo/bar/baz' which by default would be converted to 'foo%2Fbar%2Fbaz' stay as they are
+// should immediately go away and be removed after finalizing the update
+// to apply the patch to a route add meta.patchCleanPath = true to it
+// to patch needs to be enabled on a route level, to do so add meta.patchCleanPath = true property to the route
+const patchRouter = (router) => {
+ const bindMatcher = router.match.bind(router)
+ const cleanPath = (route) =>
+ [
+ ['%2F', '/'],
+ ['//', '/']
+ ].reduce((path, rule) => path.replaceAll(rule[0], rule[1]), route || '')
+
+ router.match = (raw, current, redirectFrom) => {
+ const bindMatch = bindMatcher(raw, current, redirectFrom)
+
+ if (!get(bindMatch, 'meta.patchCleanPath', false)) {
+ return bindMatch
+ }
+
+ return {
+ ...bindMatch,
+ path: cleanPath(bindMatch.path),
+ fullPath: cleanPath(bindMatch.fullPath)
+ }
+ }
+
+ return router
+}
+
// just a dummy function to trick gettext tools
function $gettext(msg) {
return msg
}
-const router = new Router({
- // mode: 'history',
- routes: [
- {
- path: '/login',
- name: 'login',
- components: {
- fullscreen: LoginPage
+const base = document.querySelector('base')
+export const router = patchRouter(
+ new Router({
+ ...(base && {
+ mode: 'history',
+ base: new URL(base.href).pathname
+ }),
+ routes: [
+ {
+ path: '/login',
+ name: 'login',
+ components: {
+ fullscreen: LoginPage
+ },
+ meta: { auth: false, hideHeadbar: true, title: $gettext('Login') }
},
- meta: { auth: false, hideHeadbar: true, title: $gettext('Login') }
- },
- {
- path: '/oidc-callback',
- components: {
- fullscreen: OidcCallbackPage
+ {
+ path: '/oidc-callback',
+ components: {
+ fullscreen: OidcCallbackPage
+ },
+ meta: { auth: false, hideHeadbar: true, title: $gettext('Oidc callback') }
},
- meta: { auth: false, hideHeadbar: true, title: $gettext('Oidc callback') }
- },
- {
- path: '/oidc-silent-redirect',
- components: {
- fullscreen: OidcCallbackPage
+ {
+ path: '/oidc-silent-redirect',
+ components: {
+ fullscreen: OidcCallbackPage
+ },
+ meta: { auth: false, hideHeadbar: true, title: $gettext('Oidc redirect') }
},
- meta: { auth: false, hideHeadbar: true, title: $gettext('Oidc redirect') }
- },
- {
- path: '/f/:fileId',
- name: 'privateLink',
- redirect: '/files/ops/resolver/private-link/:fileId',
- meta: { hideHeadbar: true, title: $gettext('Private link') }
- },
- {
- path: '/s/:token',
- name: 'publicLink',
- redirect: '/files/ops/resolver/public-link/:token',
- meta: { auth: false, hideHeadbar: true, title: $gettext('Public link') }
- },
- {
- path: '/access-denied',
- name: 'accessDenied',
- components: {
- fullscreen: AccessDeniedPage
+ {
+ path: '/f/:fileId',
+ name: 'privateLink',
+ redirect: '/files/ops/resolver/private-link/:fileId',
+ meta: { hideHeadbar: true, title: $gettext('Private link') }
},
- meta: { auth: false, hideHeadbar: true, title: $gettext('Access denied') }
- },
- {
- path: '/account',
- name: 'account',
- components: {
- app: Account
+ {
+ path: '/s/:token',
+ name: 'publicLink',
+ redirect: '/files/ops/resolver/public-link/:token',
+ meta: { auth: false, hideHeadbar: true, title: $gettext('Public link') }
},
- meta: { title: $gettext('Account') }
- }
- ]
-})
+ {
+ path: '/access-denied',
+ name: 'accessDenied',
+ components: {
+ fullscreen: AccessDeniedPage
+ },
+ meta: { auth: false, hideHeadbar: true, title: $gettext('Access denied') }
+ },
+ {
+ path: '/account',
+ name: 'account',
+ components: {
+ app: Account
+ },
+ meta: { title: $gettext('Account') }
+ }
+ ]
+ })
+)
+
+export const buildUrl = (pathname) => {
+ const baseUrl = new URL(window.location.href.split('#')[0])
+ if (baseUrl.pathname.endsWith('/index.html')) {
+ baseUrl.pathname = baseUrl.pathname.substr(0, baseUrl.pathname.length - 11)
+ }
+
+ if (/\.(html?)$/i.test(pathname)) {
+ baseUrl.pathname = base
+ ? pathname
+ : [...baseUrl.pathname.split('/'), ...pathname.split('/')].filter(Boolean).join('/')
+ } else {
+ baseUrl[base ? 'pathname' : 'hash'] = router.resolve(pathname).href
+ }
+
+ return baseUrl.href
+}
router.beforeEach(function (to, from, next) {
const store = Vue.$store
@@ -117,36 +171,3 @@ const isAuthRequired = (router, to) => {
}
return false
}
-
-// type: patch
-// temporary patch till we have upgraded web to the latest vue router which make this obsolete
-// this takes care that routes like 'foo/bar/baz' which by default would be converted to 'foo%2Fbar%2Fbaz' stay as they are
-// should immediately go away and be removed after finalizing the update
-// to apply the patch to a route add meta.patchCleanPath = true to it
-// to patch needs to be enabled on a route level, to do so add meta.patchCleanPath = true property to the route
-const patchRouter = (router) => {
- const bindMatcher = router.match.bind(router)
- const cleanPath = (route) =>
- [
- ['%2F', '/'],
- ['//', '/']
- ].reduce((path, rule) => path.replaceAll(rule[0], rule[1]), route || '')
-
- router.match = (raw, current, redirectFrom) => {
- const bindMatch = bindMatcher(raw, current, redirectFrom)
-
- if (!get(bindMatch, 'meta.patchCleanPath', false)) {
- return bindMatch
- }
-
- return {
- ...bindMatch,
- path: cleanPath(bindMatch.path),
- fullPath: cleanPath(bindMatch.fullPath)
- }
- }
-
- return router
-}
-
-export default patchRouter(router)
diff --git a/packages/web-runtime/src/services/auth.js b/packages/web-runtime/src/services/auth.js
index 34a01175a7b..b3cf48d024b 100644
--- a/packages/web-runtime/src/services/auth.js
+++ b/packages/web-runtime/src/services/auth.js
@@ -1,4 +1,5 @@
import { Log, User, UserManager, WebStorageStateStore } from 'oidc-client'
+import { buildUrl } from '../router'
export function initVueAuthenticate(config) {
if (config) {
@@ -6,21 +7,17 @@ export function initVueAuthenticate(config) {
prefix: 'oc_oAuth',
store: sessionStorage
})
- let baseUrl = window.location.href.split('#')[0]
- if (baseUrl.endsWith('/index.html')) {
- baseUrl = baseUrl.substr(0, baseUrl.length - 10)
- }
const openIdConfig = {
userStore: store,
- redirect_uri: baseUrl + 'oidc-callback.html',
+ redirect_uri: buildUrl('/oidc-callback.html'),
response_type: 'code', // code triggers auth code grant flow
response_mode: 'query',
scope: 'openid profile offline_access',
monitorSession: false,
// set uri directly to the login route to prevent problems with query parameters.
// See https://github.com/owncloud/web/issues/3285
- post_logout_redirect_uri: baseUrl + '#/login',
- silent_redirect_uri: baseUrl + 'oidc-silent-redirect.html',
+ post_logout_redirect_uri: buildUrl('/login'),
+ silent_redirect_uri: buildUrl('/oidc-silent-redirect.html'),
accessTokenExpiringNotificationTime: 10,
automaticSilentRenew: true,
filterProtocolClaims: true,
diff --git a/packages/web-runtime/src/services/clientRegistration.js b/packages/web-runtime/src/services/clientRegistration.js
index 9cd139bf853..61b545cbb56 100644
--- a/packages/web-runtime/src/services/clientRegistration.js
+++ b/packages/web-runtime/src/services/clientRegistration.js
@@ -1,3 +1,5 @@
+import { buildUrl } from '../router'
+
async function get(url) {
return await fetch(url)
.then((res) => {
@@ -35,16 +37,10 @@ export async function registerClient(openIdConfig) {
}
}
sessionStorage.removeItem('dynamicClientData')
-
- let baseUrl = window.location.href.split('#')[0]
- if (baseUrl.endsWith('/index.html')) {
- baseUrl = baseUrl.substr(0, baseUrl.length - 10)
- }
-
const wellKnown = await get(`${openIdConfig.authority}/.well-known/openid-configuration`)
const resp = await post(wellKnown.registration_endpoint, {
- redirect_uris: [baseUrl + 'oidc-callback.html'],
- client_name: `ownCloud Web on ${baseUrl}`
+ redirect_uris: [buildUrl('/oidc-callback.html')],
+ client_name: `ownCloud Web on ${window.location.origin}`
})
sessionStorage.setItem('dynamicClientData', JSON.stringify(resp))
return resp
diff --git a/packages/web-runtime/src/store/user.js b/packages/web-runtime/src/store/user.js
index 096934c5107..c2d8da9829f 100644
--- a/packages/web-runtime/src/store/user.js
+++ b/packages/web-runtime/src/store/user.js
@@ -1,7 +1,7 @@
import get from 'lodash-es/get.js'
import isEmpty from 'lodash-es/isEmpty'
import initVueAuthenticate from '../services/auth'
-import router from '../router/'
+import { router } from '../router'
import SidebarQuota from '../components/SidebarQuota.vue'
diff --git a/packages/web-runtime/tests/unit/router/index.spec.ts b/packages/web-runtime/tests/unit/router/index.spec.ts
new file mode 100644
index 00000000000..8b55eb6d126
--- /dev/null
+++ b/packages/web-runtime/tests/unit/router/index.spec.ts
@@ -0,0 +1,29 @@
+describe('buildUrl', () => {
+ it.each`
+ location | base | path | expected
+ ${'https://localhost:8080/index.php/apps/web/index.html#/files/list/all'} | ${''} | ${'/login'} | ${'https://localhost:8080/index.php/apps/web#/login'}
+ ${'https://localhost:8080/index.php/apps/web/index.html#/files/list/all'} | ${''} | ${'/login/foo'} | ${'https://localhost:8080/index.php/apps/web#/login/foo'}
+ ${'https://localhost:8080/index.php/apps/web/#/login'} | ${''} | ${'/bar.html'} | ${'https://localhost:8080/index.php/apps/web/bar.html'}
+ ${'https://localhost:9200/#/files/list/all'} | ${''} | ${'/login/foo'} | ${'https://localhost:9200/#/login/foo'}
+ ${'https://localhost:9200/#/files/list/all'} | ${''} | ${'/bar.html'} | ${'https://localhost:9200/bar.html'}
+ ${'https://localhost:9200/files/list/all'} | ${'/'} | ${'/login/foo'} | ${'https://localhost:9200/login/foo'}
+ ${'https://localhost:9200/files/list/all'} | ${'/foo'} | ${'/bar.html'} | ${'https://localhost:9200/bar.html'}
+ ${'https://localhost:9200/files/list/all'} | ${'/foo'} | ${'/bar.htm'} | ${'https://localhost:9200/bar.htm'}
+ `('$path -> $expected', async ({ location, base, path, expected }) => {
+ delete window.location
+ window.location = new URL(location) as any
+
+ document.querySelectorAll('base').forEach((e) => e.remove())
+
+ if (base) {
+ const baseElement = document.createElement('base')
+ baseElement.href = base
+ document.getElementsByTagName('head')[0].appendChild(baseElement)
+ }
+
+ const { buildUrl } = await import('../../../src/router')
+ jest.resetModules()
+
+ expect(buildUrl(path)).toBe(expected)
+ })
+})