diff options
Diffstat (limited to 'apps/theming/src')
-rw-r--r-- | apps/theming/src/AdminTheming.vue | 4 | ||||
-rw-r--r-- | apps/theming/src/UserTheming.vue | 59 | ||||
-rw-r--r-- | apps/theming/src/admin-settings.js | 4 | ||||
-rw-r--r-- | apps/theming/src/components/AppOrderSelectorElement.vue | 2 | ||||
-rw-r--r-- | apps/theming/src/components/BackgroundSettings.vue | 58 | ||||
-rw-r--r-- | apps/theming/src/components/ItemPreview.vue | 27 | ||||
-rw-r--r-- | apps/theming/src/components/UserAppMenuSection.vue | 31 | ||||
-rw-r--r-- | apps/theming/src/components/UserPrimaryColor.vue | 10 | ||||
-rw-r--r-- | apps/theming/src/components/admin/AppMenuSection.vue | 14 | ||||
-rw-r--r-- | apps/theming/src/components/admin/CheckboxField.vue | 14 | ||||
-rw-r--r-- | apps/theming/src/components/admin/ColorPickerField.vue | 11 | ||||
-rw-r--r-- | apps/theming/src/components/admin/FileInputField.vue | 14 | ||||
-rw-r--r-- | apps/theming/src/components/admin/TextField.vue | 2 | ||||
-rw-r--r-- | apps/theming/src/mixins/admin/TextValueMixin.js | 39 | ||||
-rw-r--r-- | apps/theming/src/personal-settings.js | 4 |
15 files changed, 137 insertions, 156 deletions
diff --git a/apps/theming/src/AdminTheming.vue b/apps/theming/src/AdminTheming.vue index 63620750159..e899024ca53 100644 --- a/apps/theming/src/AdminTheming.vue +++ b/apps/theming/src/AdminTheming.vue @@ -115,8 +115,8 @@ import { loadState } from '@nextcloud/initial-state' import { refreshStyles } from './helpers/refreshStyles.js' -import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js' -import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js' +import NcNoteCard from '@nextcloud/vue/components/NcNoteCard' +import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection' import CheckboxField from './components/admin/CheckboxField.vue' import ColorPickerField from './components/admin/ColorPickerField.vue' import FileInputField from './components/admin/FileInputField.vue' diff --git a/apps/theming/src/UserTheming.vue b/apps/theming/src/UserTheming.vue index a5c3d029158..baebf09bcc5 100644 --- a/apps/theming/src/UserTheming.vue +++ b/apps/theming/src/UserTheming.vue @@ -6,7 +6,6 @@ <template> <section> <NcSettingsSection :name="t('theming', 'Appearance and accessibility settings')" - :limit-width="false" class="theming"> <!-- eslint-disable-next-line vue/no-v-html --> <p v-html="description" /> @@ -77,14 +76,15 @@ </template> <script> -import { generateOcsUrl } from '@nextcloud/router' +import { showError } from '@nextcloud/dialogs' import { loadState } from '@nextcloud/initial-state' +import { generateOcsUrl } from '@nextcloud/router' import { refreshStyles } from './helpers/refreshStyles' -import axios from '@nextcloud/axios' +import axios, { isAxiosError } from '@nextcloud/axios' -import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' -import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js' +import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch' +import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection' import BackgroundSettings from './components/BackgroundSettings.vue' import ItemPreview from './components/ItemPreview.vue' @@ -138,35 +138,32 @@ export default { }, description() { - // using the `t` replace method escape html, we have to do it manually :/ return t( 'theming', - 'Universal access is very important to us. We follow web standards and check to make everything usable also without mouse, and assistive software such as screenreaders. We aim to be compliant with the {guidelines}Web Content Accessibility Guidelines{linkend} 2.1 on AA level, with the high contrast theme even on AAA level.', + 'Universal access is very important to us. We follow web standards and check to make everything usable also without mouse, and assistive software such as screenreaders. We aim to be compliant with the {linkstart}Web Content Accessibility Guidelines{linkend} 2.1 on AA level, with the high contrast theme even on AAA level.', + { + linkstart: '<a target="_blank" href="https://www.w3.org/WAI/standards-guidelines/wcag/" rel="noreferrer nofollow">', + linkend: '</a>', + }, + { + escape: false, + }, ) - .replace('{guidelines}', this.guidelinesLink) - .replace('{linkend}', '</a>') - }, - - guidelinesLink() { - return '<a target="_blank" href="https://www.w3.org/WAI/standards-guidelines/wcag/" rel="noreferrer nofollow">' }, descriptionDetail() { return t( 'theming', 'If you find any issues, do not hesitate to report them on {issuetracker}our issue tracker{linkend}. And if you want to get involved, come join {designteam}our design team{linkend}!', + { + issuetracker: '<a target="_blank" href="https://github.com/nextcloud/server/issues/" rel="noreferrer nofollow">', + designteam: '<a target="_blank" href="https://nextcloud.com/design" rel="noreferrer nofollow">', + linkend: '</a>', + }, + { + escape: false, + }, ) - .replace('{issuetracker}', this.issuetrackerLink) - .replace('{designteam}', this.designteamLink) - .replace(/\{linkend\}/g, '</a>') - }, - - issuetrackerLink() { - return '<a target="_blank" href="https://github.com/nextcloud/server/issues/" rel="noreferrer nofollow">' - }, - - designteamLink() { - return '<a target="_blank" href="https://nextcloud.com/design" rel="noreferrer nofollow">' }, }, @@ -285,9 +282,13 @@ export default { }) } - } catch (err) { - console.error(err, err.response) - OC.Notification.showTemporary(t('theming', err.response.data.ocs.meta.message + '. Unable to apply the setting.')) + } catch (error) { + console.error('theming: Unable to apply setting.', error) + let message = t('theming', 'Unable to apply the setting.') + if (isAxiosError(error) && error.response.data.ocs?.meta?.message) { + message = `${error.response.data.ocs.meta.message}. ${message}` + } + showError(message) } }, }, @@ -302,7 +303,7 @@ export default { } // Proper highlight for links and focus feedback - &::v-deep a { + :deep(a) { font-weight: bold; &:hover, @@ -313,12 +314,10 @@ export default { &__preview-list { --gap: 30px; - display: grid; margin-top: var(--gap); column-gap: var(--gap); row-gap: var(--gap); - grid-template-columns: 1fr 1fr; } } diff --git a/apps/theming/src/admin-settings.js b/apps/theming/src/admin-settings.js index d6165ee7453..622837658f9 100644 --- a/apps/theming/src/admin-settings.js +++ b/apps/theming/src/admin-settings.js @@ -2,13 +2,13 @@ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { getRequestToken } from '@nextcloud/auth' +import { getCSPNonce } from '@nextcloud/auth' import Vue from 'vue' import App from './AdminTheming.vue' // eslint-disable-next-line camelcase -__webpack_nonce__ = btoa(getRequestToken()) +__webpack_nonce__ = getCSPNonce() Vue.prototype.OC = OC Vue.prototype.t = t diff --git a/apps/theming/src/components/AppOrderSelectorElement.vue b/apps/theming/src/components/AppOrderSelectorElement.vue index 6336173f97a..fc41e8e6165 100644 --- a/apps/theming/src/components/AppOrderSelectorElement.vue +++ b/apps/theming/src/components/AppOrderSelectorElement.vue @@ -65,7 +65,7 @@ import { defineComponent, nextTick, ref } from 'vue' import IconArrowDown from 'vue-material-design-icons/ArrowDown.vue' import IconArrowUp from 'vue-material-design-icons/ArrowUp.vue' -import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' +import NcButton from '@nextcloud/vue/components/NcButton' interface IApp { id: string // app id diff --git a/apps/theming/src/components/BackgroundSettings.vue b/apps/theming/src/components/BackgroundSettings.vue index 6368f0958d5..58b76dd9602 100644 --- a/apps/theming/src/components/BackgroundSettings.vue +++ b/apps/theming/src/components/BackgroundSettings.vue @@ -78,25 +78,22 @@ </template> <script> -import { generateFilePath, generateRemoteUrl, generateUrl } from '@nextcloud/router' -import { getCurrentUser } from '@nextcloud/auth' +import { generateFilePath, generateUrl } from '@nextcloud/router' import { getFilePickerBuilder, showError } from '@nextcloud/dialogs' import { loadState } from '@nextcloud/initial-state' -import { Palette } from 'node-vibrant/lib/color.js' import axios from '@nextcloud/axios' import debounce from 'debounce' -import NcColorPicker from '@nextcloud/vue/dist/Components/NcColorPicker.js' -import Vibrant from 'node-vibrant' +import NcColorPicker from '@nextcloud/vue/components/NcColorPicker' import Check from 'vue-material-design-icons/Check.vue' import ImageEdit from 'vue-material-design-icons/ImageEdit.vue' -import ColorPalette from 'vue-material-design-icons/Palette.vue' +import ColorPalette from 'vue-material-design-icons/PaletteOutline.vue' const shippedBackgroundList = loadState('theming', 'shippedBackgrounds') const backgroundImage = loadState('theming', 'userBackgroundImage') const { backgroundImage: defaultBackgroundImage, - backgroundColor: defaultBackgroundColor, + // backgroundColor: defaultBackgroundColor, backgroundMime: defaultBackgroundMime, defaultShippedBackground, } = loadState('theming', 'themingDefaults') @@ -217,9 +214,9 @@ export default { this.update(result.data) }, - async setFile(path, color = null) { + async setFile(path) { this.loading = 'custom' - const result = await axios.post(generateUrl('/apps/theming/background/custom'), { value: path, color }) + const result = await axios.post(generateUrl('/apps/theming/background/custom'), { value: path }) this.update(result.data) }, @@ -237,7 +234,7 @@ export default { debouncePickColor: debounce(function(...args) { this.pickColor(...args) - }, 200), + }, 1000), pickFile() { const picker = getFilePickerBuilder(t('theming', 'Select a background from your files')) @@ -264,45 +261,7 @@ export default { } this.loading = 'custom' - - // Extract primary color from image - let response = null - let color = null - try { - const fileUrl = generateRemoteUrl('dav/files/' + getCurrentUser().uid + path) - response = await axios.get(fileUrl, { responseType: 'blob' }) - const blobUrl = URL.createObjectURL(response.data) - const palette = await this.getColorPaletteFromBlob(blobUrl) - - // DarkVibrant is accessible AND visually pleasing - // Vibrant is not accessible enough and others are boring - color = palette?.DarkVibrant?.hex - this.setFile(path, color) - - // Log data - console.debug('Extracted colour', color, 'from custom image', path, palette) - } catch (error) { - this.setFile(path) - console.error('Unable to extract colour from custom image', { error, path, response, color }) - } - }, - - /** - * Extract a Vibrant color palette from a blob URL - * - * @param {string} blobUrl the blob URL - * @return {Promise<Palette>} - */ - getColorPaletteFromBlob(blobUrl) { - return new Promise((resolve, reject) => { - const vibrant = new Vibrant(blobUrl) - vibrant.getPalette((error, palette) => { - if (error) { - reject(error) - } - resolve(palette) - }) - }) + this.setFile(path) }, }, } @@ -339,7 +298,6 @@ export default { background-size: cover; &__filepicker { - background-color: var(--color-main-text); background-color: var(--color-background-dark); &.background--active { diff --git a/apps/theming/src/components/ItemPreview.vue b/apps/theming/src/components/ItemPreview.vue index 1e34d947d7d..e4a1acd3e2a 100644 --- a/apps/theming/src/components/ItemPreview.vue +++ b/apps/theming/src/components/ItemPreview.vue @@ -7,11 +7,16 @@ <div class="theming__preview-image" :style="{ backgroundImage: 'url(' + img + ')' }" @click="onToggle" /> <div class="theming__preview-description"> <h3>{{ theme.title }}</h3> - <p class="theming__preview-explanation">{{ theme.description }}</p> + <p class="theming__preview-explanation"> + {{ theme.description }} + </p> <span v-if="enforced" class="theming__preview-warning" role="note"> {{ t('theming', 'Theme selection is enforced') }} </span> - <NcCheckboxRadioSwitch class="theming__preview-toggle" + + <!-- Only show checkbox if we can change themes --> + <NcCheckboxRadioSwitch v-show="!enforced" + class="theming__preview-toggle" :checked.sync="checked" :disabled="enforced" :name="name" @@ -24,7 +29,7 @@ <script> import { generateFilePath } from '@nextcloud/router' -import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' +import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch' export default { name: 'ItemPreview', @@ -71,6 +76,10 @@ export default { return this.selected }, set(checked) { + if (this.enforced) { + return + } + console.debug('Changed theme', this.theme.id, checked) // If this is a radio, we can only enable @@ -87,6 +96,10 @@ export default { methods: { onToggle() { + if (this.enforced) { + return + } + if (this.switchType === 'radio') { this.checked = true return @@ -104,11 +117,9 @@ export default { .theming__preview { // We make previews on 16/10 screens --ratio: 16; - position: relative; display: flex; justify-content: flex-start; - max-width: 800px; &, * { @@ -119,7 +130,7 @@ export default { flex-basis: calc(16px * var(--ratio)); flex-shrink: 0; height: calc(10px * var(--ratio)); - margin-right: var(--gap); + margin-inline-end: var(--gap); cursor: pointer; border-radius: var(--border-radius); background-repeat: no-repeat; @@ -145,10 +156,6 @@ export default { } } - &--default { - grid-column: span 2; - } - &-warning { color: var(--color-warning); } diff --git a/apps/theming/src/components/UserAppMenuSection.vue b/apps/theming/src/components/UserAppMenuSection.vue index b3d9d9f7694..d4221190f6b 100644 --- a/apps/theming/src/components/UserAppMenuSection.vue +++ b/apps/theming/src/components/UserAppMenuSection.vue @@ -33,6 +33,7 @@ <script lang="ts"> import type { IApp } from './AppOrderSelector.vue' +import type { INavigationEntry } from '../../../../core/src/types/navigation.d.ts' import { showError } from '@nextcloud/dialogs' import { loadState } from '@nextcloud/initial-state' @@ -43,29 +44,9 @@ import { computed, defineComponent, ref } from 'vue' import axios from '@nextcloud/axios' import AppOrderSelector from './AppOrderSelector.vue' import IconUndo from 'vue-material-design-icons/Undo.vue' -import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' -import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js' -import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js' - -/** See NavigationManager */ -interface INavigationEntry { - /** Navigation id */ - id: string - /** Order where this entry should be shown */ - order: number - /** Target of the navigation entry */ - href: string - /** The icon used for the naviation entry */ - icon: string - /** Type of the navigation entry ('link' vs 'settings') */ - type: 'link' | 'settings' - /** Localized name of the navigation entry */ - name: string - /** Whether this is the default app */ - default?: boolean - /** App that registered this navigation entry (not necessarly the same as the id) */ - app?: string -} +import NcButton from '@nextcloud/vue/components/NcButton' +import NcNoteCard from '@nextcloud/vue/components/NcNoteCard' +import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection' /** The app order user setting */ type IAppOrder = Record<string, { order: number, app?: string }> @@ -98,9 +79,9 @@ export default defineComponent({ /** * Array of all available apps, it is set by a core controller for the app menu, so it is always available */ - const initialAppOrder = Object.values(loadState<Record<string, INavigationEntry>>('core', 'apps')) + const initialAppOrder = loadState<INavigationEntry[]>('core', 'apps') .filter(({ type }) => type === 'link') - .map((app) => ({ ...app, label: app.name, default: app.default && app.app === enforcedDefaultApp })) + .map((app) => ({ ...app, label: app.name, default: app.default && app.id === enforcedDefaultApp })) /** * Check if a custom app order is used or the default is shown diff --git a/apps/theming/src/components/UserPrimaryColor.vue b/apps/theming/src/components/UserPrimaryColor.vue index ce39f449ceb..f10b8a01825 100644 --- a/apps/theming/src/components/UserPrimaryColor.vue +++ b/apps/theming/src/components/UserPrimaryColor.vue @@ -35,10 +35,10 @@ import { defineComponent } from 'vue' import axios from '@nextcloud/axios' import debounce from 'debounce' -import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' -import NcColorPicker from '@nextcloud/vue/dist/Components/NcColorPicker.js' -import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' -import IconColorPalette from 'vue-material-design-icons/Palette.vue' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcColorPicker from '@nextcloud/vue/components/NcColorPicker' +import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' +import IconColorPalette from 'vue-material-design-icons/PaletteOutline.vue' import IconUndo from 'vue-material-design-icons/UndoVariant.vue' const { primaryColor, defaultPrimaryColor } = loadState('theming', 'data', { primaryColor: '#0082c9', defaultPrimaryColor: '#0082c9' }) @@ -69,7 +69,7 @@ export default defineComponent({ }, debouncedOnUpdate() { - return debounce(this.onUpdate, 500) + return debounce(this.onUpdate, 1000) }, }, diff --git a/apps/theming/src/components/admin/AppMenuSection.vue b/apps/theming/src/components/admin/AppMenuSection.vue index 2bcb6903bdc..bf229f15df4 100644 --- a/apps/theming/src/components/admin/AppMenuSection.vue +++ b/apps/theming/src/components/admin/AppMenuSection.vue @@ -30,6 +30,8 @@ </template> <script lang="ts"> +import type { INavigationEntry } from '../../../../../core/src/types/navigation' + import { showError } from '@nextcloud/dialogs' import { loadState } from '@nextcloud/initial-state' import { translate as t } from '@nextcloud/l10n' @@ -38,9 +40,9 @@ import { computed, defineComponent } from 'vue' import axios from '@nextcloud/axios' -import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' -import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js' -import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js' +import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch' +import NcSelect from '@nextcloud/vue/components/NcSelect' +import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection' import AppOrderSelector from '../AppOrderSelector.vue' export default defineComponent({ @@ -75,9 +77,8 @@ export default defineComponent({ /** * All enabled apps which can be navigated */ - const allApps = Object.values( - loadState<Record<string, { id: string, name?: string, icon: string }>>('core', 'apps'), - ).map(({ id, name, icon }) => ({ label: name, id, icon })) + const allApps = loadState<INavigationEntry[]>('core', 'apps') + .map(({ id, name, icon }) => ({ label: name, id, icon })) /** * Currently selected app, wrapps the setter @@ -114,6 +115,7 @@ export default defineComponent({ h3, h4 { font-weight: bold; } + h4, h5 { margin-block-start: 12px; } diff --git a/apps/theming/src/components/admin/CheckboxField.vue b/apps/theming/src/components/admin/CheckboxField.vue index 17886189f51..42d86ded4e7 100644 --- a/apps/theming/src/components/admin/CheckboxField.vue +++ b/apps/theming/src/components/admin/CheckboxField.vue @@ -7,15 +7,17 @@ <div class="field"> <label :for="id">{{ displayName }}</label> <div class="field__row"> - <NcCheckboxRadioSwitch type="switch" - :id="id" + <NcCheckboxRadioSwitch :id="id" + type="switch" :checked.sync="localValue" @update:checked="save"> {{ label }} </NcCheckboxRadioSwitch> </div> - <p class="field__description">{{ description }}</p> + <p class="field__description"> + {{ description }} + </p> <NcNoteCard v-if="errorMessage" type="error" @@ -26,8 +28,8 @@ </template> <script> -import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' -import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js' +import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch' +import NcNoteCard from '@nextcloud/vue/components/NcNoteCard' import TextValueMixin from '../../mixins/admin/TextValueMixin.js' @@ -73,7 +75,7 @@ export default { </script> <style lang="scss" scoped> -@import './shared/field.scss'; +@use './shared/field' as *; .field { &__description { diff --git a/apps/theming/src/components/admin/ColorPickerField.vue b/apps/theming/src/components/admin/ColorPickerField.vue index 8e6433064ec..4ec6d47fef6 100644 --- a/apps/theming/src/components/admin/ColorPickerField.vue +++ b/apps/theming/src/components/admin/ColorPickerField.vue @@ -51,10 +51,10 @@ import { colord } from 'colord' import debounce from 'debounce' -import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' -import NcColorPicker from '@nextcloud/vue/dist/Components/NcColorPicker.js' -import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' -import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcColorPicker from '@nextcloud/vue/components/NcColorPicker' +import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' +import NcNoteCard from '@nextcloud/vue/components/NcNoteCard' import Undo from 'vue-material-design-icons/UndoVariant.vue' import Palette from 'vue-material-design-icons/Palette.vue' @@ -136,7 +136,8 @@ export default { </script> <style lang="scss" scoped> -@import './shared/field.scss'; +@use './shared/field' as *; + .description { color: var(--color-text-maxcontrast); } diff --git a/apps/theming/src/components/admin/FileInputField.vue b/apps/theming/src/components/admin/FileInputField.vue index 717f222abbf..d5e0052f5bd 100644 --- a/apps/theming/src/components/admin/FileInputField.vue +++ b/apps/theming/src/components/admin/FileInputField.vue @@ -7,8 +7,8 @@ <div class="field"> <label :for="id">{{ displayName }}</label> <div class="field__row"> - <NcButton type="secondary" - :id="id" + <NcButton :id="id" + type="secondary" :aria-label="ariaLabel" data-admin-theming-setting-file-picker @click="activateLocalFilePicker"> @@ -65,10 +65,10 @@ import axios from '@nextcloud/axios' import { generateUrl } from '@nextcloud/router' import { loadState } from '@nextcloud/initial-state' -import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' -import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' -import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js' -import Delete from 'vue-material-design-icons/Delete.vue' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' +import NcNoteCard from '@nextcloud/vue/components/NcNoteCard' +import Delete from 'vue-material-design-icons/DeleteOutline.vue' import Undo from 'vue-material-design-icons/UndoVariant.vue' import Upload from 'vue-material-design-icons/Upload.vue' @@ -209,7 +209,7 @@ export default { </script> <style lang="scss" scoped> -@import './shared/field.scss'; +@use './shared/field' as *; .field { &__loading-icon { diff --git a/apps/theming/src/components/admin/TextField.vue b/apps/theming/src/components/admin/TextField.vue index b06676a3b20..6ec52733aed 100644 --- a/apps/theming/src/components/admin/TextField.vue +++ b/apps/theming/src/components/admin/TextField.vue @@ -23,7 +23,7 @@ </template> <script> -import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' +import NcTextField from '@nextcloud/vue/components/NcTextField' import TextValueMixin from '../../mixins/admin/TextValueMixin.js' diff --git a/apps/theming/src/mixins/admin/TextValueMixin.js b/apps/theming/src/mixins/admin/TextValueMixin.js index f550884628a..94d63ce1c8c 100644 --- a/apps/theming/src/mixins/admin/TextValueMixin.js +++ b/apps/theming/src/mixins/admin/TextValueMixin.js @@ -21,25 +21,56 @@ export default { data() { return { + /** @type {string|boolean} */ localValue: this.value, } }, + computed: { + valueToPost() { + if (this.type === 'url') { + // if this is already encoded just make sure there is no doublequote (HTML XSS) + // otherwise simply URL encode + return this.isUrlEncoded(this.localValue) + ? this.localValue.replaceAll('"', '%22') + : encodeURI(this.localValue) + } + // Convert boolean to string as server expects string value + if (typeof this.localValue === 'boolean') { + return this.localValue ? 'yes' : 'no' + } + return this.localValue + }, + }, + methods: { + /** + * Check if URL is percent-encoded + * @param {string} url The URL to check + * @return {boolean} + */ + isUrlEncoded(url) { + try { + return decodeURI(url) !== url + } catch { + return false + } + }, + async save() { this.reset() const url = generateUrl('/apps/theming/ajax/updateStylesheet') - // Convert boolean to string as server expects string value - const valueToPost = this.localValue === true ? 'yes' : this.localValue === false ? 'no' : this.localValue + try { await axios.post(url, { setting: this.name, - value: valueToPost, + value: this.valueToPost, }) this.$emit('update:value', this.localValue) this.handleSuccess() } catch (e) { - this.errorMessage = e.response.data.data?.message + console.error('Failed to save changes', e) + this.errorMessage = e.response?.data.data?.message } }, diff --git a/apps/theming/src/personal-settings.js b/apps/theming/src/personal-settings.js index 15190358c36..bbee88e3804 100644 --- a/apps/theming/src/personal-settings.js +++ b/apps/theming/src/personal-settings.js @@ -2,14 +2,14 @@ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { getRequestToken } from '@nextcloud/auth' +import { getCSPNonce } from '@nextcloud/auth' import Vue from 'vue' import { refreshStyles } from './helpers/refreshStyles.js' import App from './UserTheming.vue' // eslint-disable-next-line camelcase -__webpack_nonce__ = btoa(getRequestToken()) +__webpack_nonce__ = getCSPNonce() Vue.prototype.OC = OC Vue.prototype.t = t |