Compare commits
310 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8896b40164 | |||
| 7c6d99e8ea | |||
| 7170b141ba | |||
| 39b47e1a78 | |||
| 7508e5d5be | |||
| 677451427e | |||
| aab74bea40 | |||
| 4e635933c0 | |||
| f30b8df317 | |||
| b45848d91e | |||
| 31694ca314 | |||
| c4e4aff418 | |||
| 7ca983750b | |||
| 76f60d04d8 | |||
| 592739dd97 | |||
| a3263fb329 | |||
| 2887ee0518 | |||
| 5fa94ae9af | |||
| 36d3ef1c7c | |||
| 883bda7be4 | |||
| c1c502ba4a | |||
| 794bbd36f3 | |||
| a2b5ed52da | |||
| 4bb481cbb3 | |||
| 826f0aeae0 | |||
| a2d29eddf4 | |||
| 4a448f31b2 | |||
| 44233f7c23 | |||
| 98977247da | |||
| 62e5313cda | |||
| fa570bd0e4 | |||
| 2117736e34 | |||
| 9cf71f2b5a | |||
| eb785ec9b6 | |||
| 9df81090f1 | |||
| 429c512ab7 | |||
| dba67aa021 | |||
| df583faba3 | |||
| d49c7a3bdc | |||
| afeaf93bdb | |||
| ef1957d42e | |||
| f8f357aa1b | |||
| e9d1c4afdb | |||
| 487759652b | |||
| 05c9f0acb6 | |||
| f9d40847f1 | |||
| dc2fe31250 | |||
| 415fe8d8fe | |||
| 0dc2ed24f1 | |||
| 611a6c6751 | |||
| 625d6d4d0c | |||
| 6a3f2ff022 | |||
| 0fd9d8b132 | |||
| be1ef4296f | |||
| 102093b1a2 | |||
| a0930d01b1 | |||
| 5733fccdd1 | |||
| d2333d1081 | |||
| 71511ddd37 | |||
| 1d9d6c63bc | |||
| c9b20c0b23 | |||
| b772dbd416 | |||
| 5bb1d81c71 | |||
| 707fc6599f | |||
| b851446c6a | |||
| 9a5a80438a | |||
| 9429ba0e9a | |||
| 893078cebf | |||
| 0484885d38 | |||
| 271ceb0e6c | |||
| c6dea6e609 | |||
| 0da59de01d | |||
| a318699f6d | |||
| 26dac925b3 | |||
| 49ddbb3655 | |||
| 69c6229b0b | |||
| 263108c889 | |||
| a94cc32f1e | |||
| 7b001abd08 | |||
| f5d5f019fa | |||
| 4fc531bd03 | |||
| 5437573914 | |||
| cdf6e03215 | |||
| 7a96c80afb | |||
| 3c6da41c8a | |||
| 40e2b11a68 | |||
| 69eafb3bc0 | |||
| 372db926e9 | |||
| adbaff71de | |||
| 6fe73aee37 | |||
| c63f61d62e | |||
| 73e278a8af | |||
| 414a8d7de7 | |||
| b568587783 | |||
| 7a38af2d28 | |||
| f5fa046ed9 | |||
| c7a9514aa6 | |||
| 05e320a034 | |||
| a24edd5587 | |||
| 433f8011a8 | |||
| bf526631fa | |||
| 6b1901f0e7 | |||
| fda3af7915 | |||
| 53458967b3 | |||
| 08adbbd7ef | |||
| 8052cd9d08 | |||
| f5e5d47fd9 | |||
| 20ea9a2535 | |||
| 6827b6c232 | |||
| 0368c65e05 | |||
| e487d822ee | |||
| b99e478840 | |||
| d094884057 | |||
| 3b076350d1 | |||
| 98a95fb986 | |||
| 6690df39dc | |||
| f4a14cfe69 | |||
| 52a5d0cea6 | |||
| 6119604f71 | |||
| 25efb43ac4 | |||
| 446bb96ba8 | |||
| 726d857690 | |||
| 373ba3ecee | |||
| 684bd274b8 | |||
| 7f498d0fa0 | |||
| 7c4a5b0dd1 | |||
| 08227ca28d | |||
| 1309459fee | |||
| 37cca49630 | |||
| 982534349a | |||
| 61d2937bf4 | |||
| 5c7613fd67 | |||
| 10cf013e19 | |||
| df218bc4db | |||
| f17f24ffad | |||
| b3832bcd05 | |||
| a7211e6ab0 | |||
| b695d2f107 | |||
| faab6c8072 | |||
| ae6a8d5cf5 | |||
| 6c47b26102 | |||
| 2a42dd4f4e | |||
| 1fb7567294 | |||
| 1360771be8 | |||
| 7da871437b | |||
| 1dd634c0e6 | |||
| 5e2c5e5fb6 | |||
| c2510a481e | |||
| 39938297ee | |||
| 267f3896ae | |||
| be005877ff | |||
| c48b15fb70 | |||
| ec230f9290 | |||
| 1db6d96e94 | |||
| 1d02efdd8b | |||
| 8e89c281a5 | |||
| ad0405a18b | |||
| 775e4e023d | |||
| be7b7f5d13 | |||
| 8caffb784f | |||
| ce7bfc6b33 | |||
| c98334fdac | |||
| f477a22404 | |||
| cecd778c2b | |||
| b9b201cc9a | |||
| b885e44dde | |||
| 4beffc4209 | |||
| 4cbfeec38e | |||
| 66b1cf3a19 | |||
| 873ba714ae | |||
| 9068bf5788 | |||
| 0644eab947 | |||
| 116897fc90 | |||
| 061833b83d | |||
| e50f95eab6 | |||
| 8fcb1c1d76 | |||
| 263e3e829e | |||
| 1921685e4e | |||
| 402143d4e8 | |||
| 4807c23e09 | |||
| 0c6b5cde95 | |||
| ab959249d3 | |||
| c4a6f5c7b2 | |||
| 0612e008d4 | |||
| 47e950dbee | |||
| 36473a3f63 | |||
| f4fc7daf73 | |||
| 31e5e102c1 | |||
| 511ade9497 | |||
| d1dc38dc1f | |||
| fad56d683c | |||
| c765dac633 | |||
| bbd972b2ca | |||
| a4bfea7f37 | |||
| 7b841c060d | |||
| 0a2744a998 | |||
| c26e17104c | |||
| 1f0245e68f | |||
| a3febe2a40 | |||
| 7ed5679f2c | |||
| 380aaef96e | |||
| 1b4c82156d | |||
| 07ac47812b | |||
| 46180dd9f3 | |||
| d847a89436 | |||
| eafcc4e3a1 | |||
| 51c249d9ab | |||
| b1617bca39 | |||
| 14a59c461b | |||
| a2d8bd4c11 | |||
| 115058720a | |||
| 65144258d2 | |||
| 7b2f226ce6 | |||
| ac5ec1af7d | |||
| 34ca126b37 | |||
| 02d07e14b3 | |||
| 61def5e276 | |||
| ffecfc6441 | |||
| 94b86b22ce | |||
| cfd01baf04 | |||
| 036af3556c | |||
| a3b96282ed | |||
| 56abd79050 | |||
| 5267da34b1 | |||
| 446e495c59 | |||
| cae61e00f6 | |||
| 5660589ca9 | |||
| 8aed08e058 | |||
| f2d7d13579 | |||
| 4dea095a76 | |||
| 2ea723d1e1 | |||
| 019ffa2ec0 | |||
| 885b55b7af | |||
| 467a0eaee2 | |||
| 77da30a906 | |||
| ce1dddcc64 | |||
| b84eb26f72 | |||
| b6fd615bf7 | |||
| 68284b87f2 | |||
| 904ac63310 | |||
| 1f27bd153b | |||
| 92a4ce9f3b | |||
| 729b78708f | |||
| 8b34878771 | |||
| a5c5740e84 | |||
| ab51e4ef24 | |||
| d442a3c1be | |||
| 6ac50c26ef | |||
| 4aeb701b87 | |||
| 9c073fd760 | |||
| 413610ede4 | |||
| c12f26867d | |||
| 847535ee88 | |||
| d9c07af32a | |||
| e85870afa7 | |||
| 7bdaa9fa04 | |||
| 9c21bda303 | |||
| 3bb4d01a7c | |||
| 99cb56cda2 | |||
| a85d1cad6a | |||
| 6965d4b4af | |||
| 4441416245 | |||
| 96423a1172 | |||
| 2d86948c34 | |||
| dbc421580e | |||
| 89f8179e64 | |||
| bc9f3d3d4c | |||
| 9fe747f2c5 | |||
| 6f8b3916ad | |||
| 82e9af439e | |||
| c471c8d6de | |||
| 42ca267d83 | |||
| 981da3d49f | |||
| 7083596dc1 | |||
| ac2e94f121 | |||
| de295ba8dd | |||
| 5d078383b6 | |||
| e938bbec19 | |||
| 1f0f227b43 | |||
| b2fa292ee8 | |||
| cc6b12a166 | |||
| b45d342654 | |||
| e43814f74d | |||
| 611b920e3d | |||
| c25d61c7b8 | |||
| 4be6d23d00 | |||
| f2bd15229a | |||
| a703818ee7 | |||
| 5ee61a5fc6 | |||
| 019e85d92c | |||
| 1fae3ae4ae | |||
| c6c4328e2a | |||
| aff9302638 | |||
| 1115199ae3 | |||
| 86b787d21e | |||
| 359a37fd10 | |||
| 079a8c0e0f | |||
| 42bc4a0b2a | |||
| 205760a3aa | |||
| 2948697257 | |||
| 3c47caf08b | |||
| 515e05cf16 | |||
| 060230eec7 | |||
| b2a893abad | |||
| 95dcc610fc | |||
| a5a8f4e9ef | |||
| 9366ec0fb8 | |||
| bfaa31af61 | |||
| 063aac8ebc | |||
| 773826f9e1 |
@@ -1214,7 +1214,7 @@ steps:
|
||||
commands:
|
||||
# JavaScript files are not used in integration tests so it is not needed to
|
||||
# build them.
|
||||
- git clone --depth 1 https://github.com/nextcloud/spreed apps/spreed
|
||||
- git clone --depth 1 --branch stable25 https://github.com/nextcloud/spreed apps/spreed
|
||||
- name: integration-sharing-v1-video-verification
|
||||
image: ghcr.io/nextcloud/continuous-integration-integration-php7.4:latest
|
||||
commands:
|
||||
|
||||
@@ -1,46 +1,39 @@
|
||||
name: PHPUnit
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- stable*
|
||||
on: pull_request
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: phpunit-oci-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
phpunit-oci8:
|
||||
runs-on: ubuntu-20.04
|
||||
phpunit-oci:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php-versions: [ '7.4', '8.0', '8.1']
|
||||
databases: [ 'oci' ]
|
||||
|
||||
name: php${{ matrix.php-versions }}-${{ matrix.databases }}
|
||||
|
||||
services:
|
||||
oracle:
|
||||
image: deepdiver/docker-oracle-xe-11g # "wnameless/oracle-xe-11g-r2"
|
||||
image: deepdiver/docker-oracle-xe-11g # 'wnameless/oracle-xe-11g-r2'
|
||||
ports:
|
||||
- "1521:1521"
|
||||
- 1521:1521/tcp
|
||||
|
||||
steps:
|
||||
- name: Checkout server
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Checkout submodules
|
||||
shell: bash
|
||||
run: |
|
||||
auth_header="$(git config --local --get http.https://github.com/.extraheader)"
|
||||
git submodule sync --recursive
|
||||
git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
extensions: ctype,curl,dom,fileinfo,gd,imagick,intl,json,mbstring,oci8,openssl,pdo_sqlite,posix,sqlite,xml,zip
|
||||
extensions: ctype, curl, dom, fileinfo, gd, imagick, intl, json, mbstring, oci8, openssl, pdo_sqlite, posix, sqlite, xml, zip
|
||||
tools: phpunit:9
|
||||
coverage: none
|
||||
|
||||
@@ -53,3 +46,17 @@ jobs:
|
||||
- name: PHPUnit
|
||||
working-directory: tests
|
||||
run: phpunit --configuration phpunit-autotest.xml --group DB,SLOWDB
|
||||
|
||||
summary:
|
||||
permissions:
|
||||
contents: none
|
||||
runs-on: ubuntu-latest
|
||||
needs: phpunit-oci
|
||||
|
||||
if: always()
|
||||
|
||||
name: phpunit-oci-summary
|
||||
|
||||
steps:
|
||||
- name: Summary status
|
||||
run: if ${{ needs.phpunit-oci.result != 'success' }}; then exit 1; fi
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
- @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
-
|
||||
- @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
- @author Richard Steinmetz <richard@steinmetz.cloud>
|
||||
-
|
||||
- @license GNU AGPL version 3 or any later version
|
||||
-
|
||||
@@ -31,8 +32,12 @@
|
||||
@new="onNewComment" />
|
||||
|
||||
<template v-if="!isFirstLoading">
|
||||
<NcEmptyContent v-if="!hasComments && done" icon="icon-comment">
|
||||
{{ t('comments', 'No comments yet, start the conversation!') }}
|
||||
<NcEmptyContent v-if="!hasComments && done"
|
||||
class="comments__empty"
|
||||
:title="t('comments', 'No comments yet, start the conversation!')">
|
||||
<template #icon>
|
||||
<MessageReplyTextIcon />
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<!-- Comments -->
|
||||
@@ -55,14 +60,19 @@
|
||||
</div>
|
||||
|
||||
<!-- Error message -->
|
||||
<NcEmptyContent v-else-if="error" class="comments__error" icon="icon-error">
|
||||
{{ error }}
|
||||
<template #desc>
|
||||
<button icon="icon-history" @click="getComments">
|
||||
{{ t('comments', 'Retry') }}
|
||||
</button>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
<template v-else-if="error">
|
||||
<NcEmptyContent class="comments__error" :title="error">
|
||||
<template #icon>
|
||||
<AlertCircleOutlineIcon />
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
<NcButton class="comments__retry" @click="getComments">
|
||||
<template #icon>
|
||||
<RefreshIcon />
|
||||
</template>
|
||||
{{ t('comments', 'Retry') }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -76,6 +86,10 @@ import VTooltip from 'v-tooltip'
|
||||
import Vue from 'vue'
|
||||
|
||||
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent'
|
||||
import NcButton from '@nextcloud/vue/dist/Components/NcButton'
|
||||
import RefreshIcon from 'vue-material-design-icons/Refresh'
|
||||
import MessageReplyTextIcon from 'vue-material-design-icons/MessageReplyText'
|
||||
import AlertCircleOutlineIcon from 'vue-material-design-icons/AlertCircleOutline'
|
||||
|
||||
import Comment from '../components/Comment'
|
||||
import getComments, { DEFAULT_LIMIT } from '../services/GetComments'
|
||||
@@ -90,6 +104,10 @@ export default {
|
||||
// Avatar,
|
||||
Comment,
|
||||
NcEmptyContent,
|
||||
NcButton,
|
||||
RefreshIcon,
|
||||
MessageReplyTextIcon,
|
||||
AlertCircleOutlineIcon,
|
||||
},
|
||||
|
||||
data() {
|
||||
@@ -276,8 +294,13 @@ export default {
|
||||
<style lang="scss" scoped>
|
||||
.comments {
|
||||
// Do not add emptycontent top margin
|
||||
&__error{
|
||||
margin-top: 0;
|
||||
&__empty,
|
||||
&__error {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
&__retry {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
&__info {
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { generateUrl, imagePath } from '@nextcloud/router'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import axios from '@nextcloud/axios'
|
||||
@@ -99,16 +99,10 @@ import Pencil from 'vue-material-design-icons/Pencil.vue'
|
||||
import Vue from 'vue'
|
||||
|
||||
import isMobile from './mixins/isMobile.js'
|
||||
import { getBackgroundUrl } from './helpers/getBackgroundUrl.js'
|
||||
|
||||
const panels = loadState('dashboard', 'panels')
|
||||
const firstRun = loadState('dashboard', 'firstRun')
|
||||
|
||||
const background = loadState('theming', 'background')
|
||||
const backgroundVersion = loadState('theming', 'backgroundVersion')
|
||||
const themingDefaultBackground = loadState('theming', 'themingDefaultBackground')
|
||||
const shippedBackgroundList = loadState('theming', 'shippedBackgrounds')
|
||||
|
||||
const statusInfo = {
|
||||
weather: {
|
||||
text: t('dashboard', 'Weather'),
|
||||
@@ -150,24 +144,9 @@ export default {
|
||||
modal: false,
|
||||
appStoreUrl: generateUrl('/settings/apps/dashboard'),
|
||||
statuses: {},
|
||||
background,
|
||||
themingDefaultBackground,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
backgroundImage() {
|
||||
return getBackgroundUrl(this.background, backgroundVersion, this.themingDefaultBackground)
|
||||
},
|
||||
backgroundStyle() {
|
||||
if ((this.background === 'default' && this.themingDefaultBackground === 'backgroundColor')
|
||||
|| this.background.match(/#[0-9A-Fa-f]{6}/g)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
backgroundImage: this.background === 'default' ? 'var(--image-main-background)' : `url('${this.backgroundImage}')`,
|
||||
}
|
||||
},
|
||||
greeting() {
|
||||
const time = this.timer.getHours()
|
||||
|
||||
@@ -255,7 +234,6 @@ export default {
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.updateGlobalStyles()
|
||||
this.updateSkipLink()
|
||||
window.addEventListener('scroll', this.handleScroll)
|
||||
|
||||
@@ -272,32 +250,6 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateGlobalStyles() {
|
||||
// Override primary-invert-if-bright and color-primary-text if background is set
|
||||
const isBackgroundBright = shippedBackgroundList[this.background]?.theming === 'dark'
|
||||
if (isBackgroundBright) {
|
||||
document.querySelector('#header').style.setProperty('--primary-invert-if-bright', 'invert(100%)')
|
||||
document.querySelector('#header').style.setProperty('--color-primary-text', '#000000')
|
||||
// document.body.removeAttribute('data-theme-dark')
|
||||
// document.body.setAttribute('data-theme-light', 'true')
|
||||
} else {
|
||||
document.querySelector('#header').style.setProperty('--primary-invert-if-bright', 'no')
|
||||
document.querySelector('#header').style.setProperty('--color-primary-text', '#ffffff')
|
||||
// document.body.removeAttribute('data-theme-light')
|
||||
// document.body.setAttribute('data-theme-dark', 'true')
|
||||
}
|
||||
|
||||
const themeElements = [document.documentElement, document.querySelector('#header'), document.querySelector('body')]
|
||||
for (const element of themeElements) {
|
||||
if (this.background === 'default') {
|
||||
element.style.setProperty('--image-main-background', `url('${imagePath('core', 'app-background.jpg')}')`)
|
||||
} else if (this.background.match(/#[0-9A-Fa-f]{6}/g)) {
|
||||
element.style.setProperty('--image-main-background', undefined)
|
||||
} else {
|
||||
element.style.setProperty('--image-main-background', this.backgroundStyle.backgroundImage)
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Method to register panels that will be called by the integrating apps
|
||||
*
|
||||
@@ -441,7 +393,7 @@ export default {
|
||||
.panels {
|
||||
width: auto;
|
||||
margin: auto;
|
||||
max-width: 1500px;
|
||||
max-width: 1800px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
/**
|
||||
* @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @author Avior <florian.bouillon@delta-wings.net>
|
||||
* @author Julien Veyssier <eneiluj@posteo.net>
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { prefixWithBaseUrl } from './prefixWithBaseUrl.js'
|
||||
|
||||
export const getBackgroundUrl = (background, time = 0, themingDefaultBackground = '') => {
|
||||
const enabledThemes = window.OCA?.Theming?.enabledThemes || []
|
||||
const isDarkTheme = (enabledThemes.length === 0 || enabledThemes[0] === 'default')
|
||||
? window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
: enabledThemes.join('').indexOf('dark') !== -1
|
||||
|
||||
if (background === 'default') {
|
||||
if (themingDefaultBackground && themingDefaultBackground !== 'backgroundColor') {
|
||||
return generateUrl('/apps/theming/image/background') + '?v=' + window.OCA.Theming.cacheBuster
|
||||
}
|
||||
|
||||
if (isDarkTheme) {
|
||||
return prefixWithBaseUrl('eduardo-neves-pedra-azul.jpg')
|
||||
}
|
||||
|
||||
return prefixWithBaseUrl('kamil-porembinski-clouds.jpg')
|
||||
} else if (background === 'custom') {
|
||||
return generateUrl('/apps/theming/background') + '?v=' + time
|
||||
}
|
||||
|
||||
return prefixWithBaseUrl(background)
|
||||
}
|
||||
@@ -71,6 +71,7 @@ class FilesPlugin extends ServerPlugin {
|
||||
public const GETETAG_PROPERTYNAME = '{DAV:}getetag';
|
||||
public const LASTMODIFIED_PROPERTYNAME = '{DAV:}lastmodified';
|
||||
public const CREATIONDATE_PROPERTYNAME = '{DAV:}creationdate';
|
||||
public const DISPLAYNAME_PROPERTYNAME = '{DAV:}displayname';
|
||||
public const OWNER_ID_PROPERTYNAME = '{http://owncloud.org/ns}owner-id';
|
||||
public const OWNER_DISPLAY_NAME_PROPERTYNAME = '{http://owncloud.org/ns}owner-display-name';
|
||||
public const CHECKSUMS_PROPERTYNAME = '{http://owncloud.org/ns}checksums';
|
||||
@@ -379,6 +380,15 @@ class FilesPlugin extends ServerPlugin {
|
||||
$propFind->handle(self::CREATION_TIME_PROPERTYNAME, function () use ($node) {
|
||||
return $node->getFileInfo()->getCreationTime();
|
||||
});
|
||||
/**
|
||||
* Return file/folder name as displayname. The primary reason to
|
||||
* implement it this way is to avoid costly fallback to
|
||||
* CustomPropertiesBackend (esp. visible when querying all files
|
||||
* in a folder).
|
||||
*/
|
||||
$propFind->handle(self::DISPLAYNAME_PROPERTYNAME, function () use ($node) {
|
||||
return $node->getName();
|
||||
});
|
||||
}
|
||||
|
||||
if ($node instanceof \OCA\DAV\Connector\Sabre\File) {
|
||||
@@ -554,6 +564,13 @@ class FilesPlugin extends ServerPlugin {
|
||||
$node->setCreationTime((int) $time);
|
||||
return true;
|
||||
});
|
||||
/**
|
||||
* Disable modification of the displayname property for files and
|
||||
* folders via PROPPATCH. See PROPFIND for more information.
|
||||
*/
|
||||
$propPatch->handle(self::DISPLAYNAME_PROPERTYNAME, function ($displayName) {
|
||||
return 403;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -355,23 +355,19 @@ abstract class Node implements \Sabre\DAV\INode {
|
||||
return '';
|
||||
}
|
||||
|
||||
$types = [
|
||||
IShare::TYPE_USER,
|
||||
IShare::TYPE_GROUP,
|
||||
IShare::TYPE_CIRCLE,
|
||||
IShare::TYPE_ROOM
|
||||
];
|
||||
|
||||
foreach ($types as $shareType) {
|
||||
$shares = $this->shareManager->getSharedWith($user, $shareType, $this, -1);
|
||||
foreach ($shares as $share) {
|
||||
$note = $share->getNote();
|
||||
if ($share->getShareOwner() !== $user && !empty($note)) {
|
||||
return $note;
|
||||
}
|
||||
}
|
||||
// Retrieve note from the share object already loaded into
|
||||
// memory, to avoid additional database queries.
|
||||
$storage = $this->getNode()->getStorage();
|
||||
if (!$storage->instanceOfStorage(\OCA\Files_Sharing\SharedStorage::class)) {
|
||||
return '';
|
||||
}
|
||||
/** @var \OCA\Files_Sharing\SharedStorage $storage */
|
||||
|
||||
$share = $storage->getShare();
|
||||
$note = $share->getNote();
|
||||
if ($share->getShareOwner() !== $user) {
|
||||
return $note;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
@@ -1151,7 +1151,7 @@ class FileTest extends TestCase {
|
||||
|
||||
$info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
|
||||
'permissions' => \OCP\Constants::PERMISSION_ALL,
|
||||
'type' => FileInfo::TYPE_FOLDER,
|
||||
'type' => FileInfo::TYPE_FILE,
|
||||
], null);
|
||||
|
||||
$file = new \OCA\DAV\Connector\Sabre\File($view, $info);
|
||||
@@ -1172,7 +1172,7 @@ class FileTest extends TestCase {
|
||||
|
||||
$info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
|
||||
'permissions' => \OCP\Constants::PERMISSION_ALL,
|
||||
'type' => FileInfo::TYPE_FOLDER,
|
||||
'type' => FileInfo::TYPE_FILE,
|
||||
], null);
|
||||
|
||||
$file = new \OCA\DAV\Connector\Sabre\File($view, $info);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<name>Files</name>
|
||||
<summary>File Management</summary>
|
||||
<description>File Management</description>
|
||||
<version>1.20.0</version>
|
||||
<version>1.20.1</version>
|
||||
<licence>agpl</licence>
|
||||
<author>Robin Appelman</author>
|
||||
<author>Vincent Petry</author>
|
||||
@@ -26,6 +26,7 @@
|
||||
<job>OCA\Files\BackgroundJob\DeleteOrphanedItems</job>
|
||||
<job>OCA\Files\BackgroundJob\CleanupFileLocks</job>
|
||||
<job>OCA\Files\BackgroundJob\CleanupDirectEditingTokens</job>
|
||||
<job>OCA\Files\BackgroundJob\DeleteExpiredOpenLocalEditor</job>
|
||||
</background-jobs>
|
||||
|
||||
<commands>
|
||||
|
||||
@@ -37,6 +37,8 @@ declare(strict_types=1);
|
||||
*/
|
||||
namespace OCA\Files\AppInfo;
|
||||
|
||||
use OCA\Files\Controller\OpenLocalEditorController;
|
||||
|
||||
/** @var Application $application */
|
||||
$application = \OC::$server->query(Application::class);
|
||||
$application->registerRoutes(
|
||||
@@ -169,6 +171,18 @@ $application->registerRoutes(
|
||||
'url' => '/api/v1/transferownership/{id}',
|
||||
'verb' => 'DELETE',
|
||||
],
|
||||
[
|
||||
/** @see OpenLocalEditorController::create() */
|
||||
'name' => 'OpenLocalEditor#create',
|
||||
'url' => '/api/v1/openlocaleditor',
|
||||
'verb' => 'POST',
|
||||
],
|
||||
[
|
||||
/** @see OpenLocalEditorController::validate() */
|
||||
'name' => 'OpenLocalEditor#validate',
|
||||
'url' => '/api/v1/openlocaleditor/{token}',
|
||||
'verb' => 'POST',
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
@@ -20,6 +20,7 @@ return array(
|
||||
'OCA\\Files\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php',
|
||||
'OCA\\Files\\BackgroundJob\\CleanupDirectEditingTokens' => $baseDir . '/../lib/BackgroundJob/CleanupDirectEditingTokens.php',
|
||||
'OCA\\Files\\BackgroundJob\\CleanupFileLocks' => $baseDir . '/../lib/BackgroundJob/CleanupFileLocks.php',
|
||||
'OCA\\Files\\BackgroundJob\\DeleteExpiredOpenLocalEditor' => $baseDir . '/../lib/BackgroundJob/DeleteExpiredOpenLocalEditor.php',
|
||||
'OCA\\Files\\BackgroundJob\\DeleteOrphanedItems' => $baseDir . '/../lib/BackgroundJob/DeleteOrphanedItems.php',
|
||||
'OCA\\Files\\BackgroundJob\\ScanFiles' => $baseDir . '/../lib/BackgroundJob/ScanFiles.php',
|
||||
'OCA\\Files\\BackgroundJob\\TransferOwnership' => $baseDir . '/../lib/BackgroundJob/TransferOwnership.php',
|
||||
@@ -35,9 +36,12 @@ return array(
|
||||
'OCA\\Files\\Controller\\ApiController' => $baseDir . '/../lib/Controller/ApiController.php',
|
||||
'OCA\\Files\\Controller\\DirectEditingController' => $baseDir . '/../lib/Controller/DirectEditingController.php',
|
||||
'OCA\\Files\\Controller\\DirectEditingViewController' => $baseDir . '/../lib/Controller/DirectEditingViewController.php',
|
||||
'OCA\\Files\\Controller\\OpenLocalEditorController' => $baseDir . '/../lib/Controller/OpenLocalEditorController.php',
|
||||
'OCA\\Files\\Controller\\TemplateController' => $baseDir . '/../lib/Controller/TemplateController.php',
|
||||
'OCA\\Files\\Controller\\TransferOwnershipController' => $baseDir . '/../lib/Controller/TransferOwnershipController.php',
|
||||
'OCA\\Files\\Controller\\ViewController' => $baseDir . '/../lib/Controller/ViewController.php',
|
||||
'OCA\\Files\\Db\\OpenLocalEditor' => $baseDir . '/../lib/Db/OpenLocalEditor.php',
|
||||
'OCA\\Files\\Db\\OpenLocalEditorMapper' => $baseDir . '/../lib/Db/OpenLocalEditorMapper.php',
|
||||
'OCA\\Files\\Db\\TransferOwnership' => $baseDir . '/../lib/Db/TransferOwnership.php',
|
||||
'OCA\\Files\\Db\\TransferOwnershipMapper' => $baseDir . '/../lib/Db/TransferOwnershipMapper.php',
|
||||
'OCA\\Files\\DirectEditingCapabilities' => $baseDir . '/../lib/DirectEditingCapabilities.php',
|
||||
@@ -48,6 +52,7 @@ return array(
|
||||
'OCA\\Files\\Listener\\LegacyLoadAdditionalScriptsAdapter' => $baseDir . '/../lib/Listener/LegacyLoadAdditionalScriptsAdapter.php',
|
||||
'OCA\\Files\\Listener\\LoadSidebarListener' => $baseDir . '/../lib/Listener/LoadSidebarListener.php',
|
||||
'OCA\\Files\\Migration\\Version11301Date20191205150729' => $baseDir . '/../lib/Migration/Version11301Date20191205150729.php',
|
||||
'OCA\\Files\\Migration\\Version12101Date20221011153334' => $baseDir . '/../lib/Migration/Version12101Date20221011153334.php',
|
||||
'OCA\\Files\\Notification\\Notifier' => $baseDir . '/../lib/Notification/Notifier.php',
|
||||
'OCA\\Files\\Search\\FilesSearchProvider' => $baseDir . '/../lib/Search/FilesSearchProvider.php',
|
||||
'OCA\\Files\\Service\\DirectEditingService' => $baseDir . '/../lib/Service/DirectEditingService.php',
|
||||
|
||||
@@ -35,6 +35,7 @@ class ComposerStaticInitFiles
|
||||
'OCA\\Files\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php',
|
||||
'OCA\\Files\\BackgroundJob\\CleanupDirectEditingTokens' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupDirectEditingTokens.php',
|
||||
'OCA\\Files\\BackgroundJob\\CleanupFileLocks' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupFileLocks.php',
|
||||
'OCA\\Files\\BackgroundJob\\DeleteExpiredOpenLocalEditor' => __DIR__ . '/..' . '/../lib/BackgroundJob/DeleteExpiredOpenLocalEditor.php',
|
||||
'OCA\\Files\\BackgroundJob\\DeleteOrphanedItems' => __DIR__ . '/..' . '/../lib/BackgroundJob/DeleteOrphanedItems.php',
|
||||
'OCA\\Files\\BackgroundJob\\ScanFiles' => __DIR__ . '/..' . '/../lib/BackgroundJob/ScanFiles.php',
|
||||
'OCA\\Files\\BackgroundJob\\TransferOwnership' => __DIR__ . '/..' . '/../lib/BackgroundJob/TransferOwnership.php',
|
||||
@@ -50,9 +51,12 @@ class ComposerStaticInitFiles
|
||||
'OCA\\Files\\Controller\\ApiController' => __DIR__ . '/..' . '/../lib/Controller/ApiController.php',
|
||||
'OCA\\Files\\Controller\\DirectEditingController' => __DIR__ . '/..' . '/../lib/Controller/DirectEditingController.php',
|
||||
'OCA\\Files\\Controller\\DirectEditingViewController' => __DIR__ . '/..' . '/../lib/Controller/DirectEditingViewController.php',
|
||||
'OCA\\Files\\Controller\\OpenLocalEditorController' => __DIR__ . '/..' . '/../lib/Controller/OpenLocalEditorController.php',
|
||||
'OCA\\Files\\Controller\\TemplateController' => __DIR__ . '/..' . '/../lib/Controller/TemplateController.php',
|
||||
'OCA\\Files\\Controller\\TransferOwnershipController' => __DIR__ . '/..' . '/../lib/Controller/TransferOwnershipController.php',
|
||||
'OCA\\Files\\Controller\\ViewController' => __DIR__ . '/..' . '/../lib/Controller/ViewController.php',
|
||||
'OCA\\Files\\Db\\OpenLocalEditor' => __DIR__ . '/..' . '/../lib/Db/OpenLocalEditor.php',
|
||||
'OCA\\Files\\Db\\OpenLocalEditorMapper' => __DIR__ . '/..' . '/../lib/Db/OpenLocalEditorMapper.php',
|
||||
'OCA\\Files\\Db\\TransferOwnership' => __DIR__ . '/..' . '/../lib/Db/TransferOwnership.php',
|
||||
'OCA\\Files\\Db\\TransferOwnershipMapper' => __DIR__ . '/..' . '/../lib/Db/TransferOwnershipMapper.php',
|
||||
'OCA\\Files\\DirectEditingCapabilities' => __DIR__ . '/..' . '/../lib/DirectEditingCapabilities.php',
|
||||
@@ -63,6 +67,7 @@ class ComposerStaticInitFiles
|
||||
'OCA\\Files\\Listener\\LegacyLoadAdditionalScriptsAdapter' => __DIR__ . '/..' . '/../lib/Listener/LegacyLoadAdditionalScriptsAdapter.php',
|
||||
'OCA\\Files\\Listener\\LoadSidebarListener' => __DIR__ . '/..' . '/../lib/Listener/LoadSidebarListener.php',
|
||||
'OCA\\Files\\Migration\\Version11301Date20191205150729' => __DIR__ . '/..' . '/../lib/Migration/Version11301Date20191205150729.php',
|
||||
'OCA\\Files\\Migration\\Version12101Date20221011153334' => __DIR__ . '/..' . '/../lib/Migration/Version12101Date20221011153334.php',
|
||||
'OCA\\Files\\Notification\\Notifier' => __DIR__ . '/..' . '/../lib/Notification/Notifier.php',
|
||||
'OCA\\Files\\Search\\FilesSearchProvider' => __DIR__ . '/..' . '/../lib/Search/FilesSearchProvider.php',
|
||||
'OCA\\Files\\Service\\DirectEditingService' => __DIR__ . '/..' . '/../lib/Service/DirectEditingService.php',
|
||||
|
||||
@@ -609,8 +609,8 @@ table td.selection {
|
||||
.select-all + label {
|
||||
padding: 16px;
|
||||
}
|
||||
.files-fileList tr td.selection > .selectCheckBox:focus + label,
|
||||
.select-all:focus + label {
|
||||
.files-fileList tr td.selection > .selectCheckBox:focus-visible + label,
|
||||
.select-all:focus-visible + label {
|
||||
background-color: var(--color-background-hover);
|
||||
border-radius: var(--border-radius-pill);
|
||||
outline: none !important;
|
||||
@@ -973,8 +973,7 @@ table.dragshadow td.size {
|
||||
background-image: none;
|
||||
}
|
||||
.files-filestable .filename .favorite-mark .icon-starred {
|
||||
/* $dir is the app name, so we add this to the icon var to avoid conflicts between apps */
|
||||
background-image: var(--icon-star-dark-yellow);
|
||||
background-image: var(--icon-starred-yellow) !important;
|
||||
}
|
||||
|
||||
.files-filestable .filename .action .icon.hidden,
|
||||
|
||||
@@ -504,7 +504,7 @@ table td.selection {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
&:focus + label {
|
||||
&:focus-visible + label {
|
||||
background-color: var(--color-background-hover);
|
||||
border-radius: var(--border-radius-pill);
|
||||
outline: none !important;
|
||||
@@ -874,7 +874,7 @@ table.dragshadow td.size {
|
||||
background-image: none;
|
||||
}
|
||||
& .icon-starred {
|
||||
@include icon-color('star-dark', 'actions', variables.$color-yellow, 1, true);
|
||||
background-image: var(--icon-starred-yellow) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -609,8 +609,8 @@ table td.selection {
|
||||
.select-all + label {
|
||||
padding: 16px;
|
||||
}
|
||||
.files-fileList tr td.selection > .selectCheckBox:focus + label,
|
||||
.select-all:focus + label {
|
||||
.files-fileList tr td.selection > .selectCheckBox:focus-visible + label,
|
||||
.select-all:focus-visible + label {
|
||||
background-color: var(--color-background-hover);
|
||||
border-radius: var(--border-radius-pill);
|
||||
outline: none !important;
|
||||
@@ -973,8 +973,7 @@ table.dragshadow td.size {
|
||||
background-image: none;
|
||||
}
|
||||
.files-filestable .filename .favorite-mark .icon-starred {
|
||||
/* $dir is the app name, so we add this to the icon var to avoid conflicts between apps */
|
||||
background-image: var(--icon-star-dark-yellow);
|
||||
background-image: var(--icon-starred-yellow) !important;
|
||||
}
|
||||
|
||||
.files-filestable .filename .action .icon.hidden,
|
||||
|
||||
@@ -114,13 +114,13 @@
|
||||
OCA.Files.FileList.MultiSelectMenuActions.ToggleSelectionModeAction,
|
||||
{
|
||||
name: 'delete',
|
||||
displayName: t('files', 'Delete'),
|
||||
displayName: t('files', 'Delete'),
|
||||
iconClass: 'icon-delete',
|
||||
order: 99,
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
displayName: 'Tags',
|
||||
displayName: t('files', 'Tags'),
|
||||
iconClass: 'icon-tag',
|
||||
order: 100,
|
||||
},
|
||||
|
||||
@@ -710,29 +710,31 @@
|
||||
}
|
||||
});
|
||||
|
||||
this.registerAction({
|
||||
name: 'EditLocally',
|
||||
displayName: function(context) {
|
||||
var locked = context.$file.data('locked');
|
||||
if (!locked) {
|
||||
return t('files', 'Edit locally');
|
||||
}
|
||||
},
|
||||
mime: 'all',
|
||||
order: -23,
|
||||
icon: function(filename, context) {
|
||||
var locked = context.$file.data('locked');
|
||||
if (!locked) {
|
||||
return OC.imagePath('files', 'computer.svg')
|
||||
}
|
||||
},
|
||||
permissions: OC.PERMISSION_UPDATE,
|
||||
actionHandler: function (filename, context) {
|
||||
var dir = context.dir || context.fileList.getCurrentDirectory();
|
||||
var path = dir === '/' ? dir + filename : dir + '/' + filename;
|
||||
context.fileList.openLocalClient(path);
|
||||
},
|
||||
});
|
||||
if (!/Android|iPhone|iPad|iPod/i.test(navigator.userAgent)) {
|
||||
this.registerAction({
|
||||
name: 'EditLocally',
|
||||
displayName: function(context) {
|
||||
var locked = context.$file.data('locked');
|
||||
if (!locked) {
|
||||
return t('files', 'Edit locally');
|
||||
}
|
||||
},
|
||||
mime: 'all',
|
||||
order: -23,
|
||||
icon: function(filename, context) {
|
||||
var locked = context.$file.data('locked');
|
||||
if (!locked) {
|
||||
return OC.imagePath('files', 'computer.svg')
|
||||
}
|
||||
},
|
||||
permissions: OC.PERMISSION_UPDATE,
|
||||
actionHandler: function (filename, context) {
|
||||
var dir = context.dir || context.fileList.getCurrentDirectory();
|
||||
var path = dir === '/' ? dir + filename : dir + '/' + filename;
|
||||
context.fileList.openLocalClient(path);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.registerAction({
|
||||
name: 'Open',
|
||||
|
||||
@@ -2808,12 +2808,23 @@
|
||||
},
|
||||
|
||||
openLocalClient: function(path) {
|
||||
var scheme = 'nc://';
|
||||
var command = 'open';
|
||||
var uid = OC.getCurrentUser().uid;
|
||||
var url = scheme + command + '/' + uid + '@' + window.location.host + OC.encodePath(path);
|
||||
var link = OC.linkToOCS('apps/files/api/v1', 2) + 'openlocaleditor?format=json';
|
||||
|
||||
window.location.href = url;
|
||||
$.post(link, {
|
||||
path
|
||||
})
|
||||
.success(function(result) {
|
||||
var scheme = 'nc://';
|
||||
var command = 'open';
|
||||
var uid = OC.getCurrentUser().uid;
|
||||
var url = scheme + command + '/' + uid + '@' + window.location.host + OC.encodePath(path);
|
||||
url += '?token=' + result.ocs.data.token;
|
||||
|
||||
window.location.href = url;
|
||||
})
|
||||
.fail(function() {
|
||||
OC.Notification.show(t('files', 'Failed to redirect to client'))
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
|
||||
*
|
||||
* @author Joas Schilling <coding@schilljs.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Files\BackgroundJob;
|
||||
|
||||
use OCA\Files\Controller\OpenLocalEditorController;
|
||||
use OCA\Files\Db\OpenLocalEditorMapper;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\BackgroundJob\IJob;
|
||||
use OCP\BackgroundJob\TimedJob;
|
||||
|
||||
/**
|
||||
* Delete all expired "Open local editor" token
|
||||
*/
|
||||
class DeleteExpiredOpenLocalEditor extends TimedJob {
|
||||
protected OpenLocalEditorMapper $mapper;
|
||||
|
||||
public function __construct(
|
||||
ITimeFactory $time,
|
||||
OpenLocalEditorMapper $mapper
|
||||
) {
|
||||
parent::__construct($time);
|
||||
$this->mapper = $mapper;
|
||||
|
||||
// Run every 12h
|
||||
$this->interval = 12 * 3600;
|
||||
$this->setTimeSensitivity(IJob::TIME_INSENSITIVE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the background job do its work
|
||||
*
|
||||
* @param array $argument unused argument
|
||||
*/
|
||||
public function run($argument): void {
|
||||
$this->mapper->deleteExpiredTokens($this->time->getTime());
|
||||
}
|
||||
}
|
||||
@@ -37,8 +37,11 @@ use OC\Core\Command\Base;
|
||||
use OC\Core\Command\InterruptedException;
|
||||
use OC\DB\Connection;
|
||||
use OC\DB\ConnectionAdapter;
|
||||
use OCP\Files\File;
|
||||
use OC\ForbiddenException;
|
||||
use OC\Metadata\MetadataManager;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\Files\Mount\IMountPoint;
|
||||
use OCP\Files\NotFoundException;
|
||||
use OCP\Files\StorageNotAvailableException;
|
||||
@@ -51,19 +54,22 @@ use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Scan extends Base {
|
||||
private IUserManager $userManager;
|
||||
protected float $execTime = 0;
|
||||
protected int $foldersCounter = 0;
|
||||
protected int $filesCounter = 0;
|
||||
private IRootFolder $root;
|
||||
private MetadataManager $metadataManager;
|
||||
|
||||
/** @var IUserManager $userManager */
|
||||
private $userManager;
|
||||
/** @var float */
|
||||
protected $execTime = 0;
|
||||
/** @var int */
|
||||
protected $foldersCounter = 0;
|
||||
/** @var int */
|
||||
protected $filesCounter = 0;
|
||||
|
||||
public function __construct(IUserManager $userManager) {
|
||||
public function __construct(
|
||||
IUserManager $userManager,
|
||||
IRootFolder $rootFolder,
|
||||
MetadataManager $metadataManager
|
||||
) {
|
||||
$this->userManager = $userManager;
|
||||
parent::__construct();
|
||||
$this->root = $rootFolder;
|
||||
$this->metadataManager = $metadataManager;
|
||||
}
|
||||
|
||||
protected function configure() {
|
||||
@@ -83,6 +89,12 @@ class Scan extends Base {
|
||||
InputArgument::OPTIONAL,
|
||||
'limit rescan to this path, eg. --path="/alice/files/Music", the user_id is determined by the path and the user_id parameter and --all are ignored'
|
||||
)
|
||||
->addOption(
|
||||
'generate-metadata',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Generate metadata for all scanned files'
|
||||
)
|
||||
->addOption(
|
||||
'all',
|
||||
null,
|
||||
@@ -106,21 +118,26 @@ class Scan extends Base {
|
||||
);
|
||||
}
|
||||
|
||||
protected function scanFiles($user, $path, OutputInterface $output, $backgroundScan = false, $recursive = true, $homeOnly = false) {
|
||||
protected function scanFiles(string $user, string $path, bool $scanMetadata, OutputInterface $output, bool $backgroundScan = false, bool $recursive = true, bool $homeOnly = false): void {
|
||||
$connection = $this->reconnectToDatabase($output);
|
||||
$scanner = new \OC\Files\Utils\Scanner(
|
||||
$user,
|
||||
new ConnectionAdapter($connection),
|
||||
\OC::$server->query(IEventDispatcher::class),
|
||||
\OC::$server->get(IEventDispatcher::class),
|
||||
\OC::$server->get(LoggerInterface::class)
|
||||
);
|
||||
|
||||
# check on each file/folder if there was a user interrupt (ctrl-c) and throw an exception
|
||||
|
||||
$scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function ($path) use ($output) {
|
||||
$scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function (string $path) use ($output, $scanMetadata) {
|
||||
$output->writeln("\tFile\t<info>$path</info>", OutputInterface::VERBOSITY_VERBOSE);
|
||||
++$this->filesCounter;
|
||||
$this->abortIfInterrupted();
|
||||
if ($scanMetadata) {
|
||||
$node = $this->root->get($path);
|
||||
if ($node instanceof File) {
|
||||
$this->metadataManager->generateMetadata($node, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output) {
|
||||
@@ -197,7 +214,7 @@ class Scan extends Base {
|
||||
++$user_count;
|
||||
if ($this->userManager->userExists($user)) {
|
||||
$output->writeln("Starting scan for user $user_count out of $users_total ($user)");
|
||||
$this->scanFiles($user, $path, $output, $input->getOption('unscanned'), !$input->getOption('shallow'), $input->getOption('home-only'));
|
||||
$this->scanFiles($user, $path, $input->getOption('generate-metadata'), $output, $input->getOption('unscanned'), !$input->getOption('shallow'), $input->getOption('home-only'));
|
||||
$output->writeln('', OutputInterface::VERBOSITY_VERBOSE);
|
||||
} else {
|
||||
$output->writeln("<error>Unknown user $user_count $user</error>");
|
||||
@@ -291,7 +308,7 @@ class Scan extends Base {
|
||||
protected function formatExecTime() {
|
||||
$secs = round($this->execTime);
|
||||
# convert seconds into HH:MM:SS form
|
||||
return sprintf('%02d:%02d:%02d', (int)($secs / 3600), ( (int)($secs / 60) % 60), $secs % 60);
|
||||
return sprintf('%02d:%02d:%02d', (int)($secs / 3600), ((int)($secs / 60) % 60), $secs % 60);
|
||||
}
|
||||
|
||||
protected function reconnectToDatabase(OutputInterface $output): Connection {
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
|
||||
*
|
||||
* @author Joas Schilling <coding@schilljs.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Files\Controller;
|
||||
|
||||
use OCA\Files\Db\OpenLocalEditor;
|
||||
use OCA\Files\Db\OpenLocalEditorMapper;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\OCSController;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\DB\Exception;
|
||||
use OCP\IRequest;
|
||||
use OCP\Security\ISecureRandom;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class OpenLocalEditorController extends OCSController {
|
||||
public const TOKEN_LENGTH = 128;
|
||||
public const TOKEN_DURATION = 600; // 10 Minutes
|
||||
public const TOKEN_RETRIES = 50;
|
||||
|
||||
protected ITimeFactory $timeFactory;
|
||||
protected OpenLocalEditorMapper $mapper;
|
||||
protected ISecureRandom $secureRandom;
|
||||
protected LoggerInterface $logger;
|
||||
protected ?string $userId;
|
||||
|
||||
public function __construct(
|
||||
string $appName,
|
||||
IRequest $request,
|
||||
ITimeFactory $timeFactory,
|
||||
OpenLocalEditorMapper $mapper,
|
||||
ISecureRandom $secureRandom,
|
||||
LoggerInterface $logger,
|
||||
?string $userId
|
||||
) {
|
||||
parent::__construct($appName, $request);
|
||||
|
||||
$this->timeFactory = $timeFactory;
|
||||
$this->mapper = $mapper;
|
||||
$this->secureRandom = $secureRandom;
|
||||
$this->logger = $logger;
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
* @UserRateThrottle(limit=10, period=120)
|
||||
*/
|
||||
public function create(string $path): DataResponse {
|
||||
$pathHash = sha1($path);
|
||||
|
||||
$entity = new OpenLocalEditor();
|
||||
$entity->setUserId($this->userId);
|
||||
$entity->setPathHash($pathHash);
|
||||
$entity->setExpirationTime($this->timeFactory->getTime() + self::TOKEN_DURATION); // Expire in 10 minutes
|
||||
|
||||
for ($i = 1; $i <= self::TOKEN_RETRIES; $i++) {
|
||||
$token = $this->secureRandom->generate(self::TOKEN_LENGTH, ISecureRandom::CHAR_ALPHANUMERIC);
|
||||
$entity->setToken($token);
|
||||
|
||||
try {
|
||||
$this->mapper->insert($entity);
|
||||
|
||||
return new DataResponse([
|
||||
'userId' => $this->userId,
|
||||
'pathHash' => $pathHash,
|
||||
'expirationTime' => $entity->getExpirationTime(),
|
||||
'token' => $entity->getToken(),
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
if ($e->getCode() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
|
||||
// Only retry on unique constraint violation
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->logger->error('Giving up after ' . self::TOKEN_RETRIES . ' retries to generate a unique local editor token for path hash: ' . $pathHash);
|
||||
return new DataResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
* @BruteForceProtection(action=openLocalEditor)
|
||||
*/
|
||||
public function validate(string $path, string $token): DataResponse {
|
||||
$pathHash = sha1($path);
|
||||
|
||||
try {
|
||||
$entity = $this->mapper->verifyToken($this->userId, $pathHash, $token);
|
||||
} catch (DoesNotExistException $e) {
|
||||
$response = new DataResponse([], Http::STATUS_NOT_FOUND);
|
||||
$response->throttle(['userId' => $this->userId, 'pathHash' => $pathHash]);
|
||||
return $response;
|
||||
}
|
||||
|
||||
$this->mapper->delete($entity);
|
||||
|
||||
if ($entity->getExpirationTime() <= $this->timeFactory->getTime()) {
|
||||
$response = new DataResponse([], Http::STATUS_NOT_FOUND);
|
||||
$response->throttle(['userId' => $this->userId, 'pathHash' => $pathHash]);
|
||||
return $response;
|
||||
}
|
||||
|
||||
return new DataResponse([
|
||||
'userId' => $this->userId,
|
||||
'pathHash' => $pathHash,
|
||||
'expirationTime' => $entity->getExpirationTime(),
|
||||
'token' => $entity->getToken(),
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -266,7 +266,7 @@ class ViewController extends Controller {
|
||||
$nav->assign('quota', $storageInfo['quota']);
|
||||
$nav->assign('usage_relative', $storageInfo['relative']);
|
||||
|
||||
$nav->assign('webdav_url', \OCP\Util::linkToRemote('dav/files/' . $user));
|
||||
$nav->assign('webdav_url', \OCP\Util::linkToRemote('dav/files/' . rawurlencode($user)));
|
||||
|
||||
$contentItems = [];
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
|
||||
*
|
||||
* @author Joas Schilling <coding@schilljs.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Files\Db;
|
||||
|
||||
use OCP\AppFramework\Db\Entity;
|
||||
|
||||
/**
|
||||
* @method void setUserId(string $userId)
|
||||
* @method string getUserId()
|
||||
* @method void setPathHash(string $pathHash)
|
||||
* @method string getPathHash()
|
||||
* @method void setExpirationTime(int $expirationTime)
|
||||
* @method int getExpirationTime()
|
||||
* @method void setToken(string $token)
|
||||
* @method string getToken()
|
||||
*/
|
||||
class OpenLocalEditor extends Entity {
|
||||
/** @var string */
|
||||
protected $userId;
|
||||
|
||||
/** @var string */
|
||||
protected $pathHash;
|
||||
|
||||
/** @var int */
|
||||
protected $expirationTime;
|
||||
|
||||
/** @var string */
|
||||
protected $token;
|
||||
|
||||
public function __construct() {
|
||||
$this->addType('userId', 'string');
|
||||
$this->addType('pathHash', 'string');
|
||||
$this->addType('expirationTime', 'integer');
|
||||
$this->addType('token', 'string');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
|
||||
*
|
||||
* @author Joas Schilling <coding@schilljs.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Files\Db;
|
||||
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
|
||||
use OCP\AppFramework\Db\QBMapper;
|
||||
use OCP\DB\Exception;
|
||||
use OCP\IDBConnection;
|
||||
|
||||
class OpenLocalEditorMapper extends QBMapper {
|
||||
public function __construct(IDBConnection $db) {
|
||||
parent::__construct($db, 'open_local_editor', OpenLocalEditor::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws DoesNotExistException
|
||||
* @throws MultipleObjectsReturnedException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function verifyToken(string $userId, string $pathHash, string $token): OpenLocalEditor {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)))
|
||||
->andWhere($qb->expr()->eq('path_hash', $qb->createNamedParameter($pathHash)))
|
||||
->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token)));
|
||||
|
||||
return $this->findEntity($qb);
|
||||
}
|
||||
|
||||
public function deleteExpiredTokens(int $time): void {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
|
||||
$qb->delete($this->getTableName())
|
||||
->where($qb->expr()->lt('expiration_time', $qb->createNamedParameter($time)));
|
||||
|
||||
$qb->executeStatement();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
|
||||
*
|
||||
* @author Joas Schilling <coding@schilljs.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Files\Migration;
|
||||
|
||||
use Closure;
|
||||
use OCP\DB\ISchemaWrapper;
|
||||
use OCP\DB\Types;
|
||||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\SimpleMigrationStep;
|
||||
|
||||
class Version12101Date20221011153334 extends SimpleMigrationStep {
|
||||
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
|
||||
/** @var ISchemaWrapper $schema */
|
||||
$schema = $schemaClosure();
|
||||
|
||||
$table = $schema->createTable('open_local_editor');
|
||||
$table->addColumn('id',Types::BIGINT, [
|
||||
'autoincrement' => true,
|
||||
'notnull' => true,
|
||||
'length' => 20,
|
||||
'unsigned' => true,
|
||||
]);
|
||||
$table->addColumn('user_id', Types::STRING, [
|
||||
'notnull' => true,
|
||||
'length' => 64,
|
||||
]);
|
||||
$table->addColumn('path_hash', Types::STRING, [
|
||||
'notnull' => true,
|
||||
'length' => 64,
|
||||
]);
|
||||
$table->addColumn('expiration_time', Types::BIGINT, [
|
||||
'notnull' => true,
|
||||
'unsigned' => true,
|
||||
]);
|
||||
$table->addColumn('token', Types::STRING, [
|
||||
'notnull' => true,
|
||||
'length' => 128,
|
||||
]);
|
||||
|
||||
$table->setPrimaryKey(['id']);
|
||||
$table->addUniqueIndex(['user_id', 'path_hash', 'token'], 'openlocal_user_path_token');
|
||||
|
||||
return $schema;
|
||||
}
|
||||
}
|
||||
@@ -215,20 +215,23 @@ export default {
|
||||
)
|
||||
this.logger.debug('Created new file', fileInfo)
|
||||
|
||||
// Fetch FileInfo and model
|
||||
const data = await fileList?.addAndFetchFileInfo(this.name).then((status, data) => data)
|
||||
|
||||
const model = new OCA.Files.FileInfoModel(data, {
|
||||
filesClient: fileList?.filesClient,
|
||||
})
|
||||
|
||||
// Run default action
|
||||
const fileAction = OCA.Files.fileActions.getDefaultFileAction(fileInfo.mime, 'file', OC.PERMISSION_ALL)
|
||||
fileAction.action(fileInfo.basename, {
|
||||
$file: fileList?.findFileEl(this.name),
|
||||
dir: currentDirectory,
|
||||
fileList,
|
||||
fileActions: fileList?.fileActions,
|
||||
fileInfoModel: model,
|
||||
})
|
||||
if (fileAction) {
|
||||
fileAction.action(fileInfo.basename, {
|
||||
$file: fileList?.findFileEl(this.name),
|
||||
dir: currentDirectory,
|
||||
fileList,
|
||||
fileActions: fileList?.fileActions,
|
||||
fileInfoModel: model,
|
||||
})
|
||||
}
|
||||
|
||||
this.close()
|
||||
} catch (error) {
|
||||
|
||||
@@ -47,13 +47,17 @@
|
||||
}
|
||||
|
||||
#imgframe img {
|
||||
max-height: calc(100vh - var(--header-height) - 65px - 200px) !important;
|
||||
max-height: calc(100vh - var(--header-height) - 65px - 200px - 16px) !important;
|
||||
max-width: 100% !important;
|
||||
width: unset !important;
|
||||
}
|
||||
|
||||
#imgframe :not(#viewer) img {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
#imgframe video {
|
||||
max-height: calc(100vh - var(--header-height) - 65px - 200px);
|
||||
max-height: calc(100vh - var(--header-height) - 65px - 200px - 16px);
|
||||
}
|
||||
|
||||
#imgframe audio {
|
||||
@@ -94,8 +98,8 @@
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.app-files_sharing #app-content {
|
||||
max-height: calc(100vh - var(--header-height) - 65px);
|
||||
.app-files_sharing #app-content footer {
|
||||
position: sticky !important;
|
||||
}
|
||||
|
||||
/* fix multiselect bar offset on shared page */
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"sourceRoot":"","sources":["../../../core/css/variables.scss","public.scss"],"names":[],"mappings":";AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;ACKA;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EAEI;;;AAGJ;EACI;;;AAGJ;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;;;AAID;EACI;;;AAGJ;AACA;EACC;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;AACA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;AACA;EACC;;;AAED;AAAA;AAAA;EAGC;EACA;;;AAED;AAAA;AAAA;AAGC;EACA;;;AAGD;EACC;;;AAIA;EACC;;;AAIF;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;AAAA;EAEC;EACA;EACA;;;AAGD;EACC;EACA;EACA;;AACA;EACC;EACA;;;AAIF;EACC;EACA;;;AAKD;EAII;IACC;;;AAQL;EAGG;IACC","file":"public.css"}
|
||||
{"version":3,"sourceRoot":"","sources":["../../../core/css/variables.scss","public.scss"],"names":[],"mappings":";AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;ACMA;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACI;;;AAGJ;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EAEI;;;AAGJ;EACI;;;AAGJ;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;;;AAID;EACC;;;AAGD;AACA;EACC;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;AACA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;AACA;EACC;;;AAED;AAAA;AAAA;EAGC;EACA;;;AAED;AAAA;AAAA;AAGC;EACA;;;AAGD;EACC;;;AAIA;EACC;;;AAIF;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;AAAA;EAEC;EACA;EACA;;;AAGD;EACC;EACA;EACA;;AACA;EACC;EACA;;;AAIF;EACC;EACA;;;AAKD;EAII;IACC;;;AAQL;EAGG;IACC","file":"public.css"}
|
||||
@@ -1,6 +1,7 @@
|
||||
@use 'variables';
|
||||
|
||||
$footer-height: 65px;
|
||||
$footer-padding-height: 16px;
|
||||
$download-button-section-height: 200px;
|
||||
|
||||
#preview {
|
||||
@@ -30,13 +31,17 @@ $download-button-section-height: 200px;
|
||||
}
|
||||
|
||||
#imgframe img {
|
||||
max-height: calc(100vh - var(--header-height) - #{$footer-height} - #{$download-button-section-height}) !important;
|
||||
max-height: calc(100vh - var(--header-height) - #{$footer-height} - #{$download-button-section-height} - #{$footer-padding-height}) !important;
|
||||
max-width: 100% !important;
|
||||
width: unset !important;
|
||||
}
|
||||
|
||||
#imgframe :not(#viewer) img {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
#imgframe video {
|
||||
max-height: calc(100vh - var(--header-height) - #{$footer-height} - #{$download-button-section-height});
|
||||
max-height: calc(100vh - var(--header-height) - #{$footer-height} - #{$download-button-section-height} - #{$footer-padding-height});
|
||||
}
|
||||
|
||||
#imgframe audio {
|
||||
@@ -78,9 +83,9 @@ $download-button-section-height: 200px;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
// Fix footer overlapping with app-content
|
||||
.app-files_sharing #app-content {
|
||||
max-height: calc(100vh - var(--header-height) - #{$footer-height});
|
||||
|
||||
.app-files_sharing #app-content footer {
|
||||
position: sticky !important;
|
||||
}
|
||||
|
||||
/* fix multiselect bar offset on shared page */
|
||||
|
||||
@@ -47,13 +47,17 @@
|
||||
}
|
||||
|
||||
#imgframe img {
|
||||
max-height: calc(100vh - var(--header-height) - 65px - 200px) !important;
|
||||
max-height: calc(100vh - var(--header-height) - 65px - 200px - 16px) !important;
|
||||
max-width: 100% !important;
|
||||
width: unset !important;
|
||||
}
|
||||
|
||||
#imgframe :not(#viewer) img {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
#imgframe video {
|
||||
max-height: calc(100vh - var(--header-height) - 65px - 200px);
|
||||
max-height: calc(100vh - var(--header-height) - 65px - 200px - 16px);
|
||||
}
|
||||
|
||||
#imgframe audio {
|
||||
@@ -94,8 +98,8 @@
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.app-files_sharing #app-content {
|
||||
max-height: calc(100vh - var(--header-height) - 65px);
|
||||
.app-files_sharing #app-content footer {
|
||||
position: sticky !important;
|
||||
}
|
||||
|
||||
/* fix multiselect bar offset on shared page */
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"sourceRoot":"","sources":["../../../core/css/variables.scss","public.scss","mobile.scss"],"names":[],"mappings":";AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;ACKA;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EAEI;;;AAGJ;EACI;;;AAGJ;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;;;AAID;EACI;;;AAGJ;AACA;EACC;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;AACA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;AACA;EACC;;;AAED;AAAA;AAAA;EAGC;EACA;;;AAED;AAAA;AAAA;AAGC;EACA;;;AAGD;EACC;;;AAIA;EACC;;;AAIF;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;AAAA;EAEC;EACA;EACA;;;AAGD;EACC;EACA;EACA;;AACA;EACC;EACA;;;AAIF;EACC;EACA;;;AAKD;EAII;IACC;;;AAQL;EAGG;IACC;;;ADpQJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AEEA;AAEA;EACA;IACC;;;AAGD;EACA;AAAA;AAAA;AAAA;IAIC;;;AAGD;EACA;IACC;;;AAGD;EACA;IACC;IACA;;;AAED;EACA;IACC;;;AAGD;EACA;IACC;;;AAED;EACA;IACC;;;AAGD;EACA;IACC;IACA;IACA;IACA;;;EAGD;IACI;IACA;;;EAEJ;IACC;;;EAGD;IACC","file":"publicView.css"}
|
||||
{"version":3,"sourceRoot":"","sources":["../../../core/css/variables.scss","public.scss","mobile.scss"],"names":[],"mappings":";AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;ACMA;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACI;;;AAGJ;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EAEI;;;AAGJ;EACI;;;AAGJ;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;;;AAID;EACC;;;AAGD;AACA;EACC;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;AACA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;AACA;EACC;;;AAED;AAAA;AAAA;EAGC;EACA;;;AAED;AAAA;AAAA;AAGC;EACA;;;AAGD;EACC;;;AAIA;EACC;;;AAIF;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;AAAA;EAEC;EACA;EACA;;;AAGD;EACC;EACA;EACA;;AACA;EACC;EACA;;;AAIF;EACC;EACA;;;AAKD;EAII;IACC;;;AAQL;EAGG;IACC;;;ADzQJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AEEA;AAEA;EACA;IACC;;;AAGD;EACA;AAAA;AAAA;AAAA;IAIC;;;AAGD;EACA;IACC;;;AAGD;EACA;IACC;IACA;;;AAED;EACA;IACC;;;AAGD;EACA;IACC;;;AAED;EACA;IACC;;;AAGD;EACA;IACC;IACA;IACA;IACA;;;EAGD;IACI;IACA;;;EAEJ;IACC;;;EAGD;IACC","file":"publicView.css"}
|
||||
@@ -62,6 +62,11 @@ OCA.Sharing.PublicApp = {
|
||||
|
||||
// file list mode ?
|
||||
if ($el.find('.files-filestable').length) {
|
||||
// Toggle for grid view
|
||||
this.$showGridView = $('input#showgridview');
|
||||
this.$showGridView.on('change', _.bind(this._onGridviewChange, this));
|
||||
$('#view-toggle').tooltip({placement: 'bottom', trigger: 'hover'});
|
||||
|
||||
var filesClient = new OC.Files.Client({
|
||||
host: OC.getHost(),
|
||||
port: OC.getPort(),
|
||||
@@ -364,6 +369,26 @@ OCA.Sharing.PublicApp = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle showing gridview by default or not
|
||||
*
|
||||
* @returns {undefined}
|
||||
*/
|
||||
_onGridviewChange: function() {
|
||||
const isGridView = this.$showGridView.is(':checked');
|
||||
this.$showGridView.next('#view-toggle')
|
||||
.removeClass('icon-toggle-filelist icon-toggle-pictures')
|
||||
.addClass(isGridView ? 'icon-toggle-filelist' : 'icon-toggle-pictures')
|
||||
this.$showGridView.next('#view-toggle').attr(
|
||||
'data-original-title',
|
||||
isGridView ? t('files', 'Show list view') : t('files', 'Show grid view'),
|
||||
)
|
||||
|
||||
if (this.fileList) {
|
||||
this.fileList.setGridView(isGridView);
|
||||
}
|
||||
},
|
||||
|
||||
_onDirectoryChanged: function (e) {
|
||||
OC.Util.History.pushState({
|
||||
// arghhhh, why is this not called "dir" !?
|
||||
|
||||
@@ -179,11 +179,6 @@
|
||||
// storage info like free space / used space
|
||||
},
|
||||
|
||||
updateRow: function($tr, fileInfo, options) {
|
||||
// no-op, suppress re-rendering
|
||||
return $tr
|
||||
},
|
||||
|
||||
reload: function() {
|
||||
this.showMask()
|
||||
if (this._reloadCall) {
|
||||
|
||||
@@ -109,6 +109,11 @@ class PublicPreviewController extends PublicShareController {
|
||||
return new DataResponse([], Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
|
||||
$attributes = $share->getAttributes();
|
||||
if ($attributes !== null && $attributes->getAttribute('permissions', 'download') === false) {
|
||||
return new DataResponse([], Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
|
||||
try {
|
||||
$node = $share->getNode();
|
||||
if ($node instanceof Folder) {
|
||||
@@ -159,6 +164,11 @@ class PublicPreviewController extends PublicShareController {
|
||||
return new DataResponse([], Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
|
||||
$attributes = $share->getAttributes();
|
||||
if ($attributes !== null && $attributes->getAttribute('permissions', 'download') === false) {
|
||||
return new DataResponse([], Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
|
||||
try {
|
||||
$node = $share->getNode();
|
||||
if ($node instanceof Folder) {
|
||||
|
||||
@@ -533,6 +533,11 @@ class ShareAPIController extends OCSController {
|
||||
$permissions &= ~($permissions & ~$node->getPermissions());
|
||||
}
|
||||
|
||||
if ($attributes !== null) {
|
||||
$share = $this->setShareAttributes($share, $attributes);
|
||||
}
|
||||
|
||||
$share->setSharedBy($this->currentUser);
|
||||
$this->checkInheritedAttributes($share);
|
||||
|
||||
if ($shareType === IShare::TYPE_USER) {
|
||||
@@ -687,16 +692,11 @@ class ShareAPIController extends OCSController {
|
||||
}
|
||||
|
||||
$share->setShareType($shareType);
|
||||
$share->setSharedBy($this->currentUser);
|
||||
|
||||
if ($note !== '') {
|
||||
$share->setNote($note);
|
||||
}
|
||||
|
||||
if ($attributes !== null) {
|
||||
$share = $this->setShareAttributes($share, $attributes);
|
||||
}
|
||||
|
||||
try {
|
||||
$share = $this->shareManager->createShare($share);
|
||||
} catch (GenericShareException $e) {
|
||||
@@ -1112,24 +1112,10 @@ class ShareAPIController extends OCSController {
|
||||
$share->setNote($note);
|
||||
}
|
||||
|
||||
$userFolder = $this->rootFolder->getUserFolder($this->currentUser);
|
||||
|
||||
// get the node with the point of view of the current user
|
||||
$nodes = $userFolder->getById($share->getNode()->getId());
|
||||
if (count($nodes) > 0) {
|
||||
$node = $nodes[0];
|
||||
$storage = $node->getStorage();
|
||||
if ($storage && $storage->instanceOfStorage(SharedStorage::class)) {
|
||||
/** @var \OCA\Files_Sharing\SharedStorage $storage */
|
||||
$inheritedAttributes = $storage->getShare()->getAttributes();
|
||||
if ($inheritedAttributes !== null && $inheritedAttributes->getAttribute('permissions', 'download') === false) {
|
||||
if ($hideDownload === 'false') {
|
||||
throw new OCSBadRequestException($this->l->t('Cannot increase permissions'));
|
||||
}
|
||||
$share->setHideDownload(true);
|
||||
}
|
||||
}
|
||||
if ($attributes !== null) {
|
||||
$share = $this->setShareAttributes($share, $attributes);
|
||||
}
|
||||
$this->checkInheritedAttributes($share);
|
||||
|
||||
/**
|
||||
* expirationdate, password and publicUpload only make sense for link shares
|
||||
@@ -1263,10 +1249,6 @@ class ShareAPIController extends OCSController {
|
||||
}
|
||||
}
|
||||
|
||||
if ($attributes !== null) {
|
||||
$share = $this->setShareAttributes($share, $attributes);
|
||||
}
|
||||
|
||||
try {
|
||||
$share = $this->shareManager->updateShare($share);
|
||||
} catch (GenericShareException $e) {
|
||||
@@ -1555,7 +1537,7 @@ class ShareAPIController extends OCSController {
|
||||
*/
|
||||
private function parseDate(string $expireDate): \DateTime {
|
||||
try {
|
||||
$date = new \DateTime($expireDate);
|
||||
$date = new \DateTime(trim($expireDate, "\""));
|
||||
} catch (\Exception $e) {
|
||||
throw new \Exception('Invalid date. Format must be YYYY-MM-DD');
|
||||
}
|
||||
@@ -1912,8 +1894,17 @@ class ShareAPIController extends OCSController {
|
||||
}
|
||||
|
||||
private function checkInheritedAttributes(IShare $share): void {
|
||||
if ($share->getNode()->getStorage()->instanceOfStorage(SharedStorage::class)) {
|
||||
$storage = $share->getNode()->getStorage();
|
||||
if (!$share->getSharedBy()) {
|
||||
return; // Probably in a test
|
||||
}
|
||||
$userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
|
||||
$nodes = $userFolder->getById($share->getNodeId());
|
||||
if (empty($nodes)) {
|
||||
return;
|
||||
}
|
||||
$node = $nodes[0];
|
||||
if ($node->getStorage()->instanceOfStorage(SharedStorage::class)) {
|
||||
$storage = $node->getStorage();
|
||||
if ($storage instanceof Wrapper) {
|
||||
$storage = $storage->getInstanceOfStorage(SharedStorage::class);
|
||||
if ($storage === null) {
|
||||
@@ -1926,6 +1917,11 @@ class ShareAPIController extends OCSController {
|
||||
$inheritedAttributes = $storage->getShare()->getAttributes();
|
||||
if ($inheritedAttributes !== null && $inheritedAttributes->getAttribute('permissions', 'download') === false) {
|
||||
$share->setHideDownload(true);
|
||||
$attributes = $share->getAttributes();
|
||||
if ($attributes) {
|
||||
$attributes->setAttribute('permissions', 'download', false);
|
||||
$share->setAttributes($attributes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -229,6 +229,13 @@ class MountProvider implements IMountProvider {
|
||||
->setShareType($shares[0]->getShareType())
|
||||
->setTarget($shares[0]->getTarget());
|
||||
|
||||
// Gather notes from all the shares.
|
||||
// Since these are readly available here, storing them
|
||||
// enables the DAV FilesPlugin to avoid executing many
|
||||
// DB queries to retrieve the same information.
|
||||
$allNotes = implode("\n", array_map(function ($sh) { return $sh->getNote(); }, $shares));
|
||||
$superShare->setNote($allNotes);
|
||||
|
||||
// use most permissive permissions
|
||||
// this covers the case where there are multiple shares for the same
|
||||
// file e.g. from different groups and different permissions
|
||||
|
||||
@@ -95,20 +95,15 @@
|
||||
</NcActionCheckbox>
|
||||
<NcActionInput v-if="hasExpirationDate"
|
||||
ref="expireDate"
|
||||
v-tooltip.auto="{
|
||||
content: errors.expireDate,
|
||||
show: errors.expireDate,
|
||||
trigger: 'manual'
|
||||
}"
|
||||
:is-native-picker="true"
|
||||
:hide-label="true"
|
||||
:class="{ error: errors.expireDate}"
|
||||
:disabled="saving"
|
||||
:lang="lang"
|
||||
:value="share.expireDate"
|
||||
value-type="format"
|
||||
icon="icon-calendar-dark"
|
||||
type="date"
|
||||
:disabled-date="disabledDate"
|
||||
@update:value="onExpirationChange">
|
||||
:min="dateTomorrow"
|
||||
:max="dateMaxEnforced"
|
||||
@input="onExpirationChange">
|
||||
{{ t('files_sharing', 'Enter a date') }}
|
||||
</NcActionInput>
|
||||
|
||||
@@ -380,21 +375,20 @@ export default {
|
||||
},
|
||||
set(enabled) {
|
||||
this.share.expireDate = enabled
|
||||
? this.config.defaultInternalExpirationDateString !== ''
|
||||
? this.config.defaultInternalExpirationDateString
|
||||
: moment().format('YYYY-MM-DD')
|
||||
? this.config.defaultInternalExpirationDate !== ''
|
||||
? this.config.defaultInternalExpirationDate
|
||||
: new Date()
|
||||
: ''
|
||||
},
|
||||
},
|
||||
|
||||
dateMaxEnforced() {
|
||||
if (!this.isRemote) {
|
||||
return this.config.isDefaultInternalExpireDateEnforced
|
||||
&& moment().add(1 + this.config.defaultInternalExpireDate, 'days')
|
||||
} else {
|
||||
return this.config.isDefaultRemoteExpireDateEnforced
|
||||
&& moment().add(1 + this.config.defaultRemoteExpireDate, 'days')
|
||||
if (!this.isRemote && this.config.isDefaultInternalExpireDateEnforced) {
|
||||
return new Date(new Date().setDate(new Date().getDate() + 1 + this.config.defaultInternalExpireDate))
|
||||
} else if (this.config.isDefaultRemoteExpireDateEnforced) {
|
||||
return new Date(new Date().setDate(new Date().getDate() + 1 + this.config.defaultRemoteExpireDate))
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
|
||||
<template>
|
||||
<ul>
|
||||
<SharingEntrySimple class="sharing-entry__internal"
|
||||
<SharingEntrySimple ref="shareEntrySimple"
|
||||
class="sharing-entry__internal"
|
||||
:title="t('files_sharing', 'Internal link')"
|
||||
:subtitle="internalLinkSubtitle">
|
||||
<template #avatar>
|
||||
<div class="avatar-external icon-external-white" />
|
||||
</template>
|
||||
|
||||
<NcActionLink ref="copyButton"
|
||||
:href="internalLink"
|
||||
<NcActionLink :href="internalLink"
|
||||
:aria-label="t('files_sharing', 'Copy internal link to clipboard')"
|
||||
target="_blank"
|
||||
:icon="copied && copySuccess ? 'icon-checkmark-color' : 'icon-clippy'"
|
||||
@@ -84,8 +84,8 @@ export default {
|
||||
async copyLink() {
|
||||
try {
|
||||
await this.$copyText(this.internalLink)
|
||||
// focus and show the tooltip
|
||||
this.$refs.copyButton.$el.focus()
|
||||
// focus and show the tooltip (note: cannot set ref on NcActionLink)
|
||||
this.$refs.shareEntrySimple.$refs.actionsComponent.$el.focus()
|
||||
this.copySuccess = true
|
||||
this.copied = true
|
||||
} catch (error) {
|
||||
|
||||
@@ -98,20 +98,13 @@
|
||||
</NcActionText>
|
||||
<NcActionInput v-if="pendingExpirationDate"
|
||||
v-model="share.expireDate"
|
||||
v-tooltip.auto="{
|
||||
content: errors.expireDate,
|
||||
show: errors.expireDate,
|
||||
trigger: 'manual',
|
||||
defaultContainer: '#app-sidebar'
|
||||
}"
|
||||
class="share-link-expire-date"
|
||||
:disabled="saving"
|
||||
|
||||
:lang="lang"
|
||||
icon=""
|
||||
:is-native-picker="true"
|
||||
:hide-label="true"
|
||||
type="date"
|
||||
value-type="format"
|
||||
:disabled-date="disabledDate">
|
||||
:min="dateTomorrow"
|
||||
:max="dateMaxEnforced">
|
||||
<!-- let's not submit when picked, the user
|
||||
might want to still edit or copy the password -->
|
||||
{{ t('files_sharing', 'Enter a date') }}
|
||||
@@ -220,22 +213,16 @@
|
||||
</NcActionCheckbox>
|
||||
<NcActionInput v-if="hasExpirationDate"
|
||||
ref="expireDate"
|
||||
v-tooltip.auto="{
|
||||
content: errors.expireDate,
|
||||
show: errors.expireDate,
|
||||
trigger: 'manual',
|
||||
defaultContainer: '#app-sidebar'
|
||||
}"
|
||||
:is-native-picker="true"
|
||||
:hide-label="true"
|
||||
class="share-link-expire-date"
|
||||
:class="{ error: errors.expireDate}"
|
||||
:disabled="saving"
|
||||
:lang="lang"
|
||||
:value="share.expireDate"
|
||||
value-type="format"
|
||||
icon="icon-calendar-dark"
|
||||
type="date"
|
||||
:disabled-date="disabledDate"
|
||||
@update:value="onExpirationChange">
|
||||
:min="dateTomorrow"
|
||||
:max="dateMaxEnforced"
|
||||
@input="onExpirationChange">
|
||||
{{ t('files_sharing', 'Enter a date') }}
|
||||
</NcActionInput>
|
||||
|
||||
@@ -435,20 +422,22 @@ export default {
|
||||
|| !!this.share.expireDate
|
||||
},
|
||||
set(enabled) {
|
||||
let dateString = moment(this.config.defaultExpirationDateString)
|
||||
if (!dateString.isValid()) {
|
||||
dateString = moment()
|
||||
let defaultExpirationDate = this.config.defaultExpirationDate
|
||||
if (!defaultExpirationDate) {
|
||||
defaultExpirationDate = new Date()
|
||||
}
|
||||
this.share.state.expiration = enabled
|
||||
? dateString.format('YYYY-MM-DD')
|
||||
? defaultExpirationDate
|
||||
: ''
|
||||
console.debug('Expiration date status', enabled, this.share.expireDate)
|
||||
},
|
||||
},
|
||||
|
||||
dateMaxEnforced() {
|
||||
return this.config.isDefaultExpireDateEnforced
|
||||
&& moment().add(1 + this.config.defaultExpireDate, 'days')
|
||||
if (this.config.isDefaultExpireDateEnforced) {
|
||||
return new Date(new Date().setDate(new Date().getDate() + 1 + this.config.defaultExpireDate))
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -631,7 +620,7 @@ export default {
|
||||
if (this.config.isDefaultExpireDateEnforced) {
|
||||
// default is empty string if not set
|
||||
// expiration is the share object key, not expireDate
|
||||
shareDefaults.expiration = this.config.defaultExpirationDateString
|
||||
shareDefaults.expiration = this.config.defaultExpirationDate
|
||||
}
|
||||
if (this.config.enableLinkPasswordByDefault) {
|
||||
shareDefaults.password = await GeneratePassword()
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
</div>
|
||||
<NcActions v-if="$slots['default']"
|
||||
<NcActions ref="actionsComponent"
|
||||
v-if="$slots['default']"
|
||||
class="sharing-entry__actions"
|
||||
menu-align="right"
|
||||
:aria-expanded="ariaExpandedValue">
|
||||
|
||||
@@ -97,7 +97,7 @@ export default {
|
||||
},
|
||||
|
||||
dateTomorrow() {
|
||||
return moment().add(1, 'days')
|
||||
return new Date(new Date().setDate(new Date().getDate() + 1))
|
||||
},
|
||||
|
||||
// Datepicker language
|
||||
@@ -142,7 +142,7 @@ export default {
|
||||
}
|
||||
}
|
||||
if (share.expirationDate) {
|
||||
const date = moment(share.expirationDate)
|
||||
const date = share.expirationDate
|
||||
if (!date.isValid()) {
|
||||
return false
|
||||
}
|
||||
@@ -151,16 +151,12 @@ export default {
|
||||
},
|
||||
|
||||
/**
|
||||
* ActionInput can be a little tricky to work with.
|
||||
* Since we expect a string and not a Date,
|
||||
* we need to process the value here
|
||||
* Save given value to expireDate and trigger queueUpdate
|
||||
*
|
||||
* @param {Date} date js date to be parsed by moment.js
|
||||
* @param {Date} date
|
||||
*/
|
||||
onExpirationChange(date) {
|
||||
// format to YYYY-MM-DD
|
||||
const value = moment(date).format('YYYY-MM-DD')
|
||||
this.share.expireDate = value
|
||||
this.share.expireDate = date
|
||||
this.queueUpdate('expireDate')
|
||||
},
|
||||
|
||||
@@ -318,17 +314,5 @@ export default {
|
||||
debounceQueueUpdate: debounce(function(property) {
|
||||
this.queueUpdate(property)
|
||||
}, 500),
|
||||
|
||||
/**
|
||||
* Returns which dates are disabled for the datepicker
|
||||
*
|
||||
* @param {Date} date date to check
|
||||
* @return {boolean}
|
||||
*/
|
||||
disabledDate(date) {
|
||||
const dateMoment = moment(date)
|
||||
return (this.dateTomorrow && dateMoment.isBefore(this.dateTomorrow, 'day'))
|
||||
|| (this.dateMaxEnforced && dateMoment.isSameOrAfter(this.dateMaxEnforced, 'day'))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -248,9 +248,9 @@ export default class Share {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the expiration date as a string format
|
||||
* Get the expiration date
|
||||
*
|
||||
* @return {string}
|
||||
* @return {Date|null}
|
||||
* @readonly
|
||||
* @memberof Share
|
||||
*/
|
||||
@@ -259,10 +259,9 @@ export default class Share {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the expiration date as a string format
|
||||
* e.g. YYYY-MM-DD
|
||||
* Set the expiration date
|
||||
*
|
||||
* @param {string} date the share expiration date
|
||||
* @param {Date|null} date the share expiration date
|
||||
* @memberof Share
|
||||
*/
|
||||
set expireDate(date) {
|
||||
|
||||
@@ -60,57 +60,45 @@ export default class Config {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default link share expiration date as string
|
||||
* Get the default link share expiration date
|
||||
*
|
||||
* @return {string}
|
||||
* @return {Date|null}
|
||||
* @readonly
|
||||
* @memberof Config
|
||||
*/
|
||||
get defaultExpirationDateString() {
|
||||
let expireDateString = ''
|
||||
get defaultExpirationDate() {
|
||||
if (this.isDefaultExpireDateEnabled) {
|
||||
const date = window.moment.utc()
|
||||
const expireAfterDays = this.defaultExpireDate
|
||||
date.add(expireAfterDays, 'days')
|
||||
expireDateString = date.format('YYYY-MM-DD')
|
||||
return new Date(new Date().setDate(new Date().getDate() + this.defaultExpireDate))
|
||||
}
|
||||
return expireDateString
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default internal expiration date as string
|
||||
* Get the default internal expiration date
|
||||
*
|
||||
* @return {string}
|
||||
* @return {Date|null}
|
||||
* @readonly
|
||||
* @memberof Config
|
||||
*/
|
||||
get defaultInternalExpirationDateString() {
|
||||
let expireDateString = ''
|
||||
get defaultInternalExpirationDate() {
|
||||
if (this.isDefaultInternalExpireDateEnabled) {
|
||||
const date = window.moment.utc()
|
||||
const expireAfterDays = this.defaultInternalExpireDate
|
||||
date.add(expireAfterDays, 'days')
|
||||
expireDateString = date.format('YYYY-MM-DD')
|
||||
return new Date(new Date().setDate(new Date().getDate() + this.defaultInternalExpireDate))
|
||||
}
|
||||
return expireDateString
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default remote expiration date as string
|
||||
* Get the default remote expiration date
|
||||
*
|
||||
* @return {string}
|
||||
* @return {Date|null}
|
||||
* @readonly
|
||||
* @memberof Config
|
||||
*/
|
||||
get defaultRemoteExpirationDateString() {
|
||||
let expireDateString = ''
|
||||
if (this.isDefaultRemoteExpireDateEnabled) {
|
||||
const date = window.moment.utc()
|
||||
const expireAfterDays = this.defaultRemoteExpireDate
|
||||
date.add(expireAfterDays, 'days')
|
||||
expireDateString = date.format('YYYY-MM-DD')
|
||||
return new Date(new Date().setDate(new Date().getDate() + this.defaultRemoteExpireDate))
|
||||
}
|
||||
return expireDateString
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,6 +178,17 @@ export default class Config {
|
||||
return OC.appConfig.core.defaultInternalExpireDateEnabled === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Is there a default expiration date for new remote shares ?
|
||||
*
|
||||
* @return {boolean}
|
||||
* @readonly
|
||||
* @memberof Config
|
||||
*/
|
||||
get isDefaultRemoteExpireDateEnabled() {
|
||||
return OC.appConfig.core.defaultRemoteExpireDateEnabled === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Are users on this server allowed to send shares to other servers ?
|
||||
*
|
||||
|
||||
@@ -1667,20 +1667,12 @@ class ShareAPIControllerTest extends TestCase {
|
||||
$share = $this->newShare();
|
||||
$this->shareManager->method('newShare')->willReturn($share);
|
||||
|
||||
$userFolder = $this->getMockBuilder(Folder::class)->getMock();
|
||||
$this->rootFolder->expects($this->once())
|
||||
[$userFolder, $path] = $this->getNonSharedUserFile();
|
||||
$this->rootFolder->expects($this->exactly(2))
|
||||
->method('getUserFolder')
|
||||
->with('currentUser')
|
||||
->willReturn($userFolder);
|
||||
|
||||
$path = $this->getMockBuilder(File::class)->getMock();
|
||||
$storage = $this->createMock(Storage::class);
|
||||
$storage->method('instanceOfStorage')
|
||||
->willReturnMap([
|
||||
['OCA\Files_Sharing\External\Storage', false],
|
||||
['OCA\Files_Sharing\SharedStorage', false],
|
||||
]);
|
||||
$path->method('getStorage')->willReturn($storage);
|
||||
$userFolder->expects($this->once())
|
||||
->method('get')
|
||||
->with('valid-path')
|
||||
@@ -1703,20 +1695,12 @@ class ShareAPIControllerTest extends TestCase {
|
||||
$share = $this->newShare();
|
||||
$this->shareManager->method('newShare')->willReturn($share);
|
||||
|
||||
$userFolder = $this->getMockBuilder(Folder::class)->getMock();
|
||||
$this->rootFolder->expects($this->once())
|
||||
[$userFolder, $path] = $this->getNonSharedUserFile();
|
||||
$this->rootFolder->expects($this->exactly(2))
|
||||
->method('getUserFolder')
|
||||
->with('currentUser')
|
||||
->willReturn($userFolder);
|
||||
|
||||
$path = $this->getMockBuilder(File::class)->getMock();
|
||||
$storage = $this->createMock(Storage::class);
|
||||
$storage->method('instanceOfStorage')
|
||||
->willReturnMap([
|
||||
['OCA\Files_Sharing\External\Storage', false],
|
||||
['OCA\Files_Sharing\SharedStorage', false],
|
||||
]);
|
||||
$path->method('getStorage')->willReturn($storage);
|
||||
$userFolder->expects($this->once())
|
||||
->method('get')
|
||||
->with('valid-path')
|
||||
@@ -1757,20 +1741,12 @@ class ShareAPIControllerTest extends TestCase {
|
||||
])->setMethods(['formatShare'])
|
||||
->getMock();
|
||||
|
||||
$userFolder = $this->getMockBuilder(Folder::class)->getMock();
|
||||
$this->rootFolder->expects($this->once())
|
||||
[$userFolder, $path] = $this->getNonSharedUserFile();
|
||||
$this->rootFolder->expects($this->exactly(2))
|
||||
->method('getUserFolder')
|
||||
->with('currentUser')
|
||||
->willReturn($userFolder);
|
||||
|
||||
$path = $this->getMockBuilder(File::class)->getMock();
|
||||
$storage = $this->createMock(Storage::class);
|
||||
$storage->method('instanceOfStorage')
|
||||
->willReturnMap([
|
||||
['OCA\Files_Sharing\External\Storage', false],
|
||||
['OCA\Files_Sharing\SharedStorage', false],
|
||||
]);
|
||||
$path->method('getStorage')->willReturn($storage);
|
||||
$userFolder->expects($this->once())
|
||||
->method('get')
|
||||
->with('valid-path')
|
||||
@@ -1815,20 +1791,12 @@ class ShareAPIControllerTest extends TestCase {
|
||||
$this->shareManager->method('createShare')->willReturnArgument(0);
|
||||
$this->shareManager->method('allowGroupSharing')->willReturn(true);
|
||||
|
||||
$userFolder = $this->getMockBuilder(Folder::class)->getMock();
|
||||
$this->rootFolder->expects($this->once())
|
||||
[$userFolder, $path] = $this->getNonSharedUserFile();
|
||||
$this->rootFolder->expects($this->exactly(2))
|
||||
->method('getUserFolder')
|
||||
->with('currentUser')
|
||||
->willReturn($userFolder);
|
||||
|
||||
$path = $this->getMockBuilder(File::class)->getMock();
|
||||
$storage = $this->createMock(Storage::class);
|
||||
$storage->method('instanceOfStorage')
|
||||
->willReturnMap([
|
||||
['OCA\Files_Sharing\External\Storage', false],
|
||||
['OCA\Files_Sharing\SharedStorage', false],
|
||||
]);
|
||||
$path->method('getStorage')->willReturn($storage);
|
||||
$userFolder->expects($this->once())
|
||||
->method('get')
|
||||
->with('valid-path')
|
||||
@@ -1876,20 +1844,12 @@ class ShareAPIControllerTest extends TestCase {
|
||||
['shareWith', null, 'validGroup'],
|
||||
]);
|
||||
|
||||
$userFolder = $this->getMockBuilder(Folder::class)->getMock();
|
||||
$this->rootFolder->expects($this->once())
|
||||
[$userFolder, $path] = $this->getNonSharedUserFolder();
|
||||
$this->rootFolder->expects($this->exactly(2))
|
||||
->method('getUserFolder')
|
||||
->with('currentUser')
|
||||
->willReturn($userFolder);
|
||||
|
||||
$path = $this->getMockBuilder(Folder::class)->getMock();
|
||||
$storage = $this->createMock(Storage::class);
|
||||
$storage->method('instanceOfStorage')
|
||||
->willReturnMap([
|
||||
['OCA\Files_Sharing\External\Storage', false],
|
||||
['OCA\Files_Sharing\SharedStorage', false],
|
||||
]);
|
||||
$path->method('getStorage')->willReturn($storage);
|
||||
$userFolder->expects($this->once())
|
||||
->method('get')
|
||||
->with('valid-path')
|
||||
@@ -1932,20 +1892,12 @@ class ShareAPIControllerTest extends TestCase {
|
||||
$share = $this->newShare();
|
||||
$this->shareManager->method('newShare')->willReturn($share);
|
||||
|
||||
$userFolder = $this->getMockBuilder(Folder::class)->getMock();
|
||||
$this->rootFolder->expects($this->once())
|
||||
[$userFolder, $path] = $this->getNonSharedUserFolder();
|
||||
$this->rootFolder->expects($this->exactly(2))
|
||||
->method('getUserFolder')
|
||||
->with('currentUser')
|
||||
->willReturn($userFolder);
|
||||
|
||||
$path = $this->getMockBuilder(Folder::class)->getMock();
|
||||
$storage = $this->createMock(Storage::class);
|
||||
$storage->method('instanceOfStorage')
|
||||
->willReturnMap([
|
||||
['OCA\Files_Sharing\External\Storage', false],
|
||||
['OCA\Files_Sharing\SharedStorage', false],
|
||||
]);
|
||||
$path->method('getStorage')->willReturn($storage);
|
||||
$userFolder->expects($this->once())
|
||||
->method('get')
|
||||
->with('valid-path')
|
||||
@@ -2292,20 +2244,12 @@ class ShareAPIControllerTest extends TestCase {
|
||||
])->setMethods(['formatShare'])
|
||||
->getMock();
|
||||
|
||||
$userFolder = $this->getMockBuilder(Folder::class)->getMock();
|
||||
$this->rootFolder->expects($this->once())
|
||||
[$userFolder, $path] = $this->getNonSharedUserFile();
|
||||
$this->rootFolder->expects($this->exactly(2))
|
||||
->method('getUserFolder')
|
||||
->with('currentUser')
|
||||
->willReturn($userFolder);
|
||||
|
||||
$path = $this->getMockBuilder(File::class)->getMock();
|
||||
$storage = $this->createMock(Storage::class);
|
||||
$storage->method('instanceOfStorage')
|
||||
->willReturnMap([
|
||||
['OCA\Files_Sharing\External\Storage', false],
|
||||
['OCA\Files_Sharing\SharedStorage', false],
|
||||
]);
|
||||
$path->method('getStorage')->willReturn($storage);
|
||||
$userFolder->expects($this->once())
|
||||
->method('get')
|
||||
->with('valid-path')
|
||||
@@ -2366,20 +2310,12 @@ class ShareAPIControllerTest extends TestCase {
|
||||
])->setMethods(['formatShare'])
|
||||
->getMock();
|
||||
|
||||
$userFolder = $this->getMockBuilder(Folder::class)->getMock();
|
||||
$this->rootFolder->expects($this->once())
|
||||
[$userFolder, $path] = $this->getNonSharedUserFile();
|
||||
$this->rootFolder->expects($this->exactly(2))
|
||||
->method('getUserFolder')
|
||||
->with('currentUser')
|
||||
->willReturn($userFolder);
|
||||
|
||||
$path = $this->getMockBuilder(File::class)->getMock();
|
||||
$storage = $this->createMock(Storage::class);
|
||||
$storage->method('instanceOfStorage')
|
||||
->willReturnMap([
|
||||
['OCA\Files_Sharing\External\Storage', false],
|
||||
['OCA\Files_Sharing\SharedStorage', false],
|
||||
]);
|
||||
$path->method('getStorage')->willReturn($storage);
|
||||
$userFolder->expects($this->once())
|
||||
->method('get')
|
||||
->with('valid-path')
|
||||
@@ -2422,20 +2358,12 @@ class ShareAPIControllerTest extends TestCase {
|
||||
$share = $this->newShare();
|
||||
$this->shareManager->method('newShare')->willReturn($share);
|
||||
|
||||
$userFolder = $this->getMockBuilder(Folder::class)->getMock();
|
||||
$this->rootFolder->expects($this->once())
|
||||
[$userFolder, $path] = $this->getNonSharedUserFile();
|
||||
$this->rootFolder->expects($this->exactly(2))
|
||||
->method('getUserFolder')
|
||||
->with('currentUser')
|
||||
->willReturn($userFolder);
|
||||
|
||||
$path = $this->getMockBuilder(File::class)->getMock();
|
||||
$storage = $this->createMock(Storage::class);
|
||||
$storage->method('instanceOfStorage')
|
||||
->willReturnMap([
|
||||
['OCA\Files_Sharing\External\Storage', false],
|
||||
['OCA\Files_Sharing\SharedStorage', false],
|
||||
]);
|
||||
$path->method('getStorage')->willReturn($storage);
|
||||
$userFolder->expects($this->once())
|
||||
->method('get')
|
||||
->with('valid-path')
|
||||
@@ -2508,20 +2436,12 @@ class ShareAPIControllerTest extends TestCase {
|
||||
$share = $this->newShare();
|
||||
$this->shareManager->method('newShare')->willReturn($share);
|
||||
|
||||
$userFolder = $this->getMockBuilder(Folder::class)->getMock();
|
||||
$this->rootFolder->expects($this->once())
|
||||
[$userFolder, $path] = $this->getNonSharedUserFolder();
|
||||
$this->rootFolder->expects($this->exactly(2))
|
||||
->method('getUserFolder')
|
||||
->with('currentUser')
|
||||
->willReturn($userFolder);
|
||||
|
||||
$path = $this->getMockBuilder(File::class)->getMock();
|
||||
$storage = $this->createMock(Storage::class);
|
||||
$storage->method('instanceOfStorage')
|
||||
->willReturnMap([
|
||||
['OCA\Files_Sharing\External\Storage', false],
|
||||
['OCA\Files_Sharing\SharedStorage', false],
|
||||
]);
|
||||
$path->method('getStorage')->willReturn($storage);
|
||||
$path->method('getPath')->willReturn('valid-path');
|
||||
$userFolder->expects($this->once())
|
||||
->method('get')
|
||||
@@ -2551,22 +2471,15 @@ class ShareAPIControllerTest extends TestCase {
|
||||
$ocs = $this->mockFormatShare();
|
||||
|
||||
$share = $this->newShare();
|
||||
$share->setSharedBy('currentUser');
|
||||
$this->shareManager->method('newShare')->willReturn($share);
|
||||
|
||||
$userFolder = $this->getMockBuilder(Folder::class)->getMock();
|
||||
$this->rootFolder->expects($this->once())
|
||||
[$userFolder, $path] = $this->getNonSharedUserFile();
|
||||
$this->rootFolder->expects($this->exactly(2))
|
||||
->method('getUserFolder')
|
||||
->with('currentUser')
|
||||
->willReturn($userFolder);
|
||||
|
||||
$path = $this->getMockBuilder(File::class)->getMock();
|
||||
$storage = $this->createMock(Storage::class);
|
||||
$storage->method('instanceOfStorage')
|
||||
->willReturnMap([
|
||||
['OCA\Files_Sharing\External\Storage', false],
|
||||
['OCA\Files_Sharing\SharedStorage', false],
|
||||
]);
|
||||
$path->method('getStorage')->willReturn($storage);
|
||||
$userFolder->expects($this->once())
|
||||
->method('get')
|
||||
->with('valid-path')
|
||||
@@ -2637,7 +2550,7 @@ class ShareAPIControllerTest extends TestCase {
|
||||
->getMock();
|
||||
|
||||
$userFolder = $this->getMockBuilder(Folder::class)->getMock();
|
||||
$this->rootFolder->expects($this->once())
|
||||
$this->rootFolder->expects($this->exactly(2))
|
||||
->method('getUserFolder')
|
||||
->with('currentUser')
|
||||
->willReturn($userFolder);
|
||||
@@ -2649,7 +2562,9 @@ class ShareAPIControllerTest extends TestCase {
|
||||
['OCA\Files_Sharing\External\Storage', true],
|
||||
['OCA\Files_Sharing\SharedStorage', false],
|
||||
]);
|
||||
$userFolder->method('getStorage')->willReturn($storage);
|
||||
$path->method('getStorage')->willReturn($storage);
|
||||
|
||||
$path->method('getPermissions')->willReturn(\OCP\Constants::PERMISSION_READ);
|
||||
$userFolder->expects($this->once())
|
||||
->method('get')
|
||||
@@ -2676,7 +2591,7 @@ class ShareAPIControllerTest extends TestCase {
|
||||
$this->expectException(\OCP\AppFramework\OCS\OCSNotFoundException::class);
|
||||
$this->expectExceptionMessage('Wrong share ID, share does not exist');
|
||||
|
||||
$node = $this->getMockBuilder(Folder::class)->getMock();
|
||||
[$userFolder, $node] = $this->getNonSharedUserFolder();
|
||||
$share = $this->newShare();
|
||||
$share->setNode($node);
|
||||
|
||||
@@ -2686,7 +2601,6 @@ class ShareAPIControllerTest extends TestCase {
|
||||
|
||||
$this->shareManager->method('getShareById')->with('ocinternal:42')->willReturn($share);
|
||||
|
||||
$userFolder = $this->getMockBuilder('OCP\Files\Folder')->getMock();
|
||||
$this->rootFolder->method('getUserFolder')
|
||||
->with($this->currentUser)
|
||||
->willReturn($userFolder);
|
||||
@@ -2743,7 +2657,7 @@ class ShareAPIControllerTest extends TestCase {
|
||||
public function testUpdateLinkShareClear() {
|
||||
$ocs = $this->mockFormatShare();
|
||||
|
||||
$node = $this->getMockBuilder(Folder::class)->getMock();
|
||||
[$userFolder, $node] = $this->getNonSharedUserFolder();
|
||||
$node->method('getId')
|
||||
->willReturn(42);
|
||||
$share = $this->newShare();
|
||||
@@ -2780,7 +2694,6 @@ class ShareAPIControllerTest extends TestCase {
|
||||
$this->shareManager->method('getSharedWith')
|
||||
->willReturn([]);
|
||||
|
||||
$userFolder = $this->createMock(Folder::class);
|
||||
$this->rootFolder->method('getUserFolder')
|
||||
->with($this->currentUser)
|
||||
->willReturn($userFolder);
|
||||
@@ -2805,7 +2718,7 @@ class ShareAPIControllerTest extends TestCase {
|
||||
public function testUpdateLinkShareSet() {
|
||||
$ocs = $this->mockFormatShare();
|
||||
|
||||
$folder = $this->getMockBuilder(Folder::class)->getMock();
|
||||
[$userFolder, $folder] = $this->getNonSharedUserFolder();
|
||||
$folder->method('getId')
|
||||
->willReturn(42);
|
||||
|
||||
@@ -2835,7 +2748,6 @@ class ShareAPIControllerTest extends TestCase {
|
||||
$this->shareManager->method('getSharedWith')
|
||||
->willReturn([]);
|
||||
|
||||
$userFolder = $this->createMock(Folder::class);
|
||||
$this->rootFolder->method('getUserFolder')
|
||||
->with($this->currentUser)
|
||||
->willReturn($userFolder);
|
||||
@@ -2863,7 +2775,7 @@ class ShareAPIControllerTest extends TestCase {
|
||||
public function testUpdateLinkShareEnablePublicUpload($permissions, $publicUpload, $expireDate, $password) {
|
||||
$ocs = $this->mockFormatShare();
|
||||
|
||||
$folder = $this->getMockBuilder(Folder::class)->getMock();
|
||||
[$userFolder, $folder] = $this->getNonSharedUserFolder();
|
||||
$folder->method('getId')
|
||||
->willReturn(42);
|
||||
|
||||
@@ -2886,7 +2798,6 @@ class ShareAPIControllerTest extends TestCase {
|
||||
})
|
||||
)->willReturnArgument(0);
|
||||
|
||||
$userFolder = $this->createMock(Folder::class);
|
||||
$this->rootFolder->method('getUserFolder')
|
||||
->with($this->currentUser)
|
||||
->willReturn($userFolder);
|
||||
@@ -2925,7 +2836,7 @@ class ShareAPIControllerTest extends TestCase {
|
||||
public function testUpdateLinkShareSetCRUDPermissions($permissions) {
|
||||
$ocs = $this->mockFormatShare();
|
||||
|
||||
$folder = $this->getMockBuilder(Folder::class)->getMock();
|
||||
[$userFolder, $folder] = $this->getNonSharedUserFolder();
|
||||
$folder->method('getId')
|
||||
->willReturn(42);
|
||||
|
||||
@@ -2945,7 +2856,6 @@ class ShareAPIControllerTest extends TestCase {
|
||||
->method('updateShare')
|
||||
->willReturnArgument(0);
|
||||
|
||||
$userFolder = $this->createMock(Folder::class);
|
||||
$this->rootFolder->method('getUserFolder')
|
||||
->with($this->currentUser)
|
||||
->willReturn($userFolder);
|
||||
@@ -3007,15 +2917,14 @@ class ShareAPIControllerTest extends TestCase {
|
||||
$this->expectExceptionMessage('Invalid date. Format must be YYYY-MM-DD');
|
||||
|
||||
$ocs = $this->mockFormatShare();
|
||||
$userFolder = $this->createMock(Folder::class);
|
||||
[$userFolder, $folder] = $this->getNonSharedUserFolder();
|
||||
$userFolder->method('getById')
|
||||
->with(42)
|
||||
->willReturn([]);
|
||||
->willReturn([$folder]);
|
||||
$this->rootFolder->method('getUserFolder')
|
||||
->with($this->currentUser)
|
||||
->willReturn($userFolder);
|
||||
|
||||
$folder = $this->getMockBuilder(Folder::class)->getMock();
|
||||
$folder->method('getId')
|
||||
->willReturn(42);
|
||||
|
||||
@@ -3055,15 +2964,14 @@ class ShareAPIControllerTest extends TestCase {
|
||||
$this->expectExceptionMessage('Public upload disabled by the administrator');
|
||||
|
||||
$ocs = $this->mockFormatShare();
|
||||
$userFolder = $this->createMock(Folder::class);
|
||||
[$userFolder, $folder] = $this->getNonSharedUserFolder();
|
||||
$userFolder->method('getById')
|
||||
->with(42)
|
||||
->willReturn([]);
|
||||
->willReturn([$folder]);
|
||||
$this->rootFolder->method('getUserFolder')
|
||||
->with($this->currentUser)
|
||||
->willReturn($userFolder);
|
||||
|
||||
$folder = $this->getMockBuilder(Folder::class)->getMock();
|
||||
$folder->method('getId')->willReturn(42);
|
||||
|
||||
$share = \OC::$server->getShareManager()->newShare();
|
||||
@@ -3088,10 +2996,10 @@ class ShareAPIControllerTest extends TestCase {
|
||||
$file = $this->getMockBuilder(File::class)->getMock();
|
||||
$file->method('getId')
|
||||
->willReturn(42);
|
||||
$userFolder = $this->createMock(Folder::class);
|
||||
[$userFolder, $folder] = $this->getNonSharedUserFolder();
|
||||
$userFolder->method('getById')
|
||||
->with(42)
|
||||
->willReturn([]);
|
||||
->willReturn([$folder]);
|
||||
$this->rootFolder->method('getUserFolder')
|
||||
->with($this->currentUser)
|
||||
->willReturn($userFolder);
|
||||
@@ -3114,12 +3022,11 @@ class ShareAPIControllerTest extends TestCase {
|
||||
$date = new \DateTime('2000-01-01');
|
||||
$date->setTime(0,0,0);
|
||||
|
||||
$node = $this->getMockBuilder(File::class)->getMock();
|
||||
[$userFolder, $node] = $this->getNonSharedUserFolder();
|
||||
$node->method('getId')->willReturn(42);
|
||||
$userFolder = $this->createMock(Folder::class);
|
||||
$userFolder->method('getById')
|
||||
->with(42)
|
||||
->willReturn([]);
|
||||
->willReturn([$node]);
|
||||
$this->rootFolder->method('getUserFolder')
|
||||
->with($this->currentUser)
|
||||
->willReturn($userFolder);
|
||||
@@ -3167,14 +3074,13 @@ class ShareAPIControllerTest extends TestCase {
|
||||
$date = new \DateTime('2000-01-01');
|
||||
$date->setTime(0,0,0);
|
||||
|
||||
$userFolder = $this->createMock(Folder::class);
|
||||
[$userFolder, $node] = $this->getNonSharedUserFolder();
|
||||
$userFolder->method('getById')
|
||||
->with(42)
|
||||
->willReturn([]);
|
||||
->willReturn([$node]);
|
||||
$this->rootFolder->method('getUserFolder')
|
||||
->with($this->currentUser)
|
||||
->willReturn($userFolder);
|
||||
$node = $this->getMockBuilder(File::class)->getMock();
|
||||
$node->method('getId')->willReturn(42);
|
||||
$share = $this->newShare();
|
||||
$share->setPermissions(\OCP\Constants::PERMISSION_ALL)
|
||||
@@ -3226,14 +3132,13 @@ class ShareAPIControllerTest extends TestCase {
|
||||
$date = new \DateTime('2000-01-01');
|
||||
$date->setTime(0,0,0);
|
||||
|
||||
$userFolder = $this->createMock(Folder::class);
|
||||
[$userFolder, $node] = $this->getNonSharedUserFolder();
|
||||
$userFolder->method('getById')
|
||||
->with(42)
|
||||
->willReturn([]);
|
||||
->willReturn([$node]);
|
||||
$this->rootFolder->method('getUserFolder')
|
||||
->with($this->currentUser)
|
||||
->willReturn($userFolder);
|
||||
$node = $this->getMockBuilder(File::class)->getMock();
|
||||
$node->method('getId')->willReturn(42);
|
||||
$share = $this->newShare();
|
||||
$share->setPermissions(\OCP\Constants::PERMISSION_ALL)
|
||||
@@ -3267,14 +3172,13 @@ class ShareAPIControllerTest extends TestCase {
|
||||
$date = new \DateTime('2000-01-01');
|
||||
$date->setTime(0,0,0);
|
||||
|
||||
$userFolder = $this->createMock(Folder::class);
|
||||
[$userFolder, $node] = $this->getNonSharedUserFolder();
|
||||
$userFolder->method('getById')
|
||||
->with(42)
|
||||
->willReturn([]);
|
||||
->willReturn([$node]);
|
||||
$this->rootFolder->method('getUserFolder')
|
||||
->with($this->currentUser)
|
||||
->willReturn($userFolder);
|
||||
$node = $this->getMockBuilder(File::class)->getMock();
|
||||
$node->method('getId')->willReturn(42);
|
||||
$share = $this->newShare();
|
||||
$share->setPermissions(\OCP\Constants::PERMISSION_ALL)
|
||||
@@ -3322,7 +3226,7 @@ class ShareAPIControllerTest extends TestCase {
|
||||
$date = new \DateTime('2000-01-01');
|
||||
$date->setTime(0,0,0);
|
||||
|
||||
$node = $this->getMockBuilder(File::class)->getMock();
|
||||
[$userFolder, $node] = $this->getNonSharedUserFolder();
|
||||
$node->method('getId')
|
||||
->willReturn(42);
|
||||
|
||||
@@ -3359,7 +3263,6 @@ class ShareAPIControllerTest extends TestCase {
|
||||
})
|
||||
)->willReturnArgument(0);
|
||||
|
||||
$userFolder = $this->createMock(Folder::class);
|
||||
$this->rootFolder->method('getUserFolder')
|
||||
->with($this->currentUser)
|
||||
->willReturn($userFolder);
|
||||
@@ -3390,7 +3293,7 @@ class ShareAPIControllerTest extends TestCase {
|
||||
public function testUpdateLinkShareExpireDateDoesNotChangeOther() {
|
||||
$ocs = $this->mockFormatShare();
|
||||
|
||||
$node = $this->getMockBuilder(File::class)->getMock();
|
||||
[$userFolder, $node] = $this->getNonSharedUserFolder();
|
||||
$node->method('getId')
|
||||
->willReturn(42);
|
||||
|
||||
@@ -3428,7 +3331,6 @@ class ShareAPIControllerTest extends TestCase {
|
||||
})
|
||||
)->willReturnArgument(0);
|
||||
|
||||
$userFolder = $this->createMock(Folder::class);
|
||||
$this->rootFolder->method('getUserFolder')
|
||||
->with($this->currentUser)
|
||||
->willReturn($userFolder);
|
||||
@@ -3455,7 +3357,7 @@ class ShareAPIControllerTest extends TestCase {
|
||||
|
||||
$date = new \DateTime('2000-01-01');
|
||||
|
||||
$folder = $this->getMockBuilder(Folder::class)->getMock();
|
||||
[$userFolder, $folder] = $this->getNonSharedUserFolder();
|
||||
$folder->method('getId')
|
||||
->willReturn(42);
|
||||
|
||||
@@ -3490,7 +3392,6 @@ class ShareAPIControllerTest extends TestCase {
|
||||
$this->shareManager->method('getSharedWith')
|
||||
->willReturn([]);
|
||||
|
||||
$userFolder = $this->createMock(Folder::class);
|
||||
$this->rootFolder->method('getUserFolder')
|
||||
->with($this->currentUser)
|
||||
->willReturn($userFolder);
|
||||
@@ -3517,7 +3418,7 @@ class ShareAPIControllerTest extends TestCase {
|
||||
|
||||
$date = new \DateTime('2000-01-01');
|
||||
|
||||
$folder = $this->getMockBuilder(Folder::class)->getMock();
|
||||
[$userFolder, $folder] = $this->getNonSharedUserFolder();
|
||||
$folder->method('getId')
|
||||
->willReturn(42);
|
||||
|
||||
@@ -3551,7 +3452,6 @@ class ShareAPIControllerTest extends TestCase {
|
||||
|
||||
$this->shareManager->method('getSharedWith')->willReturn([]);
|
||||
|
||||
$userFolder = $this->createMock(Folder::class);
|
||||
$this->rootFolder->method('getUserFolder')
|
||||
->with($this->currentUser)
|
||||
->willReturn($userFolder);
|
||||
@@ -3578,7 +3478,7 @@ class ShareAPIControllerTest extends TestCase {
|
||||
|
||||
$date = new \DateTime('2000-01-01');
|
||||
|
||||
$folder = $this->getMockBuilder(Folder::class)->getMock();
|
||||
[$userFolder, $folder] = $this->getNonSharedUserFolder();
|
||||
$folder->method('getId')
|
||||
->willReturn(42);
|
||||
|
||||
@@ -3610,7 +3510,6 @@ class ShareAPIControllerTest extends TestCase {
|
||||
})
|
||||
)->willReturnArgument(0);
|
||||
|
||||
$userFolder = $this->createMock(Folder::class);
|
||||
$this->rootFolder->method('getUserFolder')
|
||||
->with($this->currentUser)
|
||||
->willReturn($userFolder);
|
||||
@@ -3637,7 +3536,7 @@ class ShareAPIControllerTest extends TestCase {
|
||||
public function testUpdateOtherPermissions() {
|
||||
$ocs = $this->mockFormatShare();
|
||||
|
||||
$file = $this->getMockBuilder(File::class)->getMock();
|
||||
[$userFolder, $file] = $this->getNonSharedUserFolder();
|
||||
$file->method('getId')
|
||||
->willReturn(42);
|
||||
|
||||
@@ -3658,7 +3557,7 @@ class ShareAPIControllerTest extends TestCase {
|
||||
|
||||
$this->shareManager->method('getSharedWith')->willReturn([]);
|
||||
|
||||
$userFolder = $this->createMock(Folder::class);
|
||||
[$userFolder, $folder] = $this->getNonSharedUserFolder();
|
||||
$this->rootFolder->method('getUserFolder')
|
||||
->with($this->currentUser)
|
||||
->willReturn($userFolder);
|
||||
@@ -3683,7 +3582,7 @@ class ShareAPIControllerTest extends TestCase {
|
||||
public function testUpdateShareCannotIncreasePermissions() {
|
||||
$ocs = $this->mockFormatShare();
|
||||
|
||||
$folder = $this->createMock(Folder::class);
|
||||
[$userFolder, $folder] = $this->getNonSharedUserFolder();
|
||||
$folder->method('getId')
|
||||
->willReturn(42);
|
||||
|
||||
@@ -3725,7 +3624,6 @@ class ShareAPIControllerTest extends TestCase {
|
||||
['currentUser', IShare::TYPE_ROOM, $share->getNode(), -1, 0, []]
|
||||
]);
|
||||
|
||||
$userFolder = $this->createMock(Folder::class);
|
||||
$this->rootFolder->method('getUserFolder')
|
||||
->with($this->currentUser)
|
||||
->willReturn($userFolder);
|
||||
@@ -3756,7 +3654,7 @@ class ShareAPIControllerTest extends TestCase {
|
||||
public function testUpdateShareCanIncreasePermissionsIfOwner() {
|
||||
$ocs = $this->mockFormatShare();
|
||||
|
||||
$folder = $this->createMock(Folder::class);
|
||||
[$userFolder, $folder] = $this->getNonSharedUserFolder();
|
||||
$folder->method('getId')
|
||||
->willReturn(42);
|
||||
|
||||
@@ -3796,7 +3694,6 @@ class ShareAPIControllerTest extends TestCase {
|
||||
->with($share)
|
||||
->willReturn($share);
|
||||
|
||||
$userFolder = $this->createMock(Folder::class);
|
||||
$this->rootFolder->method('getUserFolder')
|
||||
->with($this->currentUser)
|
||||
->willReturn($userFolder);
|
||||
@@ -4916,4 +4813,32 @@ class ShareAPIControllerTest extends TestCase {
|
||||
$result = $this->invokePrivate($this->ocs, 'formatShare', [$share]);
|
||||
$this->assertEquals($expects, $result);
|
||||
}
|
||||
|
||||
private function getNonSharedUserFolder(): array {
|
||||
$node = $this->getMockBuilder(Folder::class)->getMock();
|
||||
$userFolder = $this->getMockBuilder(Folder::class)->getMock();
|
||||
$storage = $this->createMock(Storage::class);
|
||||
$storage->method('instanceOfStorage')
|
||||
->willReturnMap([
|
||||
['OCA\Files_Sharing\External\Storage', false],
|
||||
['OCA\Files_Sharing\SharedStorage', false],
|
||||
]);
|
||||
$userFolder->method('getStorage')->willReturn($storage);
|
||||
$node->method('getStorage')->willReturn($storage);
|
||||
return [$userFolder, $node];
|
||||
}
|
||||
|
||||
private function getNonSharedUserFile(): array {
|
||||
$node = $this->getMockBuilder(File::class)->getMock();
|
||||
$userFolder = $this->getMockBuilder(Folder::class)->getMock();
|
||||
$storage = $this->createMock(Storage::class);
|
||||
$storage->method('instanceOfStorage')
|
||||
->willReturnMap([
|
||||
['OCA\Files_Sharing\External\Storage', false],
|
||||
['OCA\Files_Sharing\SharedStorage', false],
|
||||
]);
|
||||
$userFolder->method('getStorage')->willReturn($storage);
|
||||
$node->method('getStorage')->willReturn($storage);
|
||||
return [$userFolder, $node];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,6 +389,9 @@ class UsersController extends AUserData {
|
||||
}
|
||||
|
||||
$generatePasswordResetToken = false;
|
||||
if (strlen($password) > 469) {
|
||||
throw new OCSException('Invalid password value', 101);
|
||||
}
|
||||
if ($password === '') {
|
||||
if ($email === '') {
|
||||
throw new OCSException('To send a password link to the user an email address is required.', 108);
|
||||
@@ -882,6 +885,9 @@ class UsersController extends AUserData {
|
||||
break;
|
||||
case self::USER_FIELD_PASSWORD:
|
||||
try {
|
||||
if (strlen($value) > 469) {
|
||||
throw new OCSException('Invalid password value', 102);
|
||||
}
|
||||
if (!$targetUser->canChangePassword()) {
|
||||
throw new OCSException('Setting the password is not supported by the users backend', 103);
|
||||
}
|
||||
|
||||
@@ -379,7 +379,7 @@ class CheckSetupController extends Controller {
|
||||
return true;
|
||||
}
|
||||
|
||||
// there are two different memcached modules for PHP
|
||||
// there are two different memcache modules for PHP
|
||||
// we only support memcached and not memcache
|
||||
// https://code.google.com/p/memcached/wiki/PHPClientComparison
|
||||
return !(!extension_loaded('memcached') && extension_loaded('memcache'));
|
||||
@@ -392,7 +392,7 @@ class CheckSetupController extends Controller {
|
||||
*/
|
||||
private function isSettimelimitAvailable() {
|
||||
if (function_exists('set_time_limit')
|
||||
&& strpos(@ini_get('disable_functions'), 'set_time_limit') === false) {
|
||||
&& strpos(ini_get('disable_functions'), 'set_time_limit') === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -819,12 +819,12 @@ Raw output
|
||||
|
||||
$tempPath = sys_get_temp_dir();
|
||||
if (!is_dir($tempPath)) {
|
||||
$this->logger->error('Error while checking the temporary PHP path - it was not properly set to a directory. value: ' . $tempPath);
|
||||
$this->logger->error('Error while checking the temporary PHP path - it was not properly set to a directory. Returned value: ' . $tempPath);
|
||||
return false;
|
||||
}
|
||||
$freeSpaceInTemp = disk_free_space($tempPath);
|
||||
$freeSpaceInTemp = function_exists('disk_free_space') ? disk_free_space($tempPath) : false;
|
||||
if ($freeSpaceInTemp === false) {
|
||||
$this->logger->error('Error while checking the available disk space of temporary PHP path - no free disk space returned. temporary path: ' . $tempPath);
|
||||
$this->logger->error('Error while checking the available disk space of temporary PHP path or no free disk space returned. Temporary path: ' . $tempPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -178,12 +178,17 @@ class Hooks {
|
||||
if ($actor instanceof IUser) {
|
||||
$subject = Provider::EMAIL_CHANGED_SELF;
|
||||
if ($actor->getUID() !== $user->getUID()) {
|
||||
// set via the OCS API
|
||||
if ($this->config->getAppValue('settings', 'disable_activity.email_address_changed_by_admin', 'no') === 'yes') {
|
||||
return;
|
||||
}
|
||||
$subject = Provider::EMAIL_CHANGED;
|
||||
}
|
||||
$text = $l->t('Your email address on %s was changed.', [$instanceUrl]);
|
||||
$event->setAuthor($actor->getUID())
|
||||
->setSubject($subject);
|
||||
} else {
|
||||
// set with occ
|
||||
if ($this->config->getAppValue('settings', 'disable_activity.email_address_changed_by_admin', 'no') === 'yes') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -38,10 +38,11 @@ class SecurityTxtHandler implements IHandler {
|
||||
}
|
||||
|
||||
$response = "Contact: https://hackerone.com/nextcloud
|
||||
Expires: 2021-12-31T23:00:00.000Z
|
||||
Expires: 2023-04-31T23:00:00.000Z
|
||||
Acknowledgments: https://hackerone.com/nextcloud/thanks
|
||||
Acknowledgments: https://github.com/nextcloud/security-advisories/security/advisories
|
||||
Policy: https://hackerone.com/nextcloud";
|
||||
Policy: https://hackerone.com/nextcloud
|
||||
Preferred-Languages: en";
|
||||
|
||||
return new GenericResponse(new TextPlainResponse($response, 200));
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
type="email"
|
||||
:placeholder="inputPlaceholder"
|
||||
:value="email"
|
||||
:aria-describedby="helperText ? `${inputId}-helper-text` : ''"
|
||||
autocapitalize="none"
|
||||
autocomplete="on"
|
||||
autocorrect="off"
|
||||
@@ -71,6 +72,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="helperText"
|
||||
:id="`${inputId}-helper-text`"
|
||||
class="email__helper-text-message email__helper-text-message--error">
|
||||
<AlertCircle class="email__helper-text-message__icon" :size="18" />
|
||||
{{ helperText }}
|
||||
</p>
|
||||
|
||||
<em v-if="isNotificationEmail">
|
||||
{{ t('settings', 'Primary email for password reset and notifications') }}
|
||||
</em>
|
||||
@@ -78,9 +86,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NcActions from '@nextcloud/vue/dist/Components/NcActions'
|
||||
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton'
|
||||
import AlertOctagon from 'vue-material-design-icons/AlertOctagon'
|
||||
import { NcActions, NcActionButton } from '@nextcloud/vue'
|
||||
import AlertCircle from 'vue-material-design-icons/AlertCircleOutline.vue'
|
||||
import AlertOctagon from 'vue-material-design-icons/AlertOctagon.vue'
|
||||
import Check from 'vue-material-design-icons/Check'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import debounce from 'debounce'
|
||||
@@ -105,6 +113,7 @@ export default {
|
||||
components: {
|
||||
NcActions,
|
||||
NcActionButton,
|
||||
AlertCircle,
|
||||
AlertOctagon,
|
||||
Check,
|
||||
FederationControl,
|
||||
@@ -143,6 +152,7 @@ export default {
|
||||
initialEmail: this.email,
|
||||
localScope: this.scope,
|
||||
saveAdditionalEmailScope,
|
||||
helperText: null,
|
||||
showCheckmarkIcon: false,
|
||||
showErrorIcon: false,
|
||||
}
|
||||
@@ -218,6 +228,11 @@ export default {
|
||||
},
|
||||
|
||||
debounceEmailChange: debounce(async function(email) {
|
||||
this.helperText = null
|
||||
if (this.$refs.email?.validationMessage) {
|
||||
this.helperText = this.$refs.email.validationMessage
|
||||
return
|
||||
}
|
||||
if (validateEmail(email) || email === '') {
|
||||
if (this.primary) {
|
||||
await this.updatePrimaryEmail(email)
|
||||
@@ -393,6 +408,22 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__helper-text-message {
|
||||
padding: 4px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&__icon {
|
||||
margin-right: 8px;
|
||||
align-self: start;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
&--error {
|
||||
color: var(--color-error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
|
||||
@@ -38,10 +38,12 @@
|
||||
autocorrect="off"
|
||||
@input="onPropertyChange" />
|
||||
<input v-else
|
||||
ref="input"
|
||||
:id="inputId"
|
||||
:placeholder="placeholder"
|
||||
:type="type"
|
||||
:value="value"
|
||||
:aria-describedby="helperText ? `${name}-helper-text` : ''"
|
||||
autocapitalize="none"
|
||||
autocomplete="on"
|
||||
autocorrect="off"
|
||||
@@ -57,6 +59,13 @@
|
||||
<span v-else>
|
||||
{{ value || t('settings', 'No {property} set', { property: readable.toLocaleLowerCase() }) }}
|
||||
</span>
|
||||
|
||||
<p v-if="helperText"
|
||||
:id="`${name}-helper-text`"
|
||||
class="property__helper-text-message property__helper-text-message--error">
|
||||
<AlertCircle class="property__helper-text-message__icon" :size="18" />
|
||||
{{ helperText }}
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -64,8 +73,9 @@
|
||||
import debounce from 'debounce'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
|
||||
import Check from 'vue-material-design-icons/Check'
|
||||
import AlertCircle from 'vue-material-design-icons/AlertCircleOutline.vue'
|
||||
import AlertOctagon from 'vue-material-design-icons/AlertOctagon'
|
||||
import Check from 'vue-material-design-icons/Check'
|
||||
|
||||
import HeaderBar from '../shared/HeaderBar.vue'
|
||||
|
||||
@@ -76,6 +86,7 @@ export default {
|
||||
name: 'AccountPropertySection',
|
||||
|
||||
components: {
|
||||
AlertCircle,
|
||||
AlertOctagon,
|
||||
Check,
|
||||
HeaderBar,
|
||||
@@ -127,6 +138,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
initialValue: this.value,
|
||||
helperText: null,
|
||||
showCheckmarkIcon: false,
|
||||
showErrorIcon: false,
|
||||
}
|
||||
@@ -145,6 +157,11 @@ export default {
|
||||
},
|
||||
|
||||
debouncePropertyChange: debounce(async function(value) {
|
||||
this.helperText = null
|
||||
if (this.$refs.input && this.$refs.input.validationMessage) {
|
||||
this.helperText = this.$refs.input.validationMessage
|
||||
return
|
||||
}
|
||||
if (this.onValidate && !this.onValidate(value)) {
|
||||
return
|
||||
}
|
||||
@@ -225,6 +242,22 @@ section {
|
||||
}
|
||||
}
|
||||
|
||||
.property__helper-text-message {
|
||||
padding: 4px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&__icon {
|
||||
margin-right: 8px;
|
||||
align-self: start;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
&--error {
|
||||
color: var(--color-error);
|
||||
}
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
ref="newuserpassword"
|
||||
v-model="newUser.password"
|
||||
:minlength="minPasswordLength"
|
||||
:maxlength="469"
|
||||
:placeholder="t('settings', 'Password')"
|
||||
:required="newUser.mailAddress===''"
|
||||
autocapitalize="none"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import axios from '@nextcloud/axios'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
|
||||
export default () => {
|
||||
return axios.get(generateOcsUrl('core/navigation', 2) + '/apps?format=json')
|
||||
@@ -8,123 +9,7 @@ export default () => {
|
||||
return
|
||||
}
|
||||
|
||||
const addedApps = {}
|
||||
const navEntries = data.ocs.data
|
||||
const container = document.querySelector('#navigation #apps ul')
|
||||
|
||||
// remove disabled apps
|
||||
navEntries.forEach((entry) => {
|
||||
if (!container.querySelector('li[data-id="' + entry.id + '"]')) {
|
||||
addedApps[entry.id] = true
|
||||
}
|
||||
})
|
||||
|
||||
container.querySelectorAll('li[data-id]').forEach((el, index) => {
|
||||
const id = el.dataset.id
|
||||
// remove all apps that are not in the correct order
|
||||
if (!navEntries[index] || (navEntries[index] && navEntries[index].id !== id)) {
|
||||
el.remove()
|
||||
document.querySelector(`#appmenu li[data-id=${id}]`).remove()
|
||||
}
|
||||
})
|
||||
|
||||
let previousEntry = {}
|
||||
// add enabled apps to #navigation and #appmenu
|
||||
navEntries.forEach((entry) => {
|
||||
if (container.querySelector(`li[data-id="${entry.id}"]`) === null) {
|
||||
const li = document.createElement('li')
|
||||
li.dataset.id = entry.id
|
||||
const img = `<svg width="20" height="20" viewBox="0 0 20 20" alt="">
|
||||
<defs>
|
||||
<filter id="invertMenuMore-${entry.id}"><feColorMatrix in="SourceGraphic" type="matrix" values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0"></feColorMatrix></filter>
|
||||
<mask id="hole">
|
||||
<rect width="100%" height="100%" fill="white"></rect>
|
||||
<circle r="4.5" cx="17" cy="3" fill="black"></circle>
|
||||
</mask>
|
||||
</defs>
|
||||
<image x="0" y="0" width="16" height="16" filter="url(#invertMenuMore-${entry.id})" preserveAspectRatio="xMinYMin meet" xlink:href="${entry.icon}" class="app-icon" />
|
||||
</svg>`
|
||||
|
||||
const imgElement = document.createElement('template')
|
||||
imgElement.innerHTML = img
|
||||
|
||||
const a = document.createElement('a')
|
||||
a.setAttribute('href', entry.href)
|
||||
|
||||
const filename = document.createElement('span')
|
||||
filename.appendChild(document.createTextNode(entry.name))
|
||||
|
||||
const loading = document.createElement('div')
|
||||
loading.setAttribute('class', 'unread-counter')
|
||||
loading.style.display = 'none'
|
||||
|
||||
// draw attention to the newly added app entry
|
||||
// by flashing twice the more apps menu
|
||||
if (addedApps[entry.id]) {
|
||||
a.classList.add('animated')
|
||||
}
|
||||
|
||||
a.prepend(imgElement.content.firstChild, loading, filename)
|
||||
li.append(a)
|
||||
|
||||
// add app icon to the navigation
|
||||
const previousElement = document.querySelector(`#navigation li[data-id=${previousEntry.id}]`)
|
||||
if (previousElement) {
|
||||
previousElement.insertAdjacentElement('afterend', li)
|
||||
} else {
|
||||
document.querySelector('#navigation #apps ul').prepend(li)
|
||||
}
|
||||
}
|
||||
|
||||
if (document.getElementById('appmenu').querySelector(`li[data-id="${entry.id}"]`) === null) {
|
||||
const li = document.createElement('li')
|
||||
li.dataset.id = entry.id
|
||||
// Generating svg embedded image (see layout.user.php)
|
||||
let img
|
||||
if (OCA.Theming && OCA.Theming.inverted) {
|
||||
img = `<svg width="20" height="20" viewBox="0 0 20 20" alt="">
|
||||
<defs>
|
||||
<filter id="invert"><feColorMatrix in="SourceGraphic" type="matrix" values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0" /></filter>
|
||||
</defs>
|
||||
<image x="0" y="0" width="20" height="20" preserveAspectRatio="xMinYMin meet" filter="url(#invert)" xlink:href="${entry.icon}" class="app-icon" />
|
||||
</svg>`
|
||||
} else {
|
||||
img = `<svg width="20" height="20" viewBox="0 0 20 20" alt="">
|
||||
<image x="0" y="0" width="20" height="20" preserveAspectRatio="xMinYMin meet" xlink:href="${entry.icon}" class="app-icon" />
|
||||
</svg>`
|
||||
}
|
||||
const imgElement = document.createElement('template')
|
||||
imgElement.innerHTML = img
|
||||
|
||||
const a = document.createElement('a')
|
||||
a.setAttribute('href', entry.href)
|
||||
|
||||
const filename = document.createElement('span')
|
||||
filename.appendChild(document.createTextNode(entry.name))
|
||||
|
||||
const loading = document.createElement('div')
|
||||
loading.setAttribute('class', 'icon-loading-dark')
|
||||
loading.style.display = 'none'
|
||||
|
||||
// draw attention to the newly added app entry
|
||||
// by flashing twice the more apps menu
|
||||
if (addedApps[entry.id]) {
|
||||
a.classList.add('animated')
|
||||
}
|
||||
|
||||
a.prepend(loading, filename, imgElement.content.firstChild)
|
||||
li.append(a)
|
||||
|
||||
// add app icon to the navigation
|
||||
const previousElement = document.querySelector('#appmenu li[data-id=' + previousEntry.id + ']')
|
||||
if (previousElement) {
|
||||
previousElement.insertAdjacentElement('afterend', li)
|
||||
} else {
|
||||
document.queryElementById('appmenu').prepend(li)
|
||||
}
|
||||
}
|
||||
previousEntry = entry
|
||||
})
|
||||
emit('nextcloud:app-menu.refresh', { apps: data.ocs.data })
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<name>Theming</name>
|
||||
<summary>Adjust the Nextcloud theme</summary>
|
||||
<description>Adjust the Nextcloud theme</description>
|
||||
<version>2.0.0</version>
|
||||
<version>2.0.1</version>
|
||||
<licence>agpl</licence>
|
||||
<author>Nextcloud</author>
|
||||
<namespace>Theming</namespace>
|
||||
@@ -31,6 +31,10 @@
|
||||
<pre-migration>
|
||||
<step>OCA\Theming\Migration\MigrateUserConfig</step>
|
||||
</pre-migration>
|
||||
<post-migration>
|
||||
<step>OCA\Theming\Migration\InitBackgroundImagesMigration</step>
|
||||
<step>OCA\Theming\Migration\CleanupOldCache</step>
|
||||
</post-migration>
|
||||
</repair-steps>
|
||||
|
||||
<commands>
|
||||
|
||||
@@ -10,23 +10,10 @@
|
||||
--color-background-darker: #dbdbdb;
|
||||
--color-placeholder-light: #e6e6e6;
|
||||
--color-placeholder-dark: #cccccc;
|
||||
--color-primary: #0082c9;
|
||||
--color-primary-text: #ffffff;
|
||||
--color-primary-hover: #329bd3;
|
||||
--color-primary-light: #e5f2f9;
|
||||
--color-primary-light-text: #0082c9;
|
||||
--color-primary-light-hover: #dbe7ee;
|
||||
--color-primary-text-dark: #ededed;
|
||||
--color-primary-element: #0082c9;
|
||||
--color-primary-element-text: #ffffff;
|
||||
--color-primary-element-hover: #329bd3;
|
||||
--color-primary-element-light: #e5f2f9;
|
||||
--color-primary-element-light-text: #0082c9;
|
||||
--color-primary-element-light-hover: #dbe7ee;
|
||||
--color-primary-element-text-dark: #ededed;
|
||||
--gradient-primary-background: linear-gradient(40deg, var(--color-primary) 0%, var(--color-primary-hover) 100%);
|
||||
--color-main-text: #222222;
|
||||
--color-text-maxcontrast: #767676;
|
||||
--color-text-maxcontrast-default: #767676;
|
||||
--color-text-maxcontrast-background-blur: #646464;
|
||||
--color-text-light: #222222;
|
||||
--color-text-lighter: #767676;
|
||||
--color-scrollbar: rgba(34,34,34, .15);
|
||||
@@ -64,8 +51,26 @@
|
||||
--header-menu-item-height: 44px;
|
||||
--header-menu-profile-item-height: 66px;
|
||||
--breakpoint-mobile: 1024px;
|
||||
--primary-invert-if-bright: no;
|
||||
--background-invert-if-dark: no;
|
||||
--background-invert-if-bright: invert(100%);
|
||||
--image-main-background: url('/core/img/app-background.jpg');
|
||||
--background-image-invert-if-bright: no;
|
||||
--image-background: url('/core/img/app-background.jpg');
|
||||
--color-background-plain: #0082c9;
|
||||
--primary-invert-if-bright: no;
|
||||
--color-primary: #00639a;
|
||||
--color-primary-default: #0082c9;
|
||||
--color-primary-text: #ffffff;
|
||||
--color-primary-hover: #3282ae;
|
||||
--color-primary-light: #e5eff4;
|
||||
--color-primary-light-text: #00273d;
|
||||
--color-primary-light-hover: #dbe4e9;
|
||||
--color-primary-text-dark: #ededed;
|
||||
--color-primary-element: #00639a;
|
||||
--color-primary-element-text: #ffffff;
|
||||
--color-primary-element-hover: #3282ae;
|
||||
--color-primary-element-light: #e5eff4;
|
||||
--color-primary-element-light-text: #00273d;
|
||||
--color-primary-element-light-hover: #dbe4e9;
|
||||
--color-primary-element-text-dark: #ededed;
|
||||
--gradient-primary-background: linear-gradient(40deg, var(--color-primary) 0%, var(--color-primary-hover) 100%);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
}
|
||||
#theming form.uploadButton {
|
||||
width: 411px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
#theming form .theme-undo,
|
||||
#theming .theme-remove-bg {
|
||||
@@ -41,6 +43,10 @@
|
||||
visibility: visible;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
margin-left: auto;
|
||||
}
|
||||
#theming form .theme-undo:not([style*="display:"]) ~ .theme-remove-bg {
|
||||
margin-left: 0;
|
||||
}
|
||||
#theming input[type=text]:hover + .theme-undo,
|
||||
#theming input[type=text] + .theme-undo:hover,
|
||||
@@ -55,6 +61,8 @@
|
||||
#theming label span {
|
||||
display: inline-block;
|
||||
min-width: 175px;
|
||||
max-width: 175px;
|
||||
white-space: wrap;
|
||||
padding: 8px 0px;
|
||||
vertical-align: top;
|
||||
}
|
||||
@@ -89,7 +97,7 @@
|
||||
margin-top: 10px;
|
||||
margin-bottom: 20px;
|
||||
cursor: pointer;
|
||||
background-color: var(--color-primary);
|
||||
background-color: var(--color-primary-default);
|
||||
background-image: var(--image-background, var(--image-background-plain, url("../../../core/img/app-background.jpg"), linear-gradient(40deg, #0082c9 0%, #30b6ff 100%)));
|
||||
}
|
||||
#theming #theming-preview #theming-preview-logo {
|
||||
@@ -120,6 +128,14 @@
|
||||
#theming #theming-preview-favicon {
|
||||
background-image: var(--image-favicon);
|
||||
}
|
||||
#theming #user-theming {
|
||||
margin-top: 44px;
|
||||
display: flex;
|
||||
}
|
||||
#theming #user-theming > div {
|
||||
max-width: 400px;
|
||||
margin-bottom: 44px;
|
||||
}
|
||||
|
||||
/* transition effects for theming value changes */
|
||||
#header {
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"sourceRoot":"","sources":["settings-admin.scss"],"names":[],"mappings":"AACI;EACI;;AAGJ;AAAA;EAEI;;AAGJ;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEJ;EACI;;AAEJ;AAAA;EAEI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAQI;;AAGJ;EACI;EACA;EACA;EACA;;AAGJ;AAAA;EAEI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;EACA;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAIR;EACI;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAGP;EAEO;;AAGP;EACO;;;AAIR;AACA;EACI;;AACA;EACI","file":"settings-admin.css"}
|
||||
{"version":3,"sourceRoot":"","sources":["settings-admin.scss"],"names":[],"mappings":"AACI;EACI;;AAGJ;AAAA;EAEI;;AAGJ;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;;AAEJ;AAAA;EAEI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;;AAEJ;EAEI;;AAGJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAQI;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAGJ;AAAA;EAEI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;EACA;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAIR;EACI;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAGP;EAEO;;AAGP;EACO;;AAGJ;EACI;EACA;;AACD;EACK;EACA;;;AAKZ;AACA;EACI;;AACA;EACI","file":"settings-admin.css"}
|
||||
@@ -31,6 +31,8 @@
|
||||
}
|
||||
form.uploadButton {
|
||||
width: 411px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
form .theme-undo,
|
||||
.theme-remove-bg {
|
||||
@@ -46,7 +48,14 @@
|
||||
visibility: visible;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
// right align
|
||||
margin-left: auto;
|
||||
}
|
||||
form .theme-undo:not([style*="display:"]) ~ .theme-remove-bg {
|
||||
// Only align the undo button if both are shown
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
input[type='text']:hover + .theme-undo,
|
||||
input[type='text'] + .theme-undo:hover,
|
||||
input[type='text']:focus + .theme-undo,
|
||||
@@ -61,6 +70,8 @@
|
||||
label span {
|
||||
display: inline-block;
|
||||
min-width: 175px;
|
||||
max-width: 175px;
|
||||
white-space: wrap;
|
||||
padding: 8px 0px;
|
||||
vertical-align: top;
|
||||
}
|
||||
@@ -100,7 +111,7 @@
|
||||
margin-top: 10px;
|
||||
margin-bottom: 20px;
|
||||
cursor: pointer;
|
||||
background-color: var(--color-primary);
|
||||
background-color: var(--color-primary-default);
|
||||
background-image: var(--image-background, var(--image-background-plain, url('../../../core/img/app-background.jpg'), linear-gradient(40deg, #0082c9 0%, #30b6ff 100%)));
|
||||
|
||||
#theming-preview-logo {
|
||||
@@ -137,6 +148,15 @@
|
||||
#theming-preview-favicon {
|
||||
background-image: var(--image-favicon);
|
||||
}
|
||||
|
||||
#user-theming {
|
||||
margin-top: 44px;
|
||||
display: flex;
|
||||
& > div {
|
||||
max-width: 400px;
|
||||
margin-bottom: 44px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* transition effects for theming value changes */
|
||||
@@ -145,4 +165,4 @@
|
||||
svg, img {
|
||||
transition: 500ms filter linear;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 422 KiB After Width: | Height: | Size: 419 KiB |
|
Before Width: | Height: | Size: 369 KiB After Width: | Height: | Size: 410 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 419 KiB |
|
After Width: | Height: | Size: 443 KiB |
|
Before Width: | Height: | Size: 382 KiB After Width: | Height: | Size: 428 KiB |
@@ -173,6 +173,11 @@ window.addEventListener('DOMContentLoaded', function () {
|
||||
var el = $(this);
|
||||
});
|
||||
|
||||
$('#userThemingDisabled').change(function(e) {
|
||||
var checked = e.target.checked
|
||||
setThemingValue('disable-user-theming', checked ? 'yes' : 'no')
|
||||
});
|
||||
|
||||
function onChange(e) {
|
||||
var el = $(this);
|
||||
var setting = el.parent().find('div[data-setting]').data('setting');
|
||||
|
||||
@@ -33,7 +33,7 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class UpdateConfig extends Command {
|
||||
public const SUPPORTED_KEYS = [
|
||||
'name', 'url', 'imprintUrl', 'privacyUrl', 'slogan', 'color'
|
||||
'name', 'url', 'imprintUrl', 'privacyUrl', 'slogan', 'color', 'disable-user-theming'
|
||||
];
|
||||
|
||||
public const SUPPORTED_IMAGE_KEYS = [
|
||||
|
||||
@@ -151,6 +151,11 @@ class ThemingController extends Controller {
|
||||
$error = $this->l10n->t('The given color is invalid');
|
||||
}
|
||||
break;
|
||||
case 'disable-user-theming':
|
||||
if ($value !== "yes" && $value !== "no") {
|
||||
$error = $this->l10n->t('Disable-user-theming should be true or false');
|
||||
}
|
||||
break;
|
||||
}
|
||||
if ($error !== null) {
|
||||
return new DataResponse([
|
||||
|
||||
@@ -34,6 +34,7 @@ use OCA\Theming\AppInfo\Application;
|
||||
use OCA\Theming\ITheme;
|
||||
use OCA\Theming\Service\BackgroundService;
|
||||
use OCA\Theming\Service\ThemesService;
|
||||
use OCA\Theming\ThemingDefaults;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\Http\FileDisplayResponse;
|
||||
@@ -53,6 +54,7 @@ class UserThemeController extends OCSController {
|
||||
private IConfig $config;
|
||||
private IUserSession $userSession;
|
||||
private ThemesService $themesService;
|
||||
private ThemingDefaults $themingDefaults;
|
||||
private BackgroundService $backgroundService;
|
||||
|
||||
/**
|
||||
@@ -63,11 +65,13 @@ class UserThemeController extends OCSController {
|
||||
IConfig $config,
|
||||
IUserSession $userSession,
|
||||
ThemesService $themesService,
|
||||
ThemingDefaults $themingDefaults,
|
||||
BackgroundService $backgroundService) {
|
||||
parent::__construct($appName, $request);
|
||||
$this->config = $config;
|
||||
$this->userSession = $userSession;
|
||||
$this->themesService = $themesService;
|
||||
$this->themingDefaults = $themingDefaults;
|
||||
$this->backgroundService = $backgroundService;
|
||||
$this->userId = $userSession->getUser()->getUID();
|
||||
}
|
||||
@@ -152,7 +156,8 @@ class UserThemeController extends OCSController {
|
||||
* @NoAdminRequired
|
||||
*/
|
||||
public function setBackground(string $type = 'default', string $value = ''): JSONResponse {
|
||||
$currentVersion = (int)$this->config->getUserValue($this->userId, Application::APP_ID, 'backgroundVersion', '0');
|
||||
$currentVersion = (int)$this->config->getUserValue($this->userId, Application::APP_ID, 'userCacheBuster', '0');
|
||||
|
||||
try {
|
||||
switch ($type) {
|
||||
case 'shipped':
|
||||
@@ -175,12 +180,14 @@ class UserThemeController extends OCSController {
|
||||
} catch (\Throwable $e) {
|
||||
return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
$currentVersion++;
|
||||
$this->config->setUserValue($this->userId, Application::APP_ID, 'backgroundVersion', (string)$currentVersion);
|
||||
$this->config->setUserValue($this->userId, Application::APP_ID, 'userCacheBuster', (string)$currentVersion);
|
||||
|
||||
return new JSONResponse([
|
||||
'type' => $type,
|
||||
'value' => $value,
|
||||
'version' => $this->config->getUserValue($this->userId, Application::APP_ID, 'backgroundVersion', $currentVersion)
|
||||
'version' => $currentVersion,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ use OCP\ITempManager;
|
||||
use OCP\IURLGenerator;
|
||||
|
||||
class ImageManager {
|
||||
public const SupportedImageKeys = ['background', 'logo', 'logoheader', 'favicon'];
|
||||
|
||||
/** @var IConfig */
|
||||
private $config;
|
||||
@@ -53,7 +54,6 @@ class ImageManager {
|
||||
/** @var IURLGenerator */
|
||||
private $urlGenerator;
|
||||
/** @var array */
|
||||
private $supportedImageKeys = ['background', 'logo', 'logoheader', 'favicon'];
|
||||
/** @var ICacheFactory */
|
||||
private $cacheFactory;
|
||||
/** @var ILogger */
|
||||
@@ -66,14 +66,13 @@ class ImageManager {
|
||||
IURLGenerator $urlGenerator,
|
||||
ICacheFactory $cacheFactory,
|
||||
ILogger $logger,
|
||||
ITempManager $tempManager
|
||||
) {
|
||||
ITempManager $tempManager) {
|
||||
$this->config = $config;
|
||||
$this->appData = $appData;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->cacheFactory = $cacheFactory;
|
||||
$this->logger = $logger;
|
||||
$this->tempManager = $tempManager;
|
||||
$this->appData = $appData;
|
||||
}
|
||||
|
||||
public function getImageUrl(string $key, bool $useSvg = true): string {
|
||||
@@ -106,10 +105,12 @@ class ImageManager {
|
||||
*/
|
||||
public function getImage(string $key, bool $useSvg = true): ISimpleFile {
|
||||
$logo = $this->config->getAppValue('theming', $key . 'Mime', '');
|
||||
$folder = $this->appData->getFolder('images');
|
||||
$folder = $this->getRootFolder()->getFolder('images');
|
||||
|
||||
if ($logo === '' || !$folder->fileExists($key)) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (!$useSvg && $this->shouldReplaceIcons()) {
|
||||
if (!$folder->fileExists($key . '.png')) {
|
||||
try {
|
||||
@@ -127,6 +128,7 @@ class ImageManager {
|
||||
return $folder->getFile($key . '.png');
|
||||
}
|
||||
}
|
||||
|
||||
return $folder->getFile($key);
|
||||
}
|
||||
|
||||
@@ -140,7 +142,7 @@ class ImageManager {
|
||||
*/
|
||||
public function getCustomImages(): array {
|
||||
$images = [];
|
||||
foreach ($this->supportedImageKeys as $key) {
|
||||
foreach ($this::SupportedImageKeys as $key) {
|
||||
$images[$key] = [
|
||||
'mime' => $this->config->getAppValue('theming', $key . 'Mime', ''),
|
||||
'url' => $this->getImageUrl($key),
|
||||
@@ -158,9 +160,9 @@ class ImageManager {
|
||||
public function getCacheFolder(): ISimpleFolder {
|
||||
$cacheBusterValue = $this->config->getAppValue('theming', 'cachebuster', '0');
|
||||
try {
|
||||
$folder = $this->appData->getFolder($cacheBusterValue);
|
||||
$folder = $this->getRootFolder()->getFolder($cacheBusterValue);
|
||||
} catch (NotFoundException $e) {
|
||||
$folder = $this->appData->newFolder($cacheBusterValue);
|
||||
$folder = $this->getRootFolder()->newFolder($cacheBusterValue);
|
||||
$this->cleanup();
|
||||
}
|
||||
return $folder;
|
||||
@@ -202,13 +204,13 @@ class ImageManager {
|
||||
public function delete(string $key): void {
|
||||
/* ignore exceptions, since we don't want to fail hard if something goes wrong during cleanup */
|
||||
try {
|
||||
$file = $this->appData->getFolder('images')->getFile($key);
|
||||
$file = $this->getRootFolder()->getFolder('images')->getFile($key);
|
||||
$file->delete();
|
||||
} catch (NotFoundException $e) {
|
||||
} catch (NotPermittedException $e) {
|
||||
}
|
||||
try {
|
||||
$file = $this->appData->getFolder('images')->getFile($key . '.png');
|
||||
$file = $this->getRootFolder()->getFolder('images')->getFile($key . '.png');
|
||||
$file->delete();
|
||||
} catch (NotFoundException $e) {
|
||||
} catch (NotPermittedException $e) {
|
||||
@@ -219,9 +221,9 @@ class ImageManager {
|
||||
$this->delete($key);
|
||||
|
||||
try {
|
||||
$folder = $this->appData->getFolder('images');
|
||||
$folder = $this->getRootFolder()->getFolder('images');
|
||||
} catch (NotFoundException $e) {
|
||||
$folder = $this->appData->newFolder('images');
|
||||
$folder = $this->getRootFolder()->newFolder('images');
|
||||
}
|
||||
|
||||
$target = $folder->newFile($key);
|
||||
@@ -288,7 +290,7 @@ class ImageManager {
|
||||
*/
|
||||
public function cleanup() {
|
||||
$currentFolder = $this->getCacheFolder();
|
||||
$folders = $this->appData->getDirectoryListing();
|
||||
$folders = $this->getRootFolder()->getDirectoryListing();
|
||||
foreach ($folders as $folder) {
|
||||
if ($folder->getName() !== 'images' && $folder->getName() !== $currentFolder->getName()) {
|
||||
$folder->delete();
|
||||
@@ -316,4 +318,12 @@ class ImageManager {
|
||||
$cache->set('shouldReplaceIcons', $value);
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function getRootFolder(): ISimpleFolder {
|
||||
try {
|
||||
return $this->appData->getFolder('global');
|
||||
} catch (NotFoundException $e) {
|
||||
return $this->appData->newFolder('global');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Arthur Schiwon <blizzz@arthur-schiwon.de>
|
||||
*
|
||||
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Theming\Jobs;
|
||||
|
||||
use OCA\Theming\AppInfo\Application;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\BackgroundJob\IJobList;
|
||||
use OCP\BackgroundJob\QueuedJob;
|
||||
use OCP\Files\AppData\IAppDataFactory;
|
||||
use OCP\Files\NotFoundException;
|
||||
use OCP\Files\NotPermittedException;
|
||||
use OCP\Files\SimpleFS\ISimpleFolder;
|
||||
use OCP\IConfig;
|
||||
|
||||
class MigrateBackgroundImages extends QueuedJob {
|
||||
public const TIME_SENSITIVE = 0;
|
||||
|
||||
private IConfig $config;
|
||||
private IAppManager $appManager;
|
||||
private IAppDataFactory $appDataFactory;
|
||||
private IJobList $jobList;
|
||||
|
||||
public function __construct(ITimeFactory $time, IAppDataFactory $appDataFactory, IConfig $config, IAppManager $appManager, IJobList $jobList) {
|
||||
parent::__construct($time);
|
||||
$this->config = $config;
|
||||
$this->appManager = $appManager;
|
||||
$this->appDataFactory = $appDataFactory;
|
||||
$this->jobList = $jobList;
|
||||
}
|
||||
|
||||
protected function run($argument): void {
|
||||
if (!$this->appManager->isEnabledForUser('dashboard')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$dashboardData = $this->appDataFactory->get('dashboard');
|
||||
|
||||
$userIds = $this->config->getUsersForUserValue('theming', 'background', 'custom');
|
||||
|
||||
$notSoFastMode = \count($userIds) > 5000;
|
||||
$reTrigger = false;
|
||||
$processed = 0;
|
||||
|
||||
foreach ($userIds as $userId) {
|
||||
try {
|
||||
// precondition
|
||||
if ($notSoFastMode) {
|
||||
if ($this->config->getUserValue($userId, 'theming', 'background-migrated', '0') === '1') {
|
||||
// already migrated
|
||||
continue;
|
||||
}
|
||||
$reTrigger = true;
|
||||
}
|
||||
|
||||
// migration
|
||||
$file = $dashboardData->getFolder($userId)->getFile('background.jpg');
|
||||
$targetDir = $this->getUserFolder($userId);
|
||||
|
||||
if (!$targetDir->fileExists('background.jpg')) {
|
||||
$targetDir->newFile('background.jpg', $file->getContent());
|
||||
}
|
||||
$file->delete();
|
||||
} catch (NotFoundException|NotPermittedException $e) {
|
||||
}
|
||||
// capture state
|
||||
if ($notSoFastMode) {
|
||||
$this->config->setUserValue($userId, 'theming', 'background-migrated', '1');
|
||||
$processed++;
|
||||
}
|
||||
if ($processed > 4999) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($reTrigger) {
|
||||
$this->jobList->add(self::class);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the root location for users theming data
|
||||
*/
|
||||
protected function getUserFolder(string $userId): ISimpleFolder {
|
||||
$themingData = $this->appDataFactory->get(Application::APP_ID);
|
||||
|
||||
try {
|
||||
$rootFolder = $themingData->getFolder('users');
|
||||
} catch (NotFoundException $e) {
|
||||
$rootFolder = $themingData->newFolder('users');
|
||||
}
|
||||
|
||||
try {
|
||||
return $rootFolder->getFolder($userId);
|
||||
} catch (NotFoundException $e) {
|
||||
return $rootFolder->newFolder($userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,11 +89,6 @@ class BeforeTemplateRenderedListener implements IEventListener {
|
||||
$this->config->getUserValue($userId, Application::APP_ID, 'background', 'default'),
|
||||
);
|
||||
|
||||
$this->initialState->provideInitialState(
|
||||
'backgroundVersion',
|
||||
$this->config->getUserValue($userId, Application::APP_ID, 'backgroundVersion', 0),
|
||||
);
|
||||
|
||||
$this->initialState->provideInitialState(
|
||||
'themingDefaultBackground',
|
||||
$this->config->getAppValue('theming', 'backgroundMime', ''),
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2022 Christopher Ng <chrng8@gmail.com>
|
||||
*
|
||||
* @author Christopher Ng <chrng8@gmail.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Theming\Migration;
|
||||
|
||||
use OCP\Files\IAppData;
|
||||
use OCP\Files\SimpleFS\ISimpleFolder;
|
||||
use OCP\IL10N;
|
||||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\IRepairStep;
|
||||
use Throwable;
|
||||
|
||||
class CleanupOldCache implements IRepairStep {
|
||||
private const CACHE_FOLDERS = [
|
||||
'global',
|
||||
'users',
|
||||
];
|
||||
|
||||
private IAppData $appData;
|
||||
private IL10N $l10n;
|
||||
|
||||
public function __construct(
|
||||
IAppData $appData,
|
||||
IL10N $l10n
|
||||
) {
|
||||
$this->appData = $appData;
|
||||
$this->l10n = $l10n;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return $this->l10n->t('Cleanup old theming cache');
|
||||
}
|
||||
|
||||
public function run(IOutput $output): void {
|
||||
$folders = array_filter(
|
||||
$this->appData->getDirectoryListing(),
|
||||
fn (ISimpleFolder $folder): bool => !in_array($folder->getName(), static::CACHE_FOLDERS, true),
|
||||
);
|
||||
|
||||
$output->startProgress(count($folders));
|
||||
|
||||
foreach ($folders as $folder) {
|
||||
try {
|
||||
$folder->delete();
|
||||
} catch (Throwable $e) {
|
||||
$output->warning($this->l10n->t('Failed to delete folder: "%1$s", error: %2$s', [$folder->getName(), $e->getMessage()]));
|
||||
}
|
||||
$output->advance();
|
||||
}
|
||||
|
||||
$output->finishProgress();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Arthur Schiwon <blizzz@arthur-schiwon.de>
|
||||
*
|
||||
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Theming\Migration;
|
||||
|
||||
use OCA\Theming\Jobs\MigrateBackgroundImages;
|
||||
use OCP\BackgroundJob\IJobList;
|
||||
use OCP\Migration\IOutput;
|
||||
|
||||
class InitBackgroundImagesMigration implements \OCP\Migration\IRepairStep {
|
||||
|
||||
private IJobList $jobList;
|
||||
|
||||
public function __construct(IJobList $jobList) {
|
||||
$this->jobList = $jobList;
|
||||
}
|
||||
|
||||
public function getName() {
|
||||
return 'Initialize migration of background images from dashboard to theming app';
|
||||
}
|
||||
|
||||
public function run(IOutput $output) {
|
||||
$this->jobList->add(MigrateBackgroundImages::class);
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ namespace OCA\Theming\Service;
|
||||
use InvalidArgumentException;
|
||||
use OC\User\NoUserException;
|
||||
use OCA\Theming\AppInfo\Application;
|
||||
use OCP\Files\AppData\IAppDataFactory;
|
||||
use OCP\Files\File;
|
||||
use OCP\Files\IAppData;
|
||||
use OCP\Files\IRootFolder;
|
||||
@@ -44,92 +45,111 @@ use OCP\PreConditionNotMetException;
|
||||
class BackgroundService {
|
||||
// true when the background is bright and need dark icons
|
||||
public const THEMING_MODE_DARK = 'dark';
|
||||
public const DEFAULT_COLOR = '#0082c9';
|
||||
public const DEFAULT_ACCESSIBLE_COLOR = '#00639a';
|
||||
|
||||
public const SHIPPED_BACKGROUNDS = [
|
||||
'anatoly-mikhaltsov-butterfly-wing-scale.jpg' => [
|
||||
'attribution' => 'Butterfly wing scale (Anatoly Mikhaltsov, CC BY-SA)',
|
||||
'attribution_url' => 'https://commons.wikimedia.org/wiki/File:%D0%A7%D0%B5%D1%88%D1%83%D0%B9%D0%BA%D0%B8_%D0%BA%D1%80%D1%8B%D0%BB%D0%B0_%D0%B1%D0%B0%D0%B1%D0%BE%D1%87%D0%BA%D0%B8.jpg',
|
||||
'primary_color' => '#a53c17',
|
||||
],
|
||||
'bernie-cetonia-aurata-take-off-composition.jpg' => [
|
||||
'attribution' => 'Cetonia aurata take off composition (Bernie, Public Domain)',
|
||||
'attribution_url' => 'https://commons.wikimedia.org/wiki/File:Cetonia_aurata_take_off_composition_05172009.jpg',
|
||||
'theming' => self::THEMING_MODE_DARK,
|
||||
'primary_color' => '#56633d',
|
||||
],
|
||||
'dejan-krsmanovic-ribbed-red-metal.jpg' => [
|
||||
'attribution' => 'Ribbed red metal (Dejan Krsmanovic, CC BY)',
|
||||
'attribution_url' => 'https://www.flickr.com/photos/dejankrsmanovic/42971456774/',
|
||||
'primary_color' => '#9c4236',
|
||||
],
|
||||
'eduardo-neves-pedra-azul.jpg' => [
|
||||
'attribution' => 'Pedra azul milky way (Eduardo Neves, CC BY-SA)',
|
||||
'attribution_url' => 'https://commons.wikimedia.org/wiki/File:Pedra_Azul_Milky_Way.jpg',
|
||||
'primary_color' => '#4f6071',
|
||||
],
|
||||
'european-space-agency-barents-bloom.jpg' => [
|
||||
'attribution' => 'Barents bloom (European Space Agency, CC BY-SA)',
|
||||
'attribution_url' => 'https://www.esa.int/ESA_Multimedia/Images/2016/08/Barents_bloom',
|
||||
'primary_color' => '#396475',
|
||||
],
|
||||
'hannes-fritz-flippity-floppity.jpg' => [
|
||||
'attribution' => 'Flippity floppity (Hannes Fritz, CC BY-SA)',
|
||||
'attribution_url' => 'http://hannes.photos/flippity-floppity',
|
||||
'primary_color' => '#98415a',
|
||||
],
|
||||
'hannes-fritz-roulette.jpg' => [
|
||||
'attribution' => 'Roulette (Hannes Fritz, CC BY-SA)',
|
||||
'attribution_url' => 'http://hannes.photos/roulette',
|
||||
'primary_color' => '#845334',
|
||||
],
|
||||
'hannes-fritz-sea-spray.jpg' => [
|
||||
'attribution' => 'Sea spray (Hannes Fritz, CC BY-SA)',
|
||||
'attribution_url' => 'http://hannes.photos/sea-spray',
|
||||
'primary_color' => '#4f6071',
|
||||
],
|
||||
'kamil-porembinski-clouds.jpg' => [
|
||||
'attribution' => 'Clouds (Kamil Porembiński, CC BY-SA)',
|
||||
'attribution_url' => 'https://www.flickr.com/photos/paszczak000/8715851521/',
|
||||
'primary_color' => self::DEFAULT_COLOR,
|
||||
],
|
||||
'bernard-spragg-new-zealand-fern.jpg' => [
|
||||
'attribution' => 'New zealand fern (Bernard Spragg, CC0)',
|
||||
'attribution_url' => 'https://commons.wikimedia.org/wiki/File:NZ_Fern.(Blechnum_chambersii)_(11263534936).jpg',
|
||||
'primary_color' => '#316b26',
|
||||
],
|
||||
'rawpixel-pink-tapioca-bubbles.jpg' => [
|
||||
'attribution' => 'Pink tapioca bubbles (Rawpixel, CC BY)',
|
||||
'attribution_url' => 'https://www.flickr.com/photos/byrawpixel/27665140298/in/photostream/',
|
||||
'theming' => self::THEMING_MODE_DARK,
|
||||
'primary_color' => '#7b4e7e',
|
||||
],
|
||||
'nasa-waxing-crescent-moon.jpg' => [
|
||||
'attribution' => 'Waxing crescent moon (NASA, Public Domain)',
|
||||
'attribution_url' => 'https://www.nasa.gov/image-feature/a-waxing-crescent-moon',
|
||||
'primary_color' => '#005ac1',
|
||||
],
|
||||
'tommy-chau-already.jpg' => [
|
||||
'attribution' => 'Cityscape (Tommy Chau, CC BY)',
|
||||
'attribution_url' => 'https://www.flickr.com/photos/90975693@N05/16910999368',
|
||||
'primary_color' => '#6a2af4',
|
||||
],
|
||||
'tommy-chau-lion-rock-hill.jpg' => [
|
||||
'attribution' => 'Lion rock hill (Tommy Chau, CC BY)',
|
||||
'attribution_url' => 'https://www.flickr.com/photos/90975693@N05/17136440246',
|
||||
'theming' => self::THEMING_MODE_DARK,
|
||||
'primary_color' => '#7f4f70',
|
||||
],
|
||||
'lali-masriera-yellow-bricks.jpg' => [
|
||||
'attribution' => 'Yellow bricks (Lali Masriera, CC BY)',
|
||||
'attribution_url' => 'https://www.flickr.com/photos/visualpanic/3982464447',
|
||||
'theming' => self::THEMING_MODE_DARK,
|
||||
]
|
||||
'primary_color' => '#7f5700',
|
||||
],
|
||||
];
|
||||
|
||||
private IRootFolder $rootFolder;
|
||||
private IAppData $appData;
|
||||
private IConfig $config;
|
||||
private string $userId;
|
||||
private IAppDataFactory $appDataFactory;
|
||||
|
||||
public function __construct(
|
||||
IRootFolder $rootFolder,
|
||||
IAppData $appData,
|
||||
IConfig $config,
|
||||
?string $userId
|
||||
) {
|
||||
public function __construct(IRootFolder $rootFolder,
|
||||
IAppData $appData,
|
||||
IConfig $config,
|
||||
?string $userId,
|
||||
IAppDataFactory $appDataFactory) {
|
||||
if ($userId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->rootFolder = $rootFolder;
|
||||
$this->appData = $appData;
|
||||
$this->config = $config;
|
||||
$this->userId = $userId;
|
||||
$this->appData = $appData;
|
||||
$this->appDataFactory = $appDataFactory;
|
||||
}
|
||||
|
||||
public function setDefaultBackground(): void {
|
||||
@@ -147,12 +167,15 @@ class BackgroundService {
|
||||
public function setFileBackground($path): void {
|
||||
$this->config->setUserValue($this->userId, Application::APP_ID, 'background', 'custom');
|
||||
$userFolder = $this->rootFolder->getUserFolder($this->userId);
|
||||
|
||||
/** @var File $file */
|
||||
$file = $userFolder->get($path);
|
||||
$image = new \OCP\Image();
|
||||
|
||||
if ($image->loadFromFileHandle($file->fopen('r')) === false) {
|
||||
throw new InvalidArgumentException('Invalid image file');
|
||||
}
|
||||
|
||||
$this->getAppDataFolder()->newFile('background.jpg', $file->fopen('r'));
|
||||
}
|
||||
|
||||
@@ -176,20 +199,32 @@ class BackgroundService {
|
||||
try {
|
||||
return $this->getAppDataFolder()->getFile('background.jpg');
|
||||
} catch (NotFoundException | NotPermittedException $e) {
|
||||
try {
|
||||
// Fallback can be removed in 26
|
||||
$dashboardFolder = $this->appDataFactory->get('dashboard');
|
||||
return $dashboardFolder->getFolder($this->userId)->getFile('background.jpg');
|
||||
} catch (\Throwable $t) {}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Storing the data in appdata/theming/users/USERID
|
||||
*
|
||||
* @return ISimpleFolder
|
||||
* @throws NotPermittedException
|
||||
*/
|
||||
private function getAppDataFolder(): ISimpleFolder {
|
||||
try {
|
||||
return $this->appData->getFolder($this->userId);
|
||||
$rootFolder = $this->appData->getFolder('users');
|
||||
} catch (NotFoundException $e) {
|
||||
return $this->appData->newFolder($this->userId);
|
||||
$rootFolder = $this->appData->newFolder('users');
|
||||
}
|
||||
try {
|
||||
return $rootFolder->getFolder($this->userId);
|
||||
} catch (NotFoundException $e) {
|
||||
return $rootFolder->newFolder($this->userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ class JSDataService implements \JsonSerializable {
|
||||
'url' => $this->themingDefaults->getBaseUrl(),
|
||||
'slogan' => $this->themingDefaults->getSlogan(),
|
||||
'color' => $this->themingDefaults->getColorPrimary(),
|
||||
'defaultColor' => $this->themingDefaults->getDefaultColorPrimary(),
|
||||
'imprintUrl' => $this->themingDefaults->getImprintUrl(),
|
||||
'privacyUrl' => $this->themingDefaults->getPrivacyUrl(),
|
||||
'inverted' => $this->util->invertTextColor($this->themingDefaults->getColorPrimary()),
|
||||
|
||||
@@ -22,8 +22,11 @@
|
||||
*/
|
||||
namespace OCA\Theming\Service;
|
||||
|
||||
use OCA\Theming\AppInfo\Application;
|
||||
use OCA\Theming\Themes\DefaultTheme;
|
||||
use OCP\IConfig;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IUserSession;
|
||||
use OCP\Util;
|
||||
|
||||
class ThemeInjectionService {
|
||||
@@ -31,13 +34,23 @@ class ThemeInjectionService {
|
||||
private IURLGenerator $urlGenerator;
|
||||
private ThemesService $themesService;
|
||||
private DefaultTheme $defaultTheme;
|
||||
private IConfig $config;
|
||||
private ?string $userId;
|
||||
|
||||
public function __construct(IURLGenerator $urlGenerator,
|
||||
ThemesService $themesService,
|
||||
DefaultTheme $defaultTheme) {
|
||||
DefaultTheme $defaultTheme,
|
||||
IConfig $config,
|
||||
IUserSession $userSession) {
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->themesService = $themesService;
|
||||
$this->defaultTheme = $defaultTheme;
|
||||
$this->config = $config;
|
||||
if ($userSession->getUser() !== null) {
|
||||
$this->userId = $userSession->getUser()->getUID();
|
||||
} else {
|
||||
$this->userId = null;
|
||||
}
|
||||
}
|
||||
|
||||
public function injectHeaders() {
|
||||
@@ -50,13 +63,13 @@ class ThemeInjectionService {
|
||||
|
||||
// Default theme fallback
|
||||
$this->addThemeHeader($defaultTheme->getId());
|
||||
|
||||
|
||||
// Themes applied by media queries
|
||||
foreach($mediaThemes as $theme) {
|
||||
$this->addThemeHeader($theme->getId(), true, $theme->getMediaQuery());
|
||||
}
|
||||
|
||||
// Themes
|
||||
// Themes
|
||||
foreach($this->themesService->getThemes() as $theme) {
|
||||
// Ignore default theme as already processed first
|
||||
if ($theme->getId() === $this->defaultTheme->getId()) {
|
||||
@@ -68,15 +81,24 @@ class ThemeInjectionService {
|
||||
|
||||
/**
|
||||
* Inject theme header into rendered page
|
||||
*
|
||||
*
|
||||
* @param string $themeId the theme ID
|
||||
* @param bool $plain request the :root syntax
|
||||
* @param string $media media query to use in the <link> element
|
||||
*/
|
||||
private function addThemeHeader(string $themeId, bool $plain = true, string $media = null) {
|
||||
$cacheBuster = $this->config->getAppValue('theming', 'cachebuster', '0');
|
||||
if ($this->userId !== null) {
|
||||
// need to bust the cache for the CSS file when the user background changed as its
|
||||
// URL is served in those files
|
||||
$userCacheBuster = $this->config->getUserValue($this->userId, Application::APP_ID, 'userCacheBuster', '0');
|
||||
$cacheBuster .= $this->userId . '_' . $userCacheBuster;
|
||||
}
|
||||
|
||||
$linkToCSS = $this->urlGenerator->linkToRoute('theming.Theming.getThemeStylesheet', [
|
||||
'themeId' => $themeId,
|
||||
'plain' => $plain,
|
||||
'v' => substr(sha1($cacheBuster), 0, 8),
|
||||
]);
|
||||
Util::addHeader('link', [
|
||||
'rel' => 'stylesheet',
|
||||
|
||||
@@ -87,9 +87,9 @@ class ThemesService {
|
||||
}
|
||||
|
||||
/** @var ITheme[] */
|
||||
$themes = array_map(function($themeId) {
|
||||
$themes = array_filter(array_map(function($themeId) {
|
||||
return $this->getThemes()[$themeId];
|
||||
}, $themesIds);
|
||||
}, $themesIds));
|
||||
|
||||
// Filtering all themes with the same type
|
||||
$filteredThemes = array_filter($themes, function(ITheme $t) use ($theme) {
|
||||
|
||||
@@ -75,13 +75,14 @@ class Admin implements IDelegatedSettings {
|
||||
'name' => $this->themingDefaults->getEntity(),
|
||||
'url' => $this->themingDefaults->getBaseUrl(),
|
||||
'slogan' => $this->themingDefaults->getSlogan(),
|
||||
'color' => $this->themingDefaults->getColorPrimary(),
|
||||
'color' => $this->themingDefaults->getDefaultColorPrimary(),
|
||||
'uploadLogoRoute' => $this->urlGenerator->linkToRoute('theming.Theming.uploadImage'),
|
||||
'canThemeIcons' => $this->imageManager->shouldReplaceIcons(),
|
||||
'iconDocs' => $this->urlGenerator->linkToDocs('admin-theming-icons'),
|
||||
'images' => $this->imageManager->getCustomImages(),
|
||||
'imprintUrl' => $this->themingDefaults->getImprintUrl(),
|
||||
'privacyUrl' => $this->themingDefaults->getPrivacyUrl(),
|
||||
'userThemingDisabled' => $this->themingDefaults->isUserThemingDisabled(),
|
||||
];
|
||||
|
||||
return new TemplateResponse($this->appName, 'settings-admin', $parameters, '');
|
||||
|
||||
@@ -27,6 +27,7 @@ namespace OCA\Theming\Settings;
|
||||
|
||||
use OCA\Theming\ITheme;
|
||||
use OCA\Theming\Service\ThemesService;
|
||||
use OCA\Theming\ThemingDefaults;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\AppFramework\Services\IInitialState;
|
||||
use OCP\IConfig;
|
||||
@@ -39,15 +40,18 @@ class Personal implements ISettings {
|
||||
private IConfig $config;
|
||||
private ThemesService $themesService;
|
||||
private IInitialState $initialStateService;
|
||||
private ThemingDefaults $themingDefaults;
|
||||
|
||||
public function __construct(string $appName,
|
||||
IConfig $config,
|
||||
ThemesService $themesService,
|
||||
IInitialState $initialStateService) {
|
||||
IInitialState $initialStateService,
|
||||
ThemingDefaults $themingDefaults) {
|
||||
$this->appName = $appName;
|
||||
$this->config = $config;
|
||||
$this->themesService = $themesService;
|
||||
$this->initialStateService = $initialStateService;
|
||||
$this->themingDefaults = $themingDefaults;
|
||||
}
|
||||
|
||||
public function getForm(): TemplateResponse {
|
||||
@@ -72,6 +76,7 @@ class Personal implements ISettings {
|
||||
|
||||
$this->initialStateService->provideInitialState('themes', array_values($themes));
|
||||
$this->initialStateService->provideInitialState('enforceTheme', $enforcedTheme);
|
||||
$this->initialStateService->provideInitialState('isUserThemingDisabled', $this->themingDefaults->isUserThemingDisabled());
|
||||
Util::addScript($this->appName, 'theming-settings');
|
||||
|
||||
return new TemplateResponse($this->appName, 'settings-personal');
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
|
||||
*
|
||||
* @author Joas Schilling <coding@schilljs.com>
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
namespace OCA\Theming\Themes;
|
||||
|
||||
use OCA\Theming\AppInfo\Application;
|
||||
use OCA\Theming\ImageManager;
|
||||
use OCA\Theming\Service\BackgroundService;
|
||||
use OCA\Theming\Util;
|
||||
|
||||
trait CommonThemeTrait {
|
||||
public Util $util;
|
||||
|
||||
/**
|
||||
* Generate primary-related variables
|
||||
* This is shared between multiple themes because colorMainBackground and colorMainText
|
||||
* will change in between.
|
||||
*/
|
||||
protected function generatePrimaryVariables(string $colorMainBackground, string $colorMainText): array {
|
||||
$colorPrimaryLight = $this->util->mix($this->primaryColor, $colorMainBackground, -80);
|
||||
$colorPrimaryElement = $this->util->elementColor($this->primaryColor);
|
||||
$colorPrimaryElementLight = $this->util->mix($colorPrimaryElement, $colorMainBackground, -80);
|
||||
|
||||
// primary related colours
|
||||
return [
|
||||
// invert filter if primary is too bright
|
||||
// to be used for legacy reasons only. Use inline
|
||||
// svg with proper css variable instead or material
|
||||
// design icons.
|
||||
// ⚠️ Using 'no' as a value to make sure we specify an
|
||||
// invalid one with no fallback. 'unset' could here fallback to some
|
||||
// other theme with media queries
|
||||
'--primary-invert-if-bright' => $this->util->invertTextColor($this->primaryColor) ? 'invert(100%)' : 'no',
|
||||
|
||||
'--color-primary' => $this->primaryColor,
|
||||
'--color-primary-default' => $this->defaultPrimaryColor,
|
||||
'--color-primary-text' => $this->util->invertTextColor($this->primaryColor) ? '#000000' : '#ffffff',
|
||||
'--color-primary-hover' => $this->util->mix($this->primaryColor, $colorMainBackground, 60),
|
||||
'--color-primary-light' => $colorPrimaryLight,
|
||||
'--color-primary-light-text' => $this->util->mix($this->primaryColor, $this->util->invertTextColor($colorPrimaryLight) ? '#000000' : '#ffffff', -20),
|
||||
'--color-primary-light-hover' => $this->util->mix($colorPrimaryLight, $colorMainText, 90),
|
||||
'--color-primary-text-dark' => $this->util->darken($this->util->invertTextColor($this->primaryColor) ? '#000000' : '#ffffff', 7),
|
||||
|
||||
// used for buttons, inputs...
|
||||
'--color-primary-element' => $colorPrimaryElement,
|
||||
'--color-primary-element-text' => $this->util->invertTextColor($colorPrimaryElement) ? '#000000' : '#ffffff',
|
||||
'--color-primary-element-hover' => $this->util->mix($colorPrimaryElement, $colorMainBackground, 60),
|
||||
'--color-primary-element-light' => $colorPrimaryElementLight,
|
||||
'--color-primary-element-light-text' => $this->util->mix($colorPrimaryElement, $this->util->invertTextColor($colorPrimaryElementLight) ? '#000000' : '#ffffff', -20),
|
||||
'--color-primary-element-light-hover' => $this->util->mix($colorPrimaryElementLight, $colorMainText, 90),
|
||||
'--color-primary-element-text-dark' => $this->util->darken($this->util->invertTextColor($colorPrimaryElement) ? '#000000' : '#ffffff', 7),
|
||||
|
||||
// to use like this: background-image: var(--gradient-primary-background);
|
||||
'--gradient-primary-background' => 'linear-gradient(40deg, var(--color-primary) 0%, var(--color-primary-hover) 100%)',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate admin theming background-related variables
|
||||
*/
|
||||
protected function generateGlobalBackgroundVariables(): array {
|
||||
$backgroundDeleted = $this->config->getAppValue(Application::APP_ID, 'backgroundMime', '') === 'backgroundColor';
|
||||
$hasCustomLogoHeader = $this->imageManager->hasImage('logo') || $this->imageManager->hasImage('logoheader');
|
||||
|
||||
$variables = [];
|
||||
|
||||
// If primary as background has been request or if we have a custom primary colour
|
||||
// let's not define the background image
|
||||
if ($backgroundDeleted && $this->themingDefaults->isUserThemingDisabled()) {
|
||||
$variables['--image-background-plain'] = 'true';
|
||||
$variables['--color-background-plain'] = $this->themingDefaults->getColorPrimary();
|
||||
}
|
||||
|
||||
// Register image variables only if custom-defined
|
||||
foreach (ImageManager::SupportedImageKeys as $image) {
|
||||
if ($this->imageManager->hasImage($image)) {
|
||||
$imageUrl = $this->imageManager->getImageUrl($image);
|
||||
if ($image === 'background') {
|
||||
// If background deleted is set, ignoring variable
|
||||
if ($backgroundDeleted) {
|
||||
continue;
|
||||
}
|
||||
$variables['--image-background-size'] = 'cover';
|
||||
}
|
||||
$variables["--image-$image"] = "url('" . $imageUrl . "')";
|
||||
}
|
||||
}
|
||||
|
||||
if ($hasCustomLogoHeader) {
|
||||
$variables["--image-logoheader-custom"] = 'true';
|
||||
}
|
||||
|
||||
return $variables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate user theming background-related variables
|
||||
*/
|
||||
protected function generateUserBackgroundVariables(): array {
|
||||
$user = $this->userSession->getUser();
|
||||
if ($user !== null
|
||||
&& !$this->themingDefaults->isUserThemingDisabled()
|
||||
&& $this->appManager->isEnabledForUser(Application::APP_ID)) {
|
||||
$themingBackground = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'background', 'default');
|
||||
$currentVersion = (int)$this->config->getUserValue($user->getUID(), Application::APP_ID, 'userCacheBuster', '0');
|
||||
|
||||
// The user uploaded a custom background
|
||||
if ($themingBackground === 'custom') {
|
||||
$cacheBuster = substr(sha1($user->getUID() . '_' . $currentVersion), 0, 8);
|
||||
return [
|
||||
'--image-background' => "url('" . $this->urlGenerator->linkToRouteAbsolute('theming.userTheme.getBackground') . "?v=$cacheBuster')",
|
||||
// TODO: implement primary color from custom background --color-background-plain
|
||||
];
|
||||
}
|
||||
|
||||
// The user picked a shipped background
|
||||
if (isset(BackgroundService::SHIPPED_BACKGROUNDS[$themingBackground])) {
|
||||
return [
|
||||
'--image-background' => "url('" . $this->urlGenerator->linkTo(Application::APP_ID, "/img/background/$themingBackground") . "')",
|
||||
'--color-background-plain' => $this->themingDefaults->getColorPrimary(),
|
||||
'--background-image-invert-if-bright' => BackgroundService::SHIPPED_BACKGROUNDS[$themingBackground]['theming'] ?? null === BackgroundService::THEMING_MODE_DARK ? 'invert(100%)' : 'no',
|
||||
];
|
||||
}
|
||||
|
||||
// The user picked a static colour
|
||||
if (substr($themingBackground, 0, 1) === '#') {
|
||||
return [
|
||||
'--image-background' => 'no',
|
||||
'--color-background-plain' => $this->themingDefaults->getColorPrimary(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -49,42 +49,51 @@ class DarkHighContrastTheme extends DarkTheme implements ITheme {
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to keep this consistent with HighContrastTheme
|
||||
* Keep this consistent with other HighContrast Themes
|
||||
*/
|
||||
public function getCSSVariables(): array {
|
||||
$variables = parent::getCSSVariables();
|
||||
$defaultVariables = parent::getCSSVariables();
|
||||
|
||||
$colorMainText = '#ffffff';
|
||||
$colorMainBackground = '#000000';
|
||||
$colorMainBackgroundRGB = join(',', $this->util->hexToRGB($colorMainBackground));
|
||||
|
||||
$variables['--color-main-background'] = $colorMainBackground;
|
||||
$variables['--color-main-background-translucent'] = 'rgba(var(--color-main-background-rgb), .1)';
|
||||
$variables['--color-main-text'] = $colorMainText;
|
||||
return array_merge(
|
||||
$defaultVariables,
|
||||
$this->generatePrimaryVariables($colorMainBackground, $colorMainText),
|
||||
[
|
||||
'--color-main-background' => $colorMainBackground,
|
||||
'--color-main-background-rgb' => $colorMainBackgroundRGB,
|
||||
'--color-main-background-translucent' => 'rgba(var(--color-main-background-rgb), 1)',
|
||||
'--color-main-text' => $colorMainText,
|
||||
|
||||
$variables['--color-background-dark'] = $this->util->lighten($colorMainBackground, 30);
|
||||
$variables['--color-background-darker'] = $this->util->lighten($colorMainBackground, 30);
|
||||
'--color-background-dark' => $this->util->lighten($colorMainBackground, 30),
|
||||
'--color-background-darker' => $this->util->lighten($colorMainBackground, 30),
|
||||
|
||||
$variables['--color-placeholder-light'] = $this->util->lighten($colorMainBackground, 30);
|
||||
$variables['--color-placeholder-dark'] = $this->util->lighten($colorMainBackground, 45);
|
||||
'--color-main-background-blur' => $colorMainBackground,
|
||||
'--filter-background-blur' => 'none',
|
||||
|
||||
$variables['--color-text-maxcontrast'] = $colorMainText;
|
||||
$variables['--color-text-light'] = $colorMainText;
|
||||
$variables['--color-text-lighter'] = $colorMainText;
|
||||
'--color-placeholder-light' => $this->util->lighten($colorMainBackground, 30),
|
||||
'--color-placeholder-dark' => $this->util->lighten($colorMainBackground, 45),
|
||||
|
||||
$variables['--color-scrollbar'] = $this->util->lighten($colorMainBackground, 35);
|
||||
'--color-text-maxcontrast' => $colorMainText,
|
||||
'--color-text-maxcontrast-background-blur' => $colorMainText,
|
||||
'--color-text-light' => $colorMainText,
|
||||
'--color-text-lighter' => $colorMainText,
|
||||
|
||||
// used for the icon loading animation
|
||||
$variables['--color-loading-light'] = '#000000';
|
||||
$variables['--color-loading-dark'] = '#dddddd';
|
||||
'--color-scrollbar' => $this->util->lighten($colorMainBackground, 35),
|
||||
|
||||
// used for the icon loading animation
|
||||
'--color-loading-light' => '#000000',
|
||||
'--color-loading-dark' => '#dddddd',
|
||||
|
||||
$variables['--color-box-shadow-rgb'] = 'var(--color-main-text)';
|
||||
$variables['--color-box-shadow'] = 'var(--color-main-text)';
|
||||
'--color-box-shadow-rgb' => $colorMainText,
|
||||
'--color-box-shadow' => $colorMainText,
|
||||
|
||||
|
||||
$variables['--color-border'] = $this->util->lighten($colorMainBackground, 50);
|
||||
$variables['--color-border-dark'] = $this->util->lighten($colorMainBackground, 50);
|
||||
|
||||
return $variables;
|
||||
'--color-border' => $this->util->lighten($colorMainBackground, 50),
|
||||
'--color-border-dark' => $this->util->lighten($colorMainBackground, 50),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function getCustomCss(): string {
|
||||
|
||||
@@ -54,45 +54,47 @@ class DarkTheme extends DefaultTheme implements ITheme {
|
||||
$colorMainText = '#D8D8D8';
|
||||
$colorMainBackground = '#171717';
|
||||
$colorMainBackgroundRGB = join(',', $this->util->hexToRGB($colorMainBackground));
|
||||
$colorTextMaxcontrast = $this->util->darken($colorMainText, 30);
|
||||
|
||||
$colorBoxShadow = $this->util->darken($colorMainBackground, 70);
|
||||
$colorBoxShadowRGB = join(',', $this->util->hexToRGB($colorBoxShadow));
|
||||
$colorPrimaryLight = $this->util->mix($this->primaryColor, $colorMainBackground, -80);
|
||||
|
||||
return array_merge($defaultVariables, [
|
||||
'--color-main-text' => $colorMainText,
|
||||
'--color-main-background' => $colorMainBackground,
|
||||
'--color-main-background-rgb' => $colorMainBackgroundRGB,
|
||||
return array_merge(
|
||||
$defaultVariables,
|
||||
$this->generatePrimaryVariables($colorMainBackground, $colorMainText),
|
||||
[
|
||||
'--color-main-text' => $colorMainText,
|
||||
'--color-main-background' => $colorMainBackground,
|
||||
'--color-main-background-rgb' => $colorMainBackgroundRGB,
|
||||
|
||||
'--color-scrollbar' => $this->util->lighten($colorMainBackground, 15),
|
||||
'--color-scrollbar' => $this->util->lighten($colorMainBackground, 15),
|
||||
|
||||
'--color-background-hover' => $this->util->lighten($colorMainBackground, 4),
|
||||
'--color-background-dark' => $this->util->lighten($colorMainBackground, 7),
|
||||
'--color-background-darker' => $this->util->lighten($colorMainBackground, 14),
|
||||
'--color-background-hover' => $this->util->lighten($colorMainBackground, 4),
|
||||
'--color-background-dark' => $this->util->lighten($colorMainBackground, 7),
|
||||
'--color-background-darker' => $this->util->lighten($colorMainBackground, 14),
|
||||
|
||||
'--color-placeholder-light' => $this->util->lighten($colorMainBackground, 10),
|
||||
'--color-placeholder-dark' => $this->util->lighten($colorMainBackground, 20),
|
||||
'--color-placeholder-light' => $this->util->lighten($colorMainBackground, 10),
|
||||
'--color-placeholder-dark' => $this->util->lighten($colorMainBackground, 20),
|
||||
|
||||
'--color-primary-hover' => $this->util->mix($this->primaryColor, $colorMainBackground, 60),
|
||||
'--color-primary-light' => $colorPrimaryLight,
|
||||
'--color-primary-light-hover' => $this->util->mix($colorPrimaryLight, $colorMainText, 90),
|
||||
'--color-primary-element' => $this->util->elementColor($this->primaryColor, false),
|
||||
'--color-primary-element-hover' => $this->util->mix($this->util->elementColor($this->primaryColor, false), $colorMainBackground, 80),
|
||||
'--color-primary-element-light' => $this->util->lighten($this->util->elementColor($this->primaryColor, false), 15),
|
||||
'--color-text-maxcontrast' => $colorTextMaxcontrast,
|
||||
'--color-text-maxcontrast-default' => $colorTextMaxcontrast,
|
||||
'--color-text-maxcontrast-background-blur' => $this->util->lighten($colorTextMaxcontrast, 2),
|
||||
'--color-text-light' => $this->util->darken($colorMainText, 10),
|
||||
'--color-text-lighter' => $this->util->darken($colorMainText, 20),
|
||||
|
||||
'--color-text-maxcontrast' => $this->util->darken($colorMainText, 30),
|
||||
'--color-text-light' => $this->util->darken($colorMainText, 10),
|
||||
'--color-text-lighter' => $this->util->darken($colorMainText, 20),
|
||||
// used for the icon loading animation
|
||||
'--color-loading-light' => '#777',
|
||||
'--color-loading-dark' => '#CCC',
|
||||
|
||||
'--color-loading-light' => '#777',
|
||||
'--color-loading-dark' => '#CCC',
|
||||
'--color-box-shadow' => $colorBoxShadow,
|
||||
'--color-box-shadow-rgb' => $colorBoxShadowRGB,
|
||||
|
||||
'--color-box-shadow-rgb' => $colorBoxShadowRGB,
|
||||
'--color-border' => $this->util->lighten($colorMainBackground, 7),
|
||||
'--color-border-dark' => $this->util->lighten($colorMainBackground, 14),
|
||||
|
||||
'--color-border' => $this->util->lighten($colorMainBackground, 7),
|
||||
'--color-border-dark' => $this->util->lighten($colorMainBackground, 14),
|
||||
|
||||
'--background-invert-if-dark' => 'invert(100%)',
|
||||
'--background-invert-if-bright' => 'no',
|
||||
]);
|
||||
'--background-invert-if-dark' => 'invert(100%)',
|
||||
'--background-invert-if-bright' => 'no',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,9 @@ declare(strict_types=1);
|
||||
*/
|
||||
namespace OCA\Theming\Themes;
|
||||
|
||||
use OCA\Theming\AppInfo\Application;
|
||||
use OCA\Theming\ImageManager;
|
||||
use OCA\Theming\ITheme;
|
||||
use OCA\Theming\Service\BackgroundService;
|
||||
use OCA\Theming\ThemingDefaults;
|
||||
use OCA\Theming\Util;
|
||||
use OCP\App\IAppManager;
|
||||
@@ -34,32 +34,46 @@ use OCP\IConfig;
|
||||
use OCP\IL10N;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IUserSession;
|
||||
use OCP\Server;
|
||||
|
||||
class DefaultTheme implements ITheme {
|
||||
use CommonThemeTrait;
|
||||
|
||||
public Util $util;
|
||||
public ThemingDefaults $themingDefaults;
|
||||
public IUserSession $userSession;
|
||||
public IURLGenerator $urlGenerator;
|
||||
public ImageManager $imageManager;
|
||||
public IConfig $config;
|
||||
public IL10N $l;
|
||||
public IAppManager $appManager;
|
||||
|
||||
public string $defaultPrimaryColor;
|
||||
public string $primaryColor;
|
||||
|
||||
public function __construct(Util $util,
|
||||
ThemingDefaults $themingDefaults,
|
||||
IUserSession $userSession,
|
||||
IURLGenerator $urlGenerator,
|
||||
ImageManager $imageManager,
|
||||
IConfig $config,
|
||||
IL10N $l) {
|
||||
IL10N $l,
|
||||
IAppManager $appManager) {
|
||||
$this->util = $util;
|
||||
$this->themingDefaults = $themingDefaults;
|
||||
$this->userSession = $userSession;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->imageManager = $imageManager;
|
||||
$this->config = $config;
|
||||
$this->l = $l;
|
||||
$this->appManager = $appManager;
|
||||
|
||||
$this->defaultPrimaryColor = $this->themingDefaults->getDefaultColorPrimary();
|
||||
$this->primaryColor = $this->themingDefaults->getColorPrimary();
|
||||
|
||||
// Override default defaultPrimaryColor if set to improve accessibility
|
||||
if ($this->primaryColor === BackgroundService::DEFAULT_COLOR) {
|
||||
$this->primaryColor = BackgroundService::DEFAULT_ACCESSIBLE_COLOR;
|
||||
}
|
||||
}
|
||||
|
||||
public function getId(): string {
|
||||
@@ -89,17 +103,11 @@ class DefaultTheme implements ITheme {
|
||||
public function getCSSVariables(): array {
|
||||
$colorMainText = '#222222';
|
||||
$colorMainTextRgb = join(',', $this->util->hexToRGB($colorMainText));
|
||||
$colorTextMaxcontrast = $this->util->lighten($colorMainText, 33);
|
||||
$colorMainBackground = '#ffffff';
|
||||
$colorMainBackgroundRGB = join(',', $this->util->hexToRGB($colorMainBackground));
|
||||
$colorBoxShadow = $this->util->darken($colorMainBackground, 70);
|
||||
$colorBoxShadowRGB = join(',', $this->util->hexToRGB($colorBoxShadow));
|
||||
$colorPrimaryLight = $this->util->mix($this->primaryColor, $colorMainBackground, -80);
|
||||
|
||||
$colorPrimaryElement = $this->util->elementColor($this->primaryColor);
|
||||
$colorPrimaryElementLight = $this->util->mix($colorPrimaryElement, $colorMainBackground, -80);
|
||||
|
||||
$hasCustomLogoHeader = $this->imageManager->hasImage('logo') || $this->imageManager->hasImage('logoheader');
|
||||
$hasCustomPrimaryColour = !empty($this->config->getAppValue(Application::APP_ID, 'color'));
|
||||
|
||||
$variables = [
|
||||
'--color-main-background' => $colorMainBackground,
|
||||
@@ -119,28 +127,11 @@ class DefaultTheme implements ITheme {
|
||||
'--color-placeholder-light' => $this->util->darken($colorMainBackground, 10),
|
||||
'--color-placeholder-dark' => $this->util->darken($colorMainBackground, 20),
|
||||
|
||||
// primary related colours
|
||||
'--color-primary' => $this->primaryColor,
|
||||
'--color-primary-text' => $this->util->invertTextColor($this->primaryColor) ? '#000000' : '#ffffff',
|
||||
'--color-primary-hover' => $this->util->mix($this->primaryColor, $colorMainBackground, 60),
|
||||
'--color-primary-light' => $colorPrimaryLight,
|
||||
'--color-primary-light-text' => $this->primaryColor,
|
||||
'--color-primary-light-hover' => $this->util->mix($colorPrimaryLight, $colorMainText, 90),
|
||||
'--color-primary-text-dark' => $this->util->darken($this->util->invertTextColor($this->primaryColor) ? '#000000' : '#ffffff', 7),
|
||||
// used for buttons, inputs...
|
||||
'--color-primary-element' => $colorPrimaryElement,
|
||||
'--color-primary-element-text' => $this->util->invertTextColor($colorPrimaryElement) ? '#000000' : '#ffffff',
|
||||
'--color-primary-element-hover' => $this->util->mix($colorPrimaryElement, $colorMainBackground, 60),
|
||||
'--color-primary-element-light' => $colorPrimaryElementLight,
|
||||
'--color-primary-element-light-text' => $colorPrimaryElement,
|
||||
'--color-primary-element-light-hover' => $this->util->mix($colorPrimaryElementLight, $colorMainText, 90),
|
||||
'--color-primary-element-text-dark' => $this->util->darken($this->util->invertTextColor($colorPrimaryElement) ? '#000000' : '#ffffff', 7),
|
||||
// to use like this: background-image: var(--gradient-primary-background);
|
||||
'--gradient-primary-background' => 'linear-gradient(40deg, var(--color-primary) 0%, var(--color-primary-hover) 100%)',
|
||||
|
||||
// max contrast for WCAG compliance
|
||||
'--color-main-text' => $colorMainText,
|
||||
'--color-text-maxcontrast' => $this->util->lighten($colorMainText, 33),
|
||||
'--color-text-maxcontrast' => $colorTextMaxcontrast,
|
||||
'--color-text-maxcontrast-default' => $colorTextMaxcontrast,
|
||||
'--color-text-maxcontrast-background-blur' => $this->util->darken($colorTextMaxcontrast, 7),
|
||||
'--color-text-light' => $colorMainText,
|
||||
'--color-text-lighter' => $this->util->lighten($colorMainText, 33),
|
||||
|
||||
@@ -196,60 +187,19 @@ class DefaultTheme implements ITheme {
|
||||
|
||||
// mobile. Keep in sync with core/js/js.js
|
||||
'--breakpoint-mobile' => '1024px',
|
||||
|
||||
// invert filter if primary is too bright
|
||||
// to be used for legacy reasons only. Use inline
|
||||
// svg with proper css variable instead or material
|
||||
// design icons.
|
||||
// ⚠️ Using 'no' as a value to make sure we specify an
|
||||
// invalid one with no fallback. 'unset' could here fallback to some
|
||||
// other theme with media queries
|
||||
'--primary-invert-if-bright' => $this->util->invertTextColor($this->primaryColor) ? 'invert(100%)' : 'no',
|
||||
'--background-invert-if-dark' => 'no',
|
||||
'--background-invert-if-bright' => 'invert(100%)',
|
||||
'--background-image-invert-if-bright' => 'no',
|
||||
|
||||
'--image-main-background' => "url('" . $this->urlGenerator->imagePath('core', 'app-background.jpg') . "')",
|
||||
// Default last fallback values
|
||||
'--image-background' => "url('" . $this->urlGenerator->imagePath('core', 'app-background.jpg') . "')",
|
||||
'--color-background-plain' => $this->defaultPrimaryColor,
|
||||
];
|
||||
|
||||
$backgroundDeleted = $this->config->getAppValue(Application::APP_ID, 'backgroundMime', '') === 'backgroundColor';
|
||||
// If primary as background has been request or if we have a custom primary colour
|
||||
// let's not define the background image
|
||||
if ($backgroundDeleted || $hasCustomPrimaryColour) {
|
||||
$variables["--image-background-plain"] = 'true';
|
||||
}
|
||||
|
||||
// Register image variables only if custom-defined
|
||||
foreach (['logo', 'logoheader', 'favicon', 'background'] as $image) {
|
||||
if ($this->imageManager->hasImage($image)) {
|
||||
$imageUrl = $this->imageManager->getImageUrl($image);
|
||||
if ($image === 'background') {
|
||||
// If background deleted is set, ignoring variable
|
||||
if ($backgroundDeleted) {
|
||||
continue;
|
||||
}
|
||||
$variables['--image-background-size'] = 'cover';
|
||||
$variables['--image-main-background'] = "url('" . $imageUrl . "')";
|
||||
}
|
||||
$variables["--image-$image"] = "url('" . $imageUrl . "')";
|
||||
}
|
||||
}
|
||||
|
||||
if ($hasCustomLogoHeader) {
|
||||
$variables["--image-logoheader-custom"] = 'true';
|
||||
}
|
||||
|
||||
$appManager = Server::get(IAppManager::class);
|
||||
$userSession = Server::get(IUserSession::class);
|
||||
$user = $userSession->getUser();
|
||||
if ($appManager->isEnabledForUser(Application::APP_ID) && $user !== null) {
|
||||
$themingBackground = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'background', 'default');
|
||||
|
||||
if ($themingBackground === 'custom') {
|
||||
$variables['--image-main-background'] = "url('" . $this->urlGenerator->linkToRouteAbsolute('theming.theming.getBackground') . "')";
|
||||
} elseif ($themingBackground !== 'default' && substr($themingBackground, 0, 1) !== '#') {
|
||||
$variables['--image-main-background'] = "url('" . $this->urlGenerator->linkTo(Application::APP_ID, "/img/background/$themingBackground") . "')";
|
||||
}
|
||||
}
|
||||
// Primary variables
|
||||
$variables = array_merge($variables, $this->generatePrimaryVariables($colorMainBackground, $colorMainText));
|
||||
$variables = array_merge($variables, $this->generateGlobalBackgroundVariables());
|
||||
$variables = array_merge($variables, $this->generateUserBackgroundVariables());
|
||||
|
||||
return $variables;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ use OCA\Theming\ITheme;
|
||||
class HighContrastTheme extends DefaultTheme implements ITheme {
|
||||
|
||||
public function getId(): string {
|
||||
return 'highcontrast';
|
||||
return 'light-highcontrast';
|
||||
}
|
||||
|
||||
public function getMediaQuery(): string {
|
||||
@@ -48,41 +48,52 @@ class HighContrastTheme extends DefaultTheme implements ITheme {
|
||||
return $this->l->t('A high contrast mode to ease your navigation. Visual quality will be reduced but clarity will be increased.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep this consistent with other HighContrast Themes
|
||||
*/
|
||||
public function getCSSVariables(): array {
|
||||
$variables = parent::getCSSVariables();
|
||||
$defaultVariables = parent::getCSSVariables();
|
||||
|
||||
$colorMainText = '#000000';
|
||||
$colorMainBackground = '#ffffff';
|
||||
$colorMainBackgroundRGB = join(',', $this->util->hexToRGB($colorMainBackground));
|
||||
|
||||
$variables['--color-main-background'] = $colorMainBackground;
|
||||
$variables['--color-main-background-translucent'] = 'rgba(var(--color-main-background-rgb), .1)';
|
||||
$variables['--color-main-text'] = $colorMainText;
|
||||
return array_merge(
|
||||
$defaultVariables,
|
||||
$this->generatePrimaryVariables($colorMainBackground, $colorMainText),
|
||||
[
|
||||
'--color-main-background' => $colorMainBackground,
|
||||
'--color-main-background-rgb' => $colorMainBackgroundRGB,
|
||||
'--color-main-background-translucent' => 'rgba(var(--color-main-background-rgb), 1)',
|
||||
'--color-main-text' => $colorMainText,
|
||||
|
||||
$variables['--color-background-dark'] = $this->util->darken($colorMainBackground, 30);
|
||||
$variables['--color-background-darker'] = $this->util->darken($colorMainBackground, 30);
|
||||
'--color-background-dark' => $this->util->darken($colorMainBackground, 30),
|
||||
'--color-background-darker' => $this->util->darken($colorMainBackground, 30),
|
||||
|
||||
$variables['--color-main-background-blur'] = $colorMainBackground;
|
||||
$variables['--filter-background-blur'] = 'none';
|
||||
'--color-main-background-blur' => $colorMainBackground,
|
||||
'--filter-background-blur' => 'none',
|
||||
|
||||
$variables['--color-placeholder-light'] = $this->util->darken($colorMainBackground, 30);
|
||||
$variables['--color-placeholder-dark'] = $this->util->darken($colorMainBackground, 45);
|
||||
'--color-placeholder-light' => $this->util->darken($colorMainBackground, 30),
|
||||
'--color-placeholder-dark' => $this->util->darken($colorMainBackground, 45),
|
||||
|
||||
$variables['--color-text-maxcontrast'] = 'var(--color-main-text)';
|
||||
$variables['--color-text-light'] = 'var(--color-main-text)';
|
||||
$variables['--color-text-lighter'] = 'var(--color-main-text)';
|
||||
'--color-text-maxcontrast' => $colorMainText,
|
||||
'--color-text-maxcontrast-background-blur' => $colorMainText,
|
||||
'--color-text-light' => $colorMainText,
|
||||
'--color-text-lighter' => $colorMainText,
|
||||
|
||||
$variables['--color-scrollbar'] = $this->util->darken($colorMainBackground, 25);
|
||||
'--color-scrollbar' => $this->util->darken($colorMainBackground, 25),
|
||||
|
||||
// used for the icon loading animation
|
||||
$variables['--color-loading-light'] = '#dddddd';
|
||||
$variables['--color-loading-dark'] = '#000000';
|
||||
// used for the icon loading animation
|
||||
'--color-loading-light' => '#dddddd',
|
||||
'--color-loading-dark' => '#000000',
|
||||
|
||||
$variables['--color-box-shadow-rgb'] = 'var(--color-main-text)';
|
||||
$variables['--color-box-shadow'] = 'var(--color-main-text)';
|
||||
'--color-box-shadow-rgb' => $colorMainText,
|
||||
'--color-box-shadow' => $colorMainText,
|
||||
|
||||
$variables['--color-border'] = $this->util->darken($colorMainBackground, 50);
|
||||
$variables['--color-border-dark'] = $this->util->darken($colorMainBackground, 50);
|
||||
|
||||
return $variables;
|
||||
'--color-border' => $this->util->darken($colorMainBackground, 50),
|
||||
'--color-border-dark' => $this->util->darken($colorMainBackground, 50),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function getCustomCss(): string {
|
||||
|
||||
@@ -40,6 +40,8 @@
|
||||
*/
|
||||
namespace OCA\Theming;
|
||||
|
||||
use OCA\Theming\AppInfo\Application;
|
||||
use OCA\Theming\Service\BackgroundService;
|
||||
use OCP\App\AppPathNotFoundException;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\Files\NotFoundException;
|
||||
@@ -49,47 +51,31 @@ use OCP\IConfig;
|
||||
use OCP\IL10N;
|
||||
use OCP\INavigationManager;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IUserSession;
|
||||
|
||||
class ThemingDefaults extends \OC_Defaults {
|
||||
|
||||
/** @var IConfig */
|
||||
private $config;
|
||||
/** @var IL10N */
|
||||
private $l;
|
||||
/** @var ImageManager */
|
||||
private $imageManager;
|
||||
/** @var IURLGenerator */
|
||||
private $urlGenerator;
|
||||
/** @var ICacheFactory */
|
||||
private $cacheFactory;
|
||||
/** @var Util */
|
||||
private $util;
|
||||
/** @var IAppManager */
|
||||
private $appManager;
|
||||
/** @var INavigationManager */
|
||||
private $navigationManager;
|
||||
private IConfig $config;
|
||||
private IL10N $l;
|
||||
private ImageManager $imageManager;
|
||||
private IUserSession $userSession;
|
||||
private IURLGenerator $urlGenerator;
|
||||
private ICacheFactory $cacheFactory;
|
||||
private Util $util;
|
||||
private IAppManager $appManager;
|
||||
private INavigationManager $navigationManager;
|
||||
|
||||
/** @var string */
|
||||
private $name;
|
||||
/** @var string */
|
||||
private $title;
|
||||
/** @var string */
|
||||
private $entity;
|
||||
/** @var string */
|
||||
private $productName;
|
||||
/** @var string */
|
||||
private $url;
|
||||
/** @var string */
|
||||
private $color;
|
||||
private string $name;
|
||||
private string $title;
|
||||
private string $entity;
|
||||
private string $productName;
|
||||
private string $url;
|
||||
private string $color;
|
||||
|
||||
/** @var string */
|
||||
private $iTunesAppId;
|
||||
/** @var string */
|
||||
private $iOSClientUrl;
|
||||
/** @var string */
|
||||
private $AndroidClientUrl;
|
||||
/** @var string */
|
||||
private $FDroidClientUrl;
|
||||
private string $iTunesAppId;
|
||||
private string $iOSClientUrl;
|
||||
private string $AndroidClientUrl;
|
||||
private string $FDroidClientUrl;
|
||||
|
||||
/**
|
||||
* ThemingDefaults constructor.
|
||||
@@ -97,6 +83,7 @@ class ThemingDefaults extends \OC_Defaults {
|
||||
* @param IConfig $config
|
||||
* @param IL10N $l
|
||||
* @param ImageManager $imageManager
|
||||
* @param IUserSession $userSession
|
||||
* @param IURLGenerator $urlGenerator
|
||||
* @param ICacheFactory $cacheFactory
|
||||
* @param Util $util
|
||||
@@ -104,6 +91,7 @@ class ThemingDefaults extends \OC_Defaults {
|
||||
*/
|
||||
public function __construct(IConfig $config,
|
||||
IL10N $l,
|
||||
IUserSession $userSession,
|
||||
IURLGenerator $urlGenerator,
|
||||
ICacheFactory $cacheFactory,
|
||||
Util $util,
|
||||
@@ -115,6 +103,7 @@ class ThemingDefaults extends \OC_Defaults {
|
||||
$this->config = $config;
|
||||
$this->l = $l;
|
||||
$this->imageManager = $imageManager;
|
||||
$this->userSession = $userSession;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->cacheFactory = $cacheFactory;
|
||||
$this->util = $util;
|
||||
@@ -225,11 +214,51 @@ class ThemingDefaults extends \OC_Defaults {
|
||||
|
||||
/**
|
||||
* Color that is used for the header as well as for mail headers
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getColorPrimary() {
|
||||
$color = $this->config->getAppValue('theming', 'color', $this->color);
|
||||
public function getColorPrimary(): string {
|
||||
$user = $this->userSession->getUser();
|
||||
|
||||
// admin-defined primary color
|
||||
$defaultColor = $this->getDefaultColorPrimary();
|
||||
|
||||
if ($this->isUserThemingDisabled()) {
|
||||
return $defaultColor;
|
||||
}
|
||||
|
||||
// user-defined primary color
|
||||
$themingBackground = '';
|
||||
if (!empty($user)) {
|
||||
$themingBackground = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'background', '');
|
||||
// If the user selected the default background
|
||||
if ($themingBackground === '') {
|
||||
return BackgroundService::DEFAULT_COLOR;
|
||||
}
|
||||
|
||||
// If the user selected a specific colour
|
||||
if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $themingBackground)) {
|
||||
return $themingBackground;
|
||||
}
|
||||
|
||||
// if the user-selected background is a background reference
|
||||
if (isset(BackgroundService::SHIPPED_BACKGROUNDS[$themingBackground]['primary_color'])) {
|
||||
return BackgroundService::SHIPPED_BACKGROUNDS[$themingBackground]['primary_color'];
|
||||
}
|
||||
}
|
||||
|
||||
// If the default color is not valid, return the default background one
|
||||
if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $defaultColor)) {
|
||||
return BackgroundService::DEFAULT_COLOR;
|
||||
}
|
||||
|
||||
// Finally, return the system global primary color
|
||||
return $defaultColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the default color primary
|
||||
*/
|
||||
public function getDefaultColorPrimary(): string {
|
||||
$color = $this->config->getAppValue(Application::APP_ID, 'color');
|
||||
if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $color)) {
|
||||
$color = '#0082c9';
|
||||
}
|
||||
@@ -408,7 +437,7 @@ class ThemingDefaults extends \OC_Defaults {
|
||||
/**
|
||||
* Increases the cache buster key
|
||||
*/
|
||||
private function increaseCacheBuster(): void {
|
||||
public function increaseCacheBuster(): void {
|
||||
$cacheBusterKey = (int)$this->config->getAppValue('theming', 'cachebuster', '0');
|
||||
$this->config->setAppValue('theming', 'cachebuster', (string)($cacheBusterKey + 1));
|
||||
$this->cacheFactory->createDistributed('theming-')->clear();
|
||||
@@ -469,4 +498,11 @@ class ThemingDefaults extends \OC_Defaults {
|
||||
public function getTextColorPrimary() {
|
||||
return $this->util->invertTextColor($this->getColorPrimary()) ? '#000000' : '#ffffff';
|
||||
}
|
||||
|
||||
/**
|
||||
* Has the admin disabled user customization
|
||||
*/
|
||||
public function isUserThemingDisabled(): bool {
|
||||
return $this->config->getAppValue('theming', 'disable-user-theming', 'no') === 'yes';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,17 +64,22 @@
|
||||
|
||||
<NcSettingsSection :title="t('theming', 'Background')"
|
||||
class="background">
|
||||
<p>{{ t('theming', 'Set a custom background') }}</p>
|
||||
<BackgroundSettings class="background__grid"
|
||||
:background="background"
|
||||
:theming-default-background="themingDefaultBackground"
|
||||
@update:background="updateBackground" />
|
||||
<template v-if="isUserThemingDisabled">
|
||||
<p>{{ t('theming', 'Customization has been disabled by your administrator') }}</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>{{ t('theming', 'Set a custom background') }}</p>
|
||||
<BackgroundSettings class="background__grid"
|
||||
:background="background"
|
||||
:theming-default-background="themingDefaultBackground"
|
||||
@update:background="updateBackground" />
|
||||
</template>
|
||||
</NcSettingsSection>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { generateOcsUrl, imagePath } from '@nextcloud/router'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import axios from '@nextcloud/axios'
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch'
|
||||
@@ -83,21 +88,19 @@ import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection'
|
||||
import BackgroundSettings from './components/BackgroundSettings.vue'
|
||||
import ItemPreview from './components/ItemPreview.vue'
|
||||
|
||||
import { getBackgroundUrl } from '../src/helpers/getBackgroundUrl.js'
|
||||
|
||||
const availableThemes = loadState('theming', 'themes', [])
|
||||
const enforceTheme = loadState('theming', 'enforceTheme', '')
|
||||
const shortcutsDisabled = loadState('theming', 'shortcutsDisabled', false)
|
||||
|
||||
const background = loadState('theming', 'background')
|
||||
const backgroundVersion = loadState('theming', 'backgroundVersion')
|
||||
const themingDefaultBackground = loadState('theming', 'themingDefaultBackground')
|
||||
const shippedBackgroundList = loadState('theming', 'shippedBackgrounds')
|
||||
const isUserThemingDisabled = loadState('theming', 'isUserThemingDisabled')
|
||||
|
||||
console.debug('Available themes', availableThemes)
|
||||
|
||||
export default {
|
||||
name: 'UserThemes',
|
||||
|
||||
components: {
|
||||
ItemPreview,
|
||||
NcCheckboxRadioSwitch,
|
||||
@@ -112,26 +115,15 @@ export default {
|
||||
shortcutsDisabled,
|
||||
background,
|
||||
themingDefaultBackground,
|
||||
isUserThemingDisabled,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
backgroundImage() {
|
||||
return getBackgroundUrl(this.background, backgroundVersion, this.themingDefaultBackground)
|
||||
},
|
||||
backgroundStyle() {
|
||||
if ((this.background === 'default' && this.themingDefaultBackground === 'backgroundColor')
|
||||
|| this.background.match(/#[0-9A-Fa-f]{6}/g)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
backgroundImage: this.background === 'default' ? 'var(--image-main-background)' : `url('${this.backgroundImage}')`,
|
||||
}
|
||||
},
|
||||
themes() {
|
||||
return this.availableThemes.filter(theme => theme.type === 1)
|
||||
},
|
||||
|
||||
fonts() {
|
||||
return this.availableThemes.filter(theme => theme.type === 2)
|
||||
},
|
||||
@@ -150,9 +142,11 @@ export default {
|
||||
.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',
|
||||
@@ -162,18 +156,16 @@ export default {
|
||||
.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">'
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.updateGlobalStyles()
|
||||
},
|
||||
|
||||
watch: {
|
||||
shortcutsDisabled(newState) {
|
||||
this.changeShortcutsDisabled(newState)
|
||||
@@ -183,34 +175,9 @@ export default {
|
||||
methods: {
|
||||
updateBackground(data) {
|
||||
this.background = (data.type === 'custom' || data.type === 'default') ? data.type : data.value
|
||||
this.updateGlobalStyles()
|
||||
this.$emit('update:background')
|
||||
},
|
||||
updateGlobalStyles() {
|
||||
// Override primary-invert-if-bright and color-primary-text if background is set
|
||||
const isBackgroundBright = shippedBackgroundList[this.background]?.theming === 'dark'
|
||||
if (isBackgroundBright) {
|
||||
document.querySelector('#header').style.setProperty('--primary-invert-if-bright', 'invert(100%)')
|
||||
document.querySelector('#header').style.setProperty('--color-primary-text', '#000000')
|
||||
// document.body.removeAttribute('data-theme-dark')
|
||||
// document.body.setAttribute('data-theme-light', 'true')
|
||||
} else {
|
||||
document.querySelector('#header').style.setProperty('--primary-invert-if-bright', 'no')
|
||||
document.querySelector('#header').style.setProperty('--color-primary-text', '#ffffff')
|
||||
// document.body.removeAttribute('data-theme-light')
|
||||
// document.body.setAttribute('data-theme-dark', 'true')
|
||||
}
|
||||
|
||||
const themeElements = [document.documentElement, document.querySelector('#header'), document.querySelector('body')]
|
||||
for (const element of themeElements) {
|
||||
if (this.background === 'default') {
|
||||
element.style.setProperty('--image-main-background', `url('${imagePath('core', 'app-background.jpg')}')`)
|
||||
} else if (this.background.match(/#[0-9A-Fa-f]{6}/g)) {
|
||||
element.style.setProperty('--image-main-background', undefined)
|
||||
} else {
|
||||
element.style.setProperty('--image-main-background', this.backgroundStyle.backgroundImage)
|
||||
}
|
||||
}
|
||||
},
|
||||
changeTheme({ enabled, id }) {
|
||||
// Reset selected and select new one
|
||||
this.themes.forEach(theme => {
|
||||
@@ -224,6 +191,7 @@ export default {
|
||||
this.updateBodyAttributes()
|
||||
this.selectItem(enabled, id)
|
||||
},
|
||||
|
||||
changeFont({ enabled, id }) {
|
||||
// Reset selected and select new one
|
||||
this.fonts.forEach(font => {
|
||||
|
||||
@@ -25,42 +25,67 @@
|
||||
|
||||
<template>
|
||||
<div class="background-selector">
|
||||
<!-- Custom background -->
|
||||
<button class="background filepicker"
|
||||
:class="{ active: background === 'custom' }"
|
||||
tabindex="0"
|
||||
@click="pickFile">
|
||||
{{ t('theming', 'Pick from Files') }}
|
||||
</button>
|
||||
|
||||
<!-- Default background -->
|
||||
<button class="background default"
|
||||
tabindex="0"
|
||||
:class="{ 'icon-loading': loading === 'default', active: background === 'default' }"
|
||||
@click="setDefault">
|
||||
{{ t('theming', 'Default image') }}
|
||||
</button>
|
||||
|
||||
<!-- Custom color picker -->
|
||||
<NcColorPicker v-model="Theming.color" @input="debouncePickColor">
|
||||
<button class="background color"
|
||||
:class="{ active: background === Theming.color}"
|
||||
tabindex="0"
|
||||
:data-color="Theming.color"
|
||||
:data-color-bright="invertTextColor(Theming.color)"
|
||||
:style="{ backgroundColor: Theming.color, color: invertTextColor(Theming.color) ? '#000000' : '#ffffff'}">
|
||||
{{ t('theming', 'Custom color') }}
|
||||
</button>
|
||||
</NcColorPicker>
|
||||
|
||||
<!-- Default admin primary color -->
|
||||
<button class="background color"
|
||||
:class="{ active: background === 'custom' }"
|
||||
:class="{ active: background === Theming.defaultColor }"
|
||||
tabindex="0"
|
||||
@click="pickColor">
|
||||
:data-color="Theming.defaultColor"
|
||||
:data-color-bright="invertTextColor(Theming.defaultColor)"
|
||||
:style="{ color: invertTextColor(Theming.defaultColor) ? '#000000' : '#ffffff'}"
|
||||
@click="debouncePickColor">
|
||||
{{ t('theming', 'Plain background') }}
|
||||
</button>
|
||||
|
||||
<!-- Background set selection -->
|
||||
<button v-for="shippedBackground in shippedBackgrounds"
|
||||
:key="shippedBackground.name"
|
||||
v-tooltip="shippedBackground.details.attribution"
|
||||
:class="{ 'icon-loading': loading === shippedBackground.name, active: background === shippedBackground.name }"
|
||||
tabindex="0"
|
||||
class="background"
|
||||
:data-color-bright="shippedBackground.details.theming === 'dark'"
|
||||
:style="{ 'background-image': 'url(' + shippedBackground.preview + ')' }"
|
||||
@click="setShipped(shippedBackground.name)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from '@nextcloud/axios'
|
||||
import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { getBackgroundUrl } from '../helpers/getBackgroundUrl.js'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { prefixWithBaseUrl } from '../helpers/prefixWithBaseUrl.js'
|
||||
import axios from '@nextcloud/axios'
|
||||
import debounce from 'debounce'
|
||||
import NcColorPicker from '@nextcloud/vue/dist/Components/NcColorPicker'
|
||||
import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip'
|
||||
|
||||
const shippedBackgroundList = loadState('theming', 'shippedBackgrounds')
|
||||
|
||||
@@ -69,6 +94,11 @@ export default {
|
||||
directives: {
|
||||
Tooltip,
|
||||
},
|
||||
|
||||
components: {
|
||||
NcColorPicker,
|
||||
},
|
||||
|
||||
props: {
|
||||
background: {
|
||||
type: String,
|
||||
@@ -79,12 +109,15 @@ export default {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
backgroundImage: generateUrl('/apps/theming/background') + '?v=' + Date.now(),
|
||||
loading: false,
|
||||
Theming: loadState('theming', 'data', {}),
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
shippedBackgrounds() {
|
||||
return Object.keys(shippedBackgroundList).map(fileName => {
|
||||
@@ -97,7 +130,39 @@ export default {
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Do we need to invert the text if color is too bright?
|
||||
*
|
||||
* @param {string} color the hex color
|
||||
*/
|
||||
invertTextColor(color) {
|
||||
return this.calculateLuma(color) > 0.6
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculate luminance of provided hex color
|
||||
*
|
||||
* @param {string} color the hex color
|
||||
*/
|
||||
calculateLuma(color) {
|
||||
const [red, green, blue] = this.hexToRGB(color)
|
||||
return (0.2126 * red + 0.7152 * green + 0.0722 * blue) / 255
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert hex color to RGB
|
||||
*
|
||||
* @param {string} hex the hex color
|
||||
*/
|
||||
hexToRGB(hex) {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
return result
|
||||
? [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)]
|
||||
: null
|
||||
},
|
||||
|
||||
async update(data) {
|
||||
const background = data.type === 'custom' || data.type === 'default' ? data.type : data.value
|
||||
this.backgroundImage = getBackgroundUrl(background, data.version, this.themingDefaultBackground)
|
||||
@@ -113,27 +178,35 @@ export default {
|
||||
}
|
||||
image.src = this.backgroundImage
|
||||
},
|
||||
|
||||
async setDefault() {
|
||||
this.loading = 'default'
|
||||
const result = await axios.post(generateUrl('/apps/theming/background/default'))
|
||||
this.update(result.data)
|
||||
},
|
||||
|
||||
async setShipped(shipped) {
|
||||
this.loading = shipped
|
||||
const result = await axios.post(generateUrl('/apps/theming/background/shipped'), { value: shipped })
|
||||
this.update(result.data)
|
||||
},
|
||||
|
||||
async setFile(path) {
|
||||
this.loading = 'custom'
|
||||
const result = await axios.post(generateUrl('/apps/theming/background/custom'), { value: path })
|
||||
this.update(result.data)
|
||||
},
|
||||
async pickColor() {
|
||||
|
||||
debouncePickColor: debounce(function() {
|
||||
this.pickColor(...arguments)
|
||||
}, 200),
|
||||
async pickColor(event) {
|
||||
this.loading = 'color'
|
||||
const color = OCA && OCA.Theming ? OCA.Theming.color : '#0082c9'
|
||||
const color = event?.target?.dataset?.color || this.Theming?.color || '#0082c9'
|
||||
const result = await axios.post(generateUrl('/apps/theming/background/color'), { value: color })
|
||||
this.update(result.data)
|
||||
},
|
||||
|
||||
pickFile() {
|
||||
window.OC.dialogs.filepicker(t('theming', 'Insert from {productName}', { productName: OC.theme.name }), (path, type) => {
|
||||
if (type === OC.dialogs.FILEPICKER_TYPE_CHOOSE) {
|
||||
@@ -171,7 +244,7 @@ export default {
|
||||
}
|
||||
|
||||
&.color {
|
||||
background-color: var(--color-primary);
|
||||
background-color: var(--color-primary-default);
|
||||
color: var(--color-primary-text);
|
||||
}
|
||||
|
||||
@@ -181,14 +254,20 @@ export default {
|
||||
border: 2px solid var(--color-primary);
|
||||
}
|
||||
|
||||
&.active:not(.icon-loading):after {
|
||||
background-image: var(--icon-checkmark-white);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 44px;
|
||||
content: '';
|
||||
display: block;
|
||||
height: 100%;
|
||||
&.active:not(.icon-loading) {
|
||||
&:after {
|
||||
background-image: var(--icon-checkmark-white);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 44px;
|
||||
content: '';
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&[data-color-bright]:after {
|
||||
background-image: var(--icon-checkmark-dark);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,6 @@
|
||||
*
|
||||
*/
|
||||
|
||||
// FIXME hoist this into a package? The same logic is used in `apps/dashboard/src/helpers/getBackgroundUrl.js`
|
||||
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { prefixWithBaseUrl } from './prefixWithBaseUrl.js'
|
||||
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
*
|
||||
*/
|
||||
|
||||
// FIXME hoist this into a package? The same logic is used in `apps/dashboard/src/helpers/prefixWithBaseUrl.js`
|
||||
|
||||
import { generateFilePath } from '@nextcloud/router'
|
||||
|
||||
export const prefixWithBaseUrl = (url) => generateFilePath('theming', '', 'img/background/') + url
|
||||
|
||||
@@ -30,3 +30,15 @@ Vue.prototype.t = t
|
||||
const View = Vue.extend(App)
|
||||
const theming = new View()
|
||||
theming.$mount('#theming')
|
||||
|
||||
theming.$on('update:background', () => {
|
||||
// Refresh server-side generated theming CSS
|
||||
[...document.head.querySelectorAll('link.theme')].forEach(theme => {
|
||||
const url = new URL(theme.href)
|
||||
url.searchParams.set('v', Date.now())
|
||||
const newTheme = theme.cloneNode()
|
||||
newTheme.href = url.toString()
|
||||
newTheme.onload = () => theme.remove()
|
||||
document.head.append(newTheme)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -81,7 +81,7 @@ style('theming', 'settings-admin');
|
||||
<form class="uploadButton" method="post" action="<?php p($_['uploadLogoRoute']) ?>" data-image-key="background">
|
||||
<input type="hidden" id="theming-backgroundMime" value="<?php p($_['images']['background']['mime']); ?>" />
|
||||
<input type="hidden" name="key" value="background" />
|
||||
<label for="upload-login-background"><span><?php p($l->t('Login image')) ?></span></label>
|
||||
<label for="upload-login-background"><span><?php p($l->t('Background and login image')) ?></span></label>
|
||||
<input id="upload-login-background" class="fileupload" name="image" type="file">
|
||||
<label for="upload-login-background" class="button icon-upload svg" id="upload-login-background" title="<?php p($l->t("Upload new login background")) ?>"></label>
|
||||
<div data-setting="backgroundMime" data-toggle="tooltip" data-original-title="<?php p($l->t('Reset to default')); ?>" class="theme-undo icon icon-history"></div>
|
||||
@@ -93,7 +93,6 @@ style('theming', 'settings-admin');
|
||||
</div>
|
||||
|
||||
<h3 class="inlineblock"><?php p($l->t('Advanced options')); ?></h3>
|
||||
|
||||
<div class="advanced-options">
|
||||
<div>
|
||||
<label>
|
||||
@@ -131,6 +130,16 @@ style('theming', 'settings-admin');
|
||||
<div data-setting="faviconMime" data-toggle="tooltip" data-original-title="<?php p($l->t('Reset to default')); ?>" class="theme-undo icon icon-history"></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="advanced-options" id="user-theming">
|
||||
<label><span><?php p($l->t('User settings')); ?></span></label>
|
||||
<div>
|
||||
<p class="info">
|
||||
<?php p($l->t('Although you can select and customize your instance, users can change their background and colors. If you want to enforce your customization, you can check this box.')); ?>
|
||||
</p>
|
||||
<input id="userThemingDisabled" class="checkbox" type="checkbox" <?php p($_['userThemingDisabled'] ? 'checked="checked"' : ''); ?> />
|
||||
<label for="userThemingDisabled"><?php p($l->t('Disable user theming')) ?></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="theming-hints">
|
||||
|
||||
@@ -33,6 +33,7 @@ use OCA\Theming\Themes\DyslexiaFont;
|
||||
use OCA\Theming\Themes\HighContrastTheme;
|
||||
use OCA\Theming\Service\ThemesService;
|
||||
use OCA\Theming\Themes\LightTheme;
|
||||
use OCA\Theming\ThemingDefaults;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\OCS\OCSBadRequestException;
|
||||
use OCP\IConfig;
|
||||
@@ -54,6 +55,8 @@ class UserThemeControllerTest extends TestCase {
|
||||
private $userSession;
|
||||
/** @var ThemeService|MockObject */
|
||||
private $themesService;
|
||||
/** @var ThemingDefaults */
|
||||
private $themingDefaults;
|
||||
/** @var BackgroundService|MockObject */
|
||||
private $backgroundService;
|
||||
|
||||
@@ -66,13 +69,14 @@ class UserThemeControllerTest extends TestCase {
|
||||
$this->config = $this->createMock(IConfig::class);
|
||||
$this->userSession = $this->createMock(IUserSession::class);
|
||||
$this->themesService = $this->createMock(ThemesService::class);
|
||||
$this->themingDefaults = $this->createMock(ThemingDefaults::class);
|
||||
$this->backgroundService = $this->createMock(BackgroundService::class);
|
||||
|
||||
$this->themes = [
|
||||
'default' => $this->createMock(DefaultTheme::class),
|
||||
'light' => $this->createMock(LightTheme::class),
|
||||
'dark' => $this->createMock(DarkTheme::class),
|
||||
'highcontrast' => $this->createMock(HighContrastTheme::class),
|
||||
'light-highcontrast' => $this->createMock(HighContrastTheme::class),
|
||||
'dark-highcontrast' => $this->createMock(DarkHighContrastTheme::class),
|
||||
'opendyslexic' => $this->createMock(DyslexiaFont::class),
|
||||
];
|
||||
@@ -91,6 +95,7 @@ class UserThemeControllerTest extends TestCase {
|
||||
$this->config,
|
||||
$this->userSession,
|
||||
$this->themesService,
|
||||
$this->themingDefaults,
|
||||
$this->backgroundService,
|
||||
);
|
||||
|
||||
@@ -102,7 +107,7 @@ class UserThemeControllerTest extends TestCase {
|
||||
['default'],
|
||||
['light'],
|
||||
['dark'],
|
||||
['highcontrast'],
|
||||
['light-highcontrast'],
|
||||
['dark-highcontrast'],
|
||||
['opendyslexic'],
|
||||
['', OCSBadRequestException::class],
|
||||
|
||||