Compare commits

..

1 Commits

Author SHA1 Message Date
Josh Richards 45da3915c0 fix(SetupChecks): Add reasoning/clarity to outdated PHP check
Signed-off-by: Josh Richards <josh.t.richards@gmail.com>
2024-05-12 11:13:06 -04:00
1225 changed files with 14605 additions and 11862 deletions
+1 -6
View File
@@ -2,9 +2,6 @@
kind: pipeline
name: litmus
# SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
steps:
- name: submodules
image: ghcr.io/nextcloud/continuous-integration-alpine-git:latest
@@ -127,6 +124,4 @@ trigger:
---
kind: signature
hmac: 06ddea3f1885983230fcc996e805245357ac90e39599ed11a70161a7c09746d7
...
hmac: 1c487e85d1dba3fec3151868f8f94fc46b4ecb0f821c35516c193700cdbc2a9c
-2
View File
@@ -1,5 +1,3 @@
# SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
# Ignoring folders for eslint
node_modules/
3rdparty/
-4
View File
@@ -1,7 +1,3 @@
/**
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
module.exports = {
globals: {
__webpack_nonce__: true,
-4
View File
@@ -1,7 +1,3 @@
<!--
- SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
## Submitting issues
If you have questions about how to install or use Nextcloud, please direct these to our [forum][forum]. We are also available on [IRC][irc].
-2
View File
@@ -1,3 +1 @@
# SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
custom: https://nextcloud.com/include/
-2
View File
@@ -1,5 +1,3 @@
# SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
name: "🐛 Bug report: Nextcloud Server"
description: "Submit a report and help us improve Nextcloud Server"
title: "[Bug]: "
@@ -1,2 +0,0 @@
SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
SPDX-License-Identifier: AGPL-3.0-or-later
-2
View File
@@ -1,5 +1,3 @@
# SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
contact_links:
- name: 🚨 Report a security or privacy issue
url: https://hackerone.com/nextcloud
-2
View File
@@ -1,5 +1,3 @@
# SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
firstPRMergeComment: >
Thanks for your first pull request and welcome to the community!
Feel free to keep them coming! If you are looking for issues to tackle then have a look at this selection: https://github.com/nextcloud/server/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22
-2
View File
@@ -1,5 +1,3 @@
# SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
version: 2
updates:
# Linting and coding style
-2
View File
@@ -1,2 +0,0 @@
SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
SPDX-License-Identifier: AGPL-3.0-or-later
+3 -3
View File
@@ -53,11 +53,11 @@ jobs:
strategy:
matrix:
php-versions: ['8.1']
# To keep the matrix smaller we ignore PostgreSQL '13', '14', and '15' as we already test 12 and 16 as lower and upper bound
postgres-versions: ['12', '16']
# To keep the matrix smaller we ignore PostgreSQL '11', '13', '14' as we already test 10 and 15 as lower and upper bound
postgres-versions: ['10', '15', '16']
include:
- php-versions: '8.3'
postgres-versions: '16'
postgres-versions: '15'
coverage: ${{ github.event_name != 'pull_request' }}
name: PostgreSQL ${{ matrix.postgres-versions }} (PHP ${{ matrix.php-versions }}) - database tests
-4
View File
@@ -1,7 +1,3 @@
# SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
# SPDX-FileCopyrightText: 2011-2016 ownCloud contributors
# SPDX-FileCopyrightText: 2010 ownCloud contributors
# SPDX-License-Identifier: AGPL-3.0-only
# the default generated dir + db file
/data
/config/config.php
+1 -4
View File
@@ -1,10 +1,7 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
require_once './vendor-bin/cs-fixer/vendor/autoload.php';
use Nextcloud\CodingStandard\Config;
-2
View File
@@ -1,7 +1,5 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
# SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
+2 -106
View File
@@ -7,62 +7,6 @@ Files: lib/l10n/*.js lib/l10n/*.json core/l10n/*.js core/l10n/*.json apps/admin_
Copyright: 2016 ownCloud, Inc., 2016-2024 Nextcloud translators
License: AGPL-3.0-only OR AGPL-3.0-or-later
Files: tests/data/block-aligned-plus-one.txt tests/data/block-aligned.txt tests/data/data.tar.gz tests/data/data.zip tests/data/desktopapp.png tests/data/desktopapp.svg tests/data/certificates/badCertificate.crt tests/data/certificates/expiredCertificate.crt tests/data/certificates/goodCertificate.crt tests/data/integritycheck/app/AnotherFile.txt tests/data/integritycheck/app/subfolder/file.txt tests/data/integritycheck/appWithInvalidData/AnotherFile.txt tests/data/integritycheck/appWithInvalidData/UnecessaryFile
Copyright: 2015 ownCloud, Inc.
License: AGPL-3.0-only
Files: tests/data/integritycheck/htaccessUnmodified/.htaccess tests/data/integritycheck/htaccessUnmodified/subfolder/.htaccess tests/data/integritycheck/htaccessWithInvalidModifiedContent/.htaccess tests/data/integritycheck/htaccessWithValidModifiedContent/subfolder/.htaccess
Copyright: 2016 ownCloud, Inc.
License: AGPL-3.0-only
Files: tests/data/l10n/cs.json tests/data/l10n/de.json tests/data/l10n/ru.json
Copyright: 2015 ownCloud, Inc., 2021 Nextcloud GmbH and Nextcloud contributors
License: AGPL-3.0-only
Files: tests/data/lorem.txt
Copyright: 2012 ownCloud, Inc.
License: AGPL-3.0-only
Files: tests/data/testavatar.png tests/data/testimage.gif tests/data/testimage.jpg tests/data/testimage.png
Copyright: 2013 ownCloud, Inc.
License: AGPL-3.0-only
Files: tests/data/lorem-big.txt tests/data/strängé*filename*(duplicate*#2).txt tests/data/strõngÚ*filename*(duplicate*#2).txt
Copyright: 2014 ownCloud, Inc.
License: AGPL-3.0-only
Files: tests/data/testimagelarge.svg
Copyright: 2015 ownCloud, Inc., 2016 Nextcloud GmbH and Nextcloud contributors
License: AGPL-3.0-only
Files: tests/data/setUploadLimit/htaccess tests/data/setUploadLimit/user.ini
Copyright: 2015 ownCloud, Inc., 2018 Nextcloud GmbH and Nextcloud contributors
License: AGPL-3.0-only
Files: tests/data/testimage-wide.png tests/data/testimage.eps tests/data/testimage.mp3 tests/data/testimage.mp4 tests/data/testimage.odt
Copyright: 2015 ownCloud, Inc.
License: AGPL-3.0-only
Files: tests/data/testapp.zip tests/data/testapp2.zip tests/docker/mariadb/oc.cnf
Copyright: 2016 ownCloud, Inc.
License: AGPL-3.0-only
Files: tests/docker/mysqlmb4/mb4.cnf
Copyright: 2016 ownCloud, Inc., 2017 Nextcloud GmbH and Nextcloud contributors
License: AGPL-3.0-only
Files: tests/data/integritycheck/htaccessWithValidModifiedContent/.htaccess
Copyright: 2016 ownCloud, Inc., 2019 Nextcloud GmbH and Nextcloud contributors
License: AGPL-3.0-only
Files: core/img/favicon*.* core/img/logo/logo*.* tests/data/testimage.webp
Copyright: 2016-2024 Nextcloud GmbH
License: LicenseRef-NextcloudTrademarks
Files: core/css/*.css core/css/*.css.map
Copyright: 2016-2024 Nextcloud GmbH and Nextcloud contributors
License: AGPL-3.0-or-later
Files: build/psalm-baseline-ocp.xml build/psalm-baseline.xml build/stubs/xsl.php build/stubs/gd.php build/stubs/imagick.php build/stubs/intl.php build/stubs/IntlChar.php build/stubs/ldap.php build/stubs/memcached.php build/stubs/redis.php build/stubs/redis_cluster.php build/stubs/sftp.php build/stubs/ssh2.php build/stubs/apcu.php
Copyright: 2020 Nextcloud GmbH and Nextcloud contributors
License: AGPL-3.0-or-later
@@ -71,35 +15,11 @@ Files: build/stubs/pcntl.php build/stubs/zip.php
Copyright: 2022 JetBrains
License: Apache-2.0
Files: core/js/mimetypelist.js core/js/core.json themes/example/core/img
Copyright: 2016 ownCloud, Inc., 2016-2024 Nextcloud GmbH and Nextcloud contributors
License: AGPL-3.0-only
Files: tests/data/testapp.0.8.tar.gz tests/data/testapp.tar.gz tests/data/testapp1.tar.gz
Copyright: 2016 Nextcloud GmbH and Nextcloud contributors
License: AGPL-3.0-or-later
Files: core/js/merged-template-prepend.json tests/data/certificates/goodCertificate.crt tests/data/emails/new-account-email-custom-text-alternative.txt tests/data/emails/new-account-email-custom.html tests/data/emails/new-account-email-custom.txt tests/data/emails/new-account-email-single-button.html tests/data/emails/new-account-email-single-button.txt tests/data/emails/new-account-email.html tests/data/emails/new-account-email.txt
Copyright: 2017 Nextcloud GmbH and Nextcloud contributors
License: AGPL-3.0-or-later
Files: core/src/systemtags/templates/result.handlebars core/src/systemtags/templates/result_form.handlebars core/src/systemtags/templates/selection.handlebars tests/data/testimage.heic
Copyright: 2018 Nextcloud GmbH and Nextcloud contributors
License: AGPL-3.0-or-later
Files: core/src/jquery/contactsmenu/jquery_entry.handlebars tests/data/integritycheck/mimetypeListModified/core/js/mimetypelist.js tests/data/integritycheck/mimetypeListModified/core/signature.json tests/data/guest_avatar_einstein_32.png tests/data/guest_avatar_einstein_32.svg tests/data/test.pdf
Copyright: 2019 Nextcloud GmbH and Nextcloud contributors
License: AGPL-3.0-or-later
Files: tests/data/testimage-badheader.jpg
Copyright: 2021 Nextcloud GmbH and Nextcloud contributors
License: AGPL-3.0-or-later
Files: build/stubs/ftp.php tests/data/integritycheck/mimetypeListModified/coresignature.json
Files: build/stubs/ftp.php
Copyright: 2022 Nextcloud GmbH and Nextcloud contributors
License: AGPL-3.0-or-later
Files: build/stubs/app_api.php build/stubs/SensitiveParameter.phpstub build/stubs/psr_container.php tests/data/testimage-large.jpg
Files: build/stubs/app_api.php build/stubs/SensitiveParameter.phpstub build/stubs/psr_container.php
Copyright: 2023 Nextcloud GmbH and Nextcloud contributors
License: AGPL-3.0-or-later
@@ -110,27 +30,3 @@ License: AGPL-3.0-or-later
Files: .gitattributes composer.json composer.lock .github/CODEOWNERS __tests__/tsconfig.json tsconfig.json build/integration/composer.* build/integration/data/*.png
Copyright: 2011-2016 ownCloud, Inc., 2016-2024 Nextcloud GmbH and Nextcloud contributors
License: AGPL-3.0-only OR AGPL-3.0-or-later
Files: core/img/twitter.svg
Copyright: X Corp.
License: LicenseRef-XTrademarks
Files: core/img/facebook.svg
Copyright: 2024 Meta
License: LicenseRef-FacebookTrademarks
Files: core/img/mastodon.svg
Copyright: 2024 Mastodon gGmbH
License: LicenseRef-MastodonTrademarks
Files: core/img/googleplay.png
Copyright: 2024 Google LLC.
License: LicenseRef-GooglePlayBadge
Files: core/img/appstore.svg
Copyright: 2024 Apple Inc.
License: LicenseRef-AppleAppStoreBadge
Files: core/img/fdroid.svg
Copyright: 2016 Andrew Nayenko <relan@airpost.net>
License: CC-BY-SA-3.0 OR GPL-3.0-or-later
-3
View File
@@ -1,6 +1,3 @@
# SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
# SPDX-FileCopyrightText: 2016 ownCloud, Inc.
# SPDX-License-Identifier: AGPL-3.0-only
before_commands:
- 'git submodule update --init --recursive'
+1 -9
View File
@@ -28,7 +28,6 @@
- Andreas Fischer <bantu@owncloud.com>
- Andreas Pflug <dev@admin4.org>
- Andrew Brown <andrew@casabrown.com>
- Andrey Borysenko <andrey.borysenko@nextcloud.com>
- Andy Xheli <axheli@axtsolutions.com>
- Anna Larch <anna@nextcloud.com>
- ArcticFall <23174635+ArcticFall@users.noreply.github.com>
@@ -113,7 +112,7 @@
- David <37280718+yeyulantu@users.noreply.github.com>
- David Prévot <taffit@debian.org>
- David Toledo <dtoledo@solidgear.es>
- Denis MosolovDenis Mosolov <denismosolov@gmail.com>
- Denis Mosolov <denismosolov@gmail.com>
- Derek <derek.kelly27@gmail.com>
- Dominik Schmidt <dev@dominik-schmidt.de>
- Donquixote <marjunebatac@gmail.com>
@@ -137,7 +136,6 @@
- Florent <florent@coppint.com>
- Florian Schunk <florian.schunk@rwth-aachen.de>
- Florin Peter <github@florin-peter.de>
- Fon E. Noel NFEBE <opensource@nfebe.com>
- Frank Karlitschek <frank@karlitschek.de>
- François Freitag <mail@franek.fr>
- François Kubler <francois@kubler.org>
@@ -203,7 +201,6 @@
- Jos Poortvliet <jos@opensuse.org>
- Jose Quinteiro <github@quinteiro.org>
- Josh Richards <josh.t.richards@gmail.com>
- Joshua Trees <me@jtrees.io>
- Juan Pablo Villafañez <jvillafanez@solidgear.es>
- Juan Pablo Villafáñez <jvillafanez@solidgear.es>
- Julien Lutran <julien.lutran@corp.ovh.com>
@@ -224,7 +221,6 @@
- Klaas Freitag <freitag@owncloud.com>
- Knut Ahlers <knut@ahlers.me>
- Ko- <k.stoffelen@cs.ru.nl>
- Konrad Abicht <hi@inspirito.de>
- Konrad Bucheli <kb@open.ch>
- Kristof Provost <github@sigsegv.be>
- Kyle Fazzari <kyrofa@ubuntu.com>
@@ -246,7 +242,6 @@
- Louis <6653109+artonge@users.noreply.github.com>
- Louis Chemineau <louis@chmn.me>
- Loïc Hermann <loic.hermann@sciam.fr>
- Lucas Azevedo <lhs_azevedo@hotmail.com>
- Luka Trovic <luka@nextcloud.com>
- Lukas Reschke <lukas@statuscode.ch>
- Lukas Stabe <lukas@stabe.de>
@@ -312,7 +307,6 @@
- Mohammed Abdellatif <m.latief@gmail.com>
- Morris Jobke <hey@morrisjobke.de>
- Mátyás Jani <jzombi@gmail.com>
- nacho <nacho@ownyourbits.com>
- Naoto Kobayashi <naoto.kobayashi4c@gmail.com>
- Nazar Mokrynskyi <nazar@mokrynskyi.com>
- Nicolai Ehemann <en@enlightened.de>
@@ -340,7 +334,6 @@
- Peter Kubica <peter@kubica.ch>
- Petre T <petre.tudor@dorkfarm.com>
- Phil Davis <phil.davis@inf.org>
- Philip Gatzka <philip.gatzka@mailbox.org>
- Philipp Kapfer <philipp.kapfer@gmx.at>
- Philipp Schaffrath <github@philipp.schaffrath.email>
- Philipp Staiger <philipp@staiger.it>
@@ -408,7 +401,6 @@
- Sergey Shliakhov <husband.sergey@gmail.com>
- Sergio Bertolin <sbertolin@solidgear.es>
- Sergio Bertolín <sbertolin@solidgear.es>
- Serhii Shliakhov <shlyakhov.up@gmail.com>
- Sijmen Schoon <me@sijmenschoon.nl>
- Simon Könnecke <simonkoennecke@gmail.com>
- Simon L <szaimen@e.mail.de>
+1 -1
View File
@@ -18,4 +18,4 @@ Licensing of components:
All unmodified files from these and other sources retain their original copyright
and license notices: see the relevant individual files.
Attribution information for Nextcloud is contained in the `AUTHORS` file.
Attribution information for Nextcloud is contained in the `AUTHORS.md` file.
@@ -1,46 +0,0 @@
App Store Badges
Include App Store badges in all digital and printed marketing materials as a clear call to action to get your app. App Store badges are available in 40 localizations to help you reach a broader audience. Versions are available for the App Store for iPhone and iPad, the Mac App Store, and Apple TV.
Preferred Badges
Use the preferred black badge in all marketing communications promoting your app. The gray border surrounding the black badge is part of the badge artwork and should not be modified. Whenever one or more badges for other app platforms appear in the layout, use the preferred black badge. Place the App Store badge first in the lineup of badges.
Legal Requirements
Trademark Symbols
In communications distributed only in the United States, the appropriate symbol (™, ℠, or ®) must follow each Apple trademark the first time it is mentioned in body copy. Do not use trademark symbols on products, product documentation, or other product communications that will be distributed outside the United States.
For example, use Apple Watch®, iPhone®, iPad®, iPod touch®, Apple TV®, App Store®, Mac App Store℠, Mac®, MacBook Pro®, MacBook Air®, and iMac®.
Dont add symbols to headline copy or to the App Store badge artwork provided by Apple.
For the correct trademark symbols, refer to the Apple Trademark List.
Credit Lines
Use the appropriate credit lines in all communications worldwide, listing all the Apple trademarks and products included in your communication and advertising. Include the credit lines only once in your communication or website, and place the credit lines wherever you provide legal notification. Follow standard practices for the placement of legal copy, such as creating additional screens or providing interactive links. When the App Store badge is used, credit both Apple and the Apple Logo.
Refer to the Apple Trademark List for the correct trademark symbol, spelling of the trademark, and generic term to use with the trademark. Generally, the symbol appears at the right shoulder of the trademark (except the Apple Logo, where the logo appears at the right foot).
Use the following formats for distribution within the United States only:
______ and ______ are registered trademarks of Apple Inc.
______ and ______ are trademarks of Apple Inc.
For distribution outside the United States, use one of the following international credit notices:
______ and______ are trademarks of Apple Inc., registered in the U.S. and other countries.
______ and______ are trademarks of Apple Inc.
A translation of the legal notice and credit lines (but not the trademarks) can be used in materials distributed outside the U.S.
For more information on using Apple trademarks, see Using Apple Trademarks and Copyrights.
Association with Apple
Your app screen images, Mac, Apple Watch, iPhone, iPad, iPod touch, and Apple TV product images, or photographs thereof cannot be used in any manner that falsely suggests an association with Apple or is likely to reduce, diminish, or damage the goodwill, value, or reputation associated with the App Store, the Mac App Store, iPhone, iPad, iPod touch, Apple Watch, Apple TV, or Apple itself.
see https://developer.apple.com/app-store/marketing/guidelines/
@@ -1,17 +0,0 @@
Facebook brand resources and guidelines
The logo is our most important brand asset. We use it with consistency and intention to represent the world of social discovery to billions of people around the world.
Our Logo Evolution
Our logo has evolved over time, building on our past heritage, using an 'f' within a blue circle. The current logo has been refined in shape and color to make it more accessible and legible. Always ensure you are using the current version of our logo in any new designs or communications.
Legal
Meta dedicates substantial resources to the development and protection of its intellectual property. In addition to seeking registration of its trademarks and logos around the world, Meta enforces its rights against people who misuse its trademarks.
Meta's trademarks are owned by Meta and may only be used as provided in these guidelines or with Metas permission. A list of some of Metas trademarks can be found here (https://about.meta.com/brand/resources/meta/our-trademarks/). You may not use or register, or otherwise claim rights in any Meta trademark, including as or as part of any trademark, service mark, company name, trade name, username or domain registration. You should not use or claim rights in any trademark in a way that is confusingly similar to or dilutive of Metas trademarks, including as, or as any part of, a trademark. Do not use Meta's trademarks for anything that would be inconsistent with Metas Terms of Service (https://www.facebook.com/terms.php) or Community Standards (https://transparency.meta.com/policies/community-standards/).
We may revoke permission to use Metas trademarks at any time. Meta reserves the right to withhold approval of content that it considers inconsistent with the Meta brand.
A copy can be found at https://about.meta.com/brand/resources/facebook/logo/
-23
View File
@@ -1,23 +0,0 @@
badge guidelines
Dos & Don'ts
Follow these guidelines whenever you are using a Google Play badge.
* Use the badges as provided. Never alter the badges.
* There must be clear space surrounding the badge equal to one-quarter the height of the badge.
* The badge must be large enough that all of the text is legible.
* The Google Play badge should be the same size or larger than other application store badges.
* Badges must be shown on a solid colored background or a simple background image that does not obscure the badge.
* Match the badge language to the language of your marketing campaign whenever possible.
* Any online use of the badge must link to the Google Play store. Use the generator below to get the HTML to include in your digital marketing.
* The badge can only be used to promote content available on Google Play.
* Include the appropriate Google Play legal attribution when there is space in the creative. See the badge generator for localized legal attributions.
* Use of the Google Play badge must be reviewed and approved by the Google Play Partner Brand team if the badge will be in:
* a TV commercial
* an out-of-home marketing campaign
* a marketing campaign that will receive over 1 million impressions
* Use this form (https://partnermarketinghub.withgoogle.com/login/?next=/submit-assets/) to submit your marketing for approval.
see https://play.google.com/intl/en_us/badges/
Google Play and the Google Play logo are trademarks of Google LLC.
@@ -1,46 +0,0 @@
Trademark Policy
Last updated December 21, 2022
The Mastodon name and logos are trademarks of Mastodon gGmbH. As such, their use is restricted and protected by intellectual property law. While the software we create is available under a free and open source software license, the copyright license does not include an implied right or license to use our trademarks.
The role of trademarks is to prevent the exploitation of the good name and reputation of Mastodon by other people and organizations, and to provide assurance about the quality of the products and services associated with it.
To use our trademarks beyond what is considered "fair" or "nominative" use, you must follow these guidelines. By making use of our trademarks, you agree to abide by the following terms and conditions. You further agree that any dispute arising in connection with your use of our trademarks or under these terms and conditions shall be under the exclusive jurisdiction of the state and federal courts of New York in the United States of America and that the state and federal courts of New York shall have personal jurisdiction over you for the purposes of adjudicating any dispute concerning the use of our trademarks or these terms and conditions.
You agree to defend and indemnify Mastodon gGmbH from and against any and all claims and losses brought by a third party in connection with your use of the Mastodon trademarks.
To request the use of the Mastodon name and logos in a way not covered in these guidelines, or to report violations, please contact us at trademark@joinmastodon.org. In the event that we do not approve such use of the Mastodon name and logos within ten (10) business days, your request shall be deemed denied.
General guidelines
In general:
* Only use the Mastodon marks to accurately identify those goods or services that are built using the Mastodon software.
* Do not use the Mastodon marks in any way that could mistakenly imply that Mastodon GmbH has reviewed, approved, or guaranteed your goods or services.
* Do not use or register, in whole or in part, the Mastodon marks as part of your own or any other trademark, service mark, domain name, company name, trade name, product name, or service name.
* Do not use the Mastodon marks in a manner that disparages or defames the marks, Mastodon gGmbH, or Mastodons products.
* Do not use the Mastodon marks in connection with any illegal activity.
* You may use the Mastodon word mark in referential phrases such as "for", "for use with", or "compatible with".
* You may use the Mastodon marks when embedding or otherwise displaying user generated content published using the Mastodon software.
* Do not change or modify the Mastodon marks.
* Any all use of the Mastodon marks, and any goodwill accrued as a result of that use, belongs entirely to, and shall inure for the benefit of, Mastodon gGmbH.
Server guidelines
If you run your own Mastodon server using the Mastodon software, including modified Mastodon software on the condition that the modifications are limited to switching on or off features already included in the software, minor tweaks in visual appearance, translations into other languages, and bug fixes:
* You may not use the Mastodon word mark, or any similar mark, in your domain name, unless you have written permission from Mastodon gGmbH.
* As long as you abide by the terms and conditions of this agreement, you may use the Mastodon marks included in the Mastodon server software for the purposes of running the server.
Open source project guidelines
If you choose to build on or modify Mastodon's open-source code, beyond modifications limited to switching on or off features already included in the software, minor tweaks in visual appearance, translations into other languages, and bug fixes:
* You must choose your own branding, logos, and trademarks that denote your unique identity so as to clearly signal to users that there is no affiliation with or endorsement by Mastodon gGmbH.
* You may use word marks, but not our logos, in truthful statements that describe the relationship between your software and ours, for example "this software is derived from the source code of the Mastodon software".
Social media guidelines
The name and handle of your social media account and any and all pages cannot begin with a Mastodon word mark, or a similar mark (e.g. "mastodoon", "mast0don", "mstdn"). In addition, Mastodon logos cannot be used in a way that might suggest affiliation with or endorsement by Mastodon.
A copy can be found at https://joinmastodon.org/de/trademark
@@ -1,9 +0,0 @@
The Nextcloud marks
Nextcloud and the Nextcloud logo is a registered trademark of Nextcloud GmbH in Germany and/or other countries.
These guidelines cover the following marks pertaining both to the product names and the logo: “Nextcloud”
and the blue/white cloud logo with or without the word Nextcloud; the service “Nextcloud Enterprise”;
and our products: “Nextcloud Files”; “Nextcloud Groupware” and “Nextcloud Talk”.
This set of marks is collectively referred to as the “Nextcloud marks.”
Use of Nextcloud logos and other marks is only permitted under the guidelines provided by the Nextcloud GmbH.
A copy can be found at https://discord.com/branding
-49
View File
@@ -1,49 +0,0 @@
Trademark policy
April 2023
You may not violate others intellectual property rights, including copyright and trademark.
A trademark is a word, logo, phrase, or device that distinguishes a trademark holders good or service in the marketplace. Trademark law may prevent others from using a trademark in an unauthorized or confusing manner.
What is in violation of this policy?
Using anothers trademark in a way that may mislead or confuse people about your affiliation may be a violation of our trademark policy.
What is not a violation of this policy?
Referencing anothers trademark is not automatically a violation of X's trademark policy. Examples of non-violations include:
* using a trademark in a way that is outside the scope of the trademark registration e.g., in a different territory, or a different class of goods or services than that identified in the registration; and
* using a trademark in a nominative or other fair use manner. For more information, see our Misleading and deceptive identities policy (https://help.twitter.com/en/rules-and-policies/twitter-impersonation-and-deceptive-identities-policy.html).
Who can report violations of this policy?
X only investigates requests that are submitted by the trademark holder or their authorized representative e.g., a legal representative or other representative for a brand.
How can I report violations of this policy?
You can submit a trademark report through our trademark report form (https://help.twitter.com/forms/trademark). Please provide all the information requested in the form. If you submit an incomplete report, well need to follow up about the missing information. Please note that this will result in a delay in processing your report.
Note: We may provide the account holder with your name and other information included in the copy of the report.
What happens if you violate this policy?
If we determine that you violated our trademark policy, we may suspend your account. Depending on the type of violation, we may give you an opportunity to comply with our policies. In other instances, an account may be permanently suspended upon first review. If you believe that your account was suspended in error, you can submit an appeal (https://help.twitter.com/forms/general?subtopic=suspended).
Additional resources
Learn more about our range of enforcement options (https://help.twitter.com/rules-and-policies/enforcement-options) and our approach to policy development and enforcement (https://help.twitter.com/rules-and-policies/enforcement-philosophy).
Legal disclaimer
By using the X trademarks and resources on this site, you agree to follow the X Trademark Guidelines in our Brand Guidelines — as well as our Terms of Service and all other X rules and policies. If you have any questions, contact us at trademarks@x.com.
A copy can be found at https://about.x.com/en/who-we-are/brand-toolkit and https://help.twitter.com/en/rules-and-policies/x-trademark-policy
+19 -2
View File
@@ -1,6 +1,23 @@
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @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/>.
*
*/
export const getCurrentUser = function() {
return {
+19 -2
View File
@@ -1,6 +1,23 @@
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @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/>.
*
*/
export default {
get: async () => ({ status: 200, data: {} }),
+19 -2
View File
@@ -1,6 +1,23 @@
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @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 { jest } from '@jest/globals'
+19 -2
View File
@@ -1,6 +1,23 @@
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @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/>.
*
*/
export const loadState = function(app: string, key: string, fallback?: any) {
+19 -2
View File
@@ -1,5 +1,22 @@
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
* @copyright Copyright (c) 2023 Lucas Azevedo <lhs_azevedo@hotmail.com>
*
* @author Lucas Azevedo <lhs_azevedo@hotmail.com>
*
* @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/>.
*
*/
export default {}
+19 -2
View File
@@ -1,5 +1,22 @@
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @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/>.
*
*/
export default '<svg>SvgMock</svg>'
+19 -2
View File
@@ -1,6 +1,23 @@
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @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/>.
*
*/
export const createClient = () => {}
export const getPatcher = () => {
+2 -2
View File
@@ -471,8 +471,8 @@ export default {
background-attachment: fixed;
> h2 {
// this is shown directly on the background image / color
color: var(--color-background-plain-text);
// this is shown directly on the background which has `color-primary`, so we need `color-primary-text`
color: var(--color-primary-text);
text-align: center;
font-size: 32px;
line-height: 130%;
+12 -9
View File
@@ -97,16 +97,19 @@ class CalendarObject extends \Sabre\CalDAV\CalendarObject {
* @param Component\VCalendar $vObject
* @return void
*/
private function createConfidentialObject(Component\VCalendar $vObject): void {
private function createConfidentialObject(Component\VCalendar $vObject) {
/** @var Component $vElement */
$vElements = array_filter($vObject->getComponents(), static function ($vElement) {
return $vElement instanceof Component\VEvent || $vElement instanceof Component\VJournal || $vElement instanceof Component\VTodo;
});
foreach ($vElements as $vElement) {
if (empty($vElement->select('SUMMARY'))) {
$vElement->add('SUMMARY', $this->l10n->t('Busy')); // This is needed to mask "Untitled Event" events
}
$vElement = null;
if (isset($vObject->VEVENT)) {
$vElement = $vObject->VEVENT;
}
if (isset($vObject->VJOURNAL)) {
$vElement = $vObject->VJOURNAL;
}
if (isset($vObject->VTODO)) {
$vElement = $vObject->VTODO;
}
if (!is_null($vElement)) {
foreach ($vElement->children() as &$property) {
/** @var Property $property */
switch ($property->name) {
@@ -578,10 +578,6 @@ class FederatedShareProvider implements IShareProvider {
public function getSharesInFolder($userId, Folder $node, $reshares, $shallow = true) {
if (!$shallow) {
throw new \Exception("non-shallow getSharesInFolder is no longer supported");
}
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('*')
->from('share', 's')
@@ -609,7 +605,12 @@ class FederatedShareProvider implements IShareProvider {
$qb->innerJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid'));
$qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId())));
$qb->andWhere($qb->expr()->eq('f.storage', $qb->createNamedParameter($node->getMountPoint()->getNumericStorageId(), IQueryBuilder::PARAM_INT)));
if ($shallow) {
$qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId(), IQueryBuilder::PARAM_INT)));
} else {
$qb->andWhere($qb->expr()->like('f.path', $qb->createNamedParameter($this->dbConnection->escapeLikeParameter($node->getInternalPath()) . '/%')));
}
$qb->orderBy('id');
File diff suppressed because one or more lines are too long
+2 -17
View File
@@ -22,7 +22,7 @@
import { emit } from '@nextcloud/event-bus'
import { Permission, Node, View, FileAction, FileType } from '@nextcloud/files'
import { showInfo } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
import axios from '@nextcloud/axios'
import CloseSvg from '@mdi/svg/svg/close.svg?raw'
@@ -30,7 +30,6 @@ import NetworkOffSvg from '@mdi/svg/svg/network-off.svg?raw'
import TrashCanSvg from '@mdi/svg/svg/trash-can.svg?raw'
import logger from '../logger.js'
import PQueue from 'p-queue'
const canUnshareOnly = (nodes: Node[]) => {
return nodes.every(node => node.attributes['is-mount-root'] === true
@@ -120,8 +119,6 @@ const displayName = (nodes: Node[], view: View) => {
return t('files', 'Delete')
}
const queue = new PQueue({ concurrency: 1 })
export const action = new FileAction({
id: 'delete',
displayName,
@@ -186,19 +183,7 @@ export const action = new FileAction({
return Promise.all(nodes.map(() => false))
}
// Map each node to a promise that resolves with the result of exec(node)
const promises = nodes.map(node => {
// Create a promise that resolves with the result of exec(node)
const promise = new Promise<boolean>(resolve => {
queue.add(async () => {
const result = await this.exec(node, view, dir)
resolve(result !== null ? result : false)
})
})
return promise
})
return Promise.all(promises)
return Promise.all(nodes.map(node => this.exec(node, view, dir)))
},
order: 100,
+16 -16
View File
@@ -121,13 +121,14 @@ import type { Upload } from '@nextcloud/upload'
import type { UserConfig } from '../types.ts'
import type { View, ContentsWithRoot } from '@nextcloud/files'
import { getCapabilities } from '@nextcloud/capabilities'
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
import { Folder, Node, Permission } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import { getCapabilities } from '@nextcloud/capabilities'
import { join, dirname } from 'path'
import { orderBy } from 'natural-orderby'
import { Parser } from 'xml2js'
import { showError } from '@nextcloud/dialogs'
import { translate, translatePlural } from '@nextcloud/l10n'
import { Type } from '@nextcloud/sharing'
import { UploadPicker } from '@nextcloud/upload'
import { loadState } from '@nextcloud/initial-state'
@@ -468,8 +469,6 @@ export default defineComponent({
},
methods: {
t,
async fetchContent() {
this.loading = true
const dir = this.dir
@@ -570,19 +569,17 @@ export default defineComponent({
}
// Else we try to parse the response error message
if (typeof upload.response?.data === 'string') {
try {
const parser = new DOMParser()
const doc = parser.parseFromString(upload.response.data, 'text/xml')
const message = doc.getElementsByTagName('s:message')[0]?.textContent ?? ''
if (message.trim() !== '') {
// The server message is also translated
showError(t('files', 'Error during upload: {message}', { message }))
return
}
} catch (error) {
logger.error('Could not parse message', { error })
try {
const parser = new Parser({ trim: true, explicitRoot: false })
const response = await parser.parseStringPromise(upload.response?.data)
const message = response['s:message'][0] as string
if (typeof message === 'string' && message.trim() !== '') {
// The server message is also translated
showError(this.t('files', 'Error during upload: {message}', { message }))
return
}
} catch (error) {
logger.error('Error while parsing', { error })
}
// Finally, check the status code if we have one
@@ -635,6 +632,9 @@ export default defineComponent({
toggleGridView() {
this.userConfigStore.update('grid_view', !this.userConfig.grid_view)
},
t: translate,
n: translatePlural,
},
})
</script>
@@ -19,10 +19,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import type { Node, View } from '@nextcloud/files'
import { FileAction } from '@nextcloud/files'
import { FileAction, Node } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import CalendarClockSvg from '@mdi/svg/svg/calendar-clock.svg?raw'
@@ -35,10 +32,7 @@ export const action = new FileAction({
title: () => t('files_reminders', 'Set reminder at custom date & time'),
iconSvgInline: () => CalendarClockSvg,
enabled: (_nodes: Node[], view: View) => {
return view.id !== 'trashbin'
},
enabled: () => true,
parent: SET_REMINDER_MENU_ID,
async exec(file: Node) {
@@ -19,9 +19,6 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import type { Node, View } from '@nextcloud/files'
import { FileAction } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import AlarmSvg from '@mdi/svg/svg/alarm.svg?raw'
@@ -33,9 +30,7 @@ export const action = new FileAction({
displayName: () => t('files_reminders', 'Set reminder'),
iconSvgInline: () => AlarmSvg,
enabled: (_nodes: Node[], view: View) => {
return view.id !== 'trashbin'
},
enabled: () => true,
async exec() {
return null
@@ -21,7 +21,7 @@
*/
import Vue from 'vue'
import type { Node, View } from '@nextcloud/files'
import type { Node } from '@nextcloud/files'
import { FileAction } from '@nextcloud/files'
import { emit } from '@nextcloud/event-bus'
@@ -91,13 +91,7 @@ const generateFileAction = (option: ReminderOption): FileAction|null => {
// Empty svg to hide the icon
iconSvgInline: () => '<svg></svg>',
enabled: (_nodes: Node[], view: View) => {
if (view.id === 'trashbin') {
return false
}
return Boolean(getDateTime(option.dateTimePreset))
},
enabled: () => Boolean(getDateTime(option.dateTimePreset)),
parent: SET_REMINDER_MENU_ID,
async exec(node: Node) {
+5 -27
View File
@@ -26,11 +26,9 @@
*/
namespace OCA\Files_Sharing;
use OC\Files\Cache\FileAccess;
use OC\Files\Mount\MountPoint;
use OCP\Constants;
use OCP\Files\Folder;
use OCP\Server;
use OCP\Share\IShare;
class Updater {
@@ -60,40 +58,20 @@ class Updater {
if ($userFolder === null) {
return;
}
$user = $userFolder->getOwner();
if (!$user) {
throw new \Exception("user folder has no owner");
}
$src = $userFolder->get($path);
$shareManager = \OC::$server->getShareManager();
// FIXME: should CIRCLES be included here ??
$shares = $shareManager->getSharesBy($user->getUID(), IShare::TYPE_USER, $src, false, -1);
$shares = array_merge($shares, $shareManager->getSharesBy($user->getUID(), IShare::TYPE_GROUP, $src, false, -1));
$shares = array_merge($shares, $shareManager->getSharesBy($user->getUID(), IShare::TYPE_ROOM, $src, false, -1));
$shares = $shareManager->getSharesBy($userFolder->getOwner()->getUID(), IShare::TYPE_USER, $src, false, -1);
$shares = array_merge($shares, $shareManager->getSharesBy($userFolder->getOwner()->getUID(), IShare::TYPE_GROUP, $src, false, -1));
$shares = array_merge($shares, $shareManager->getSharesBy($userFolder->getOwner()->getUID(), IShare::TYPE_ROOM, $src, false, -1));
if ($src instanceof Folder) {
$cacheAccess = Server::get(FileAccess::class);
$sourceStorageId = $src->getStorage()->getCache()->getNumericStorageId();
$sourceInternalPath = $src->getInternalPath();
$subShares = array_merge(
$shareManager->getSharesBy($user->getUID(), IShare::TYPE_USER),
$shareManager->getSharesBy($user->getUID(), IShare::TYPE_GROUP),
$shareManager->getSharesBy($user->getUID(), IShare::TYPE_ROOM),
);
$shareSourceIds = array_map(fn (IShare $share) => $share->getNodeId(), $subShares);
$shareSources = $cacheAccess->getByFileIdsInStorage($shareSourceIds, $sourceStorageId);
$subShares = $shareManager->getSharesInFolder($userFolder->getOwner()->getUID(), $src, false, false);
foreach ($subShares as $subShare) {
$shareCacheEntry = $shareSources[$subShare->getNodeId()] ?? null;
if (
$shareCacheEntry &&
str_starts_with($shareCacheEntry->getPath(), $sourceInternalPath . '/')
) {
$shares[] = $subShare;
}
$shares = array_merge($shares, array_values($subShare));
}
}
@@ -137,9 +137,6 @@ abstract class AUserData extends OCSController {
$groups = $this->groupManager->getUserGroups($targetUserObject);
$gids = [];
foreach ($groups as $group) {
if (!$this->groupManager->getSubAdmin()->isSubAdminOfGroup($currentLoggedInUser, $group)) {
continue;
};
$gids[] = $group->getGID();
}
+1 -1
View File
@@ -1 +1 @@
{"version":3,"sourceRoot":"","sources":["settings.scss","../../../core/css/functions.scss"],"names":[],"mappings":"AAQC,0BACC,WAKF,OACC,WAID,4BC6BC,2CDzBD,mBCyBC,kDDrBD,qBCqBC,yCDjBD,0BCiBC,wCDbD,oECaC,2CDTD,oCACC,oBACA,0BACA,+BACA,mBAGD,4BACC,oBACA,kCAGD,yBACC,WAIA,wCACC,kBACA,yDACC,gBAIA,mOACC,WAKH,uCACC,aAGD,sCACC,WAED,uDACC,WAKD,gBACC,WAIF,mBACC,aACA,aACA,iBACA,uEACA,qBAEA,4BACC,kBACA,SAEA,+BACC,mBAIA,qCACC,iBAKH,kCACC,iBACA,mBACA,gBAGD,mGACC,4BACA,kBACA,WAMF,oBACC,kBACA,wCACC,0BAIF,aACC,qBACA,YACA,kBACA,8CACA,wCACA,wCACA,8CACA,6CAEA,qFAIC,6DACA,oDAGD,uBACC,kBACA,yBAIF,6BACC,oBACA,kCAEA,mCACC,WAIA,oCACC,kBACA,oBACA,iBACA,2BACA,WACA,mBACA,QAEA,0CACC,mBACA,uBACA,gBAKD,gIACC,kBACA,UACA,UACA,oBACA,YAKH,qCACC,kBACA,UACA,MACA,SAEA,yCACC,qBAIF,4CACC,eAGD,4CACC,sBACA,WACA,YACA,YAMF,qBACC,aACA,WACA,SACA,YAEA,uBACC,aAGD,uCACC,sBACA,cACA,yBAIF,iBACC,kBACA,eACA,WACA,YACA,aACA,SACA,gBACA,YAEA,8CAEC,+CACA,wCAEA,0FACC,WAIF,uCACC,kBACA,qBACA,gCACA,WACA,eAEA,wDACC,qBACA,sBACA,eAIF,sCACC,SAGC,4DAEC,iBACA,kBAEA,kFACC,YAGD,mEACC,oDAEA,kFACC,iBAIF,qEACC,WAEA,eAEA,uEACC,eAQN,gBACC,YAIA,2BACC,kCAGD,mBACC,YAIF,sCAEC,aAGD,eACC,WAGD,YACC,qBAIA,aACC,WACA,yBACA,YAGD,WACC,WACA,yBACA,YAMD,oBACC,iBAGD,iBACC,eAKD,iCACC,aACA,eACA,sBACA,SACA,gDACC,aACA,eACA,sBACA,sDACC,oBAIF,kGACC,cACA,YACA,gBAKA,iEACC,kBACA,UAED,+EACC,oBACA,eACA,wBACA,UAIF,wCACC,WAGD,iDACC,qBAGD,sDACC,kBACA,OACA,WACA,0BACA,eACA,gBACA,WAQF,oBACC,gBAGD,wBACC,iBAGD,oDACC,WACA,YACA,mBACA,wCAOD,oBACC,UACA,cACA,gBACA,uBAGD,2BACC,UAKD,oCAEC,cAKD,wEAEC,aAIF,gBACC,kBACA,QACA,QAEA,sBACC,YAGD,sBACC,iBAKF,WACC,WAEA,cACC,WACA,kBACA,4CACA,gBACA,mBAGD,cACC,4CACA,kBACA,gBACA,mBAKD,gBACC,kBACA,cACA,eACA,uBACA,gBAGD,wBACC,kBAEA,gCACC,kBAIF,sCACC,kBAGD,sDAEC,cACA,eACA,eAEA,0EACC,UACA,qBACA,uBACA,gBAIF,8BACC,eAGD,kCACC,mBACA,cAIF,2BACC,mBAMA,oBACC,mBACA,iBACA,WAGD,gCACC,kBAIA,gGACC,cAMH,SACC,gBAEA,0BACC,4CAID,YACC,mBAEA,uBACC,iBACA,2BACA,qBAMH,KACC,mBACA,mBAGD,SACC,aAGD,mBACC,mBAGD,eACC,gBAOA,+IACC,sBAEA,+KACC,aAGD,mKACC,WACA,YACA,kCACA,qBACA,kBAGD,mOACC,sCAGD,mNACC,sCAGD,mNACC,oCAMF,sBACC,aAGD,YACC,oBAGD,kBACC,kBAGD,yBACC,kBAGD,sBACC,kBAGD,oCACC,uBAIF,yCACC,kBAGD,wBACC,qBAGD,2BACC,wBAEA,gBACA,aACA,iBACA,sBAKD,WACC,kBACA,2BACA,WAGD,2DAGC,qBAIA,mCACC,qBACA,YACA,iBAGD,+EAEC,YAIF,yBACC,mCACC,YACA,gBACA,cACA,iDAIF,eACC,WAGD,SACC,iBAGD,QACC,qBACA,YACA,WACA,2BAEA,gBACC,kBAIF,qBACC,sBACA,qBACA,YACA,iBAGD,kBACC,qBACA,gBAIA,aACC,sCACA,mCAGD,WACC,oCAGD,mBACC,sCACA,oBAMF,8CACC,WACA,YAGD,wBACC,WACA,YACA,mBACA,kBACA,+DAIA,oBACC,iBACA,gBAEA,uBACC,cAGD,uBACC,kBAIF,0BACC,YACA,gCAGD,oDACC,yBAGD,wDACC,2BAGD,uBACC,cAKD,oBACC,0BAGD,oCACC,gBAIF,2BACC,aACA,eACA,mDAEA,8BACC,kBAGD,6BACC,WAIF,eACC,mBAEA,iBACC,qBACA,cAIF,SACC,UAGD,eACC,iBACA,mBACA,WAGD,UACI,+CAGJ,2BACE,GACE,YAGJ,mCACE,GACE","file":"settings.css"}
{"version":3,"sourceRoot":"","sources":["settings.scss","../../../core/css/functions.scss"],"names":[],"mappings":"AAQC,0BACC,WAKF,OACC,WAID,4BC8CC,2CD1CD,mBC0CC,kDDtCD,qBCsCC,yCDlCD,0BCkCC,wCD9BD,oEC8BC,2CD1BD,oCACC,oBACA,0BACA,+BACA,mBAGD,4BACC,oBACA,kCAGD,yBACC,WAIA,wCACC,kBACA,yDACC,gBAIA,mOACC,WAKH,uCACC,aAGD,sCACC,WAED,uDACC,WAKD,gBACC,WAIF,mBACC,aACA,aACA,iBACA,uEACA,qBAEA,4BACC,kBACA,SAEA,+BACC,mBAIA,qCACC,iBAKH,kCACC,iBACA,mBACA,gBAGD,mGACC,4BACA,kBACA,WAMF,oBACC,kBACA,wCACC,0BAIF,aACC,qBACA,YACA,kBACA,8CACA,wCACA,wCACA,8CACA,6CAEA,qFAIC,6DACA,oDAGD,uBACC,kBACA,yBAIF,6BACC,oBACA,kCAEA,mCACC,WAIA,oCACC,kBACA,oBACA,iBACA,2BACA,WACA,mBACA,QAEA,0CACC,mBACA,uBACA,gBAKD,gIACC,kBACA,UACA,UACA,oBACA,YAKH,qCACC,kBACA,UACA,MACA,SAEA,yCACC,qBAIF,4CACC,eAGD,4CACC,sBACA,WACA,YACA,YAMF,qBACC,aACA,WACA,SACA,YAEA,uBACC,aAGD,uCACC,sBACA,cACA,yBAIF,iBACC,kBACA,eACA,WACA,YACA,aACA,SACA,gBACA,YAEA,8CAEC,+CACA,wCAEA,0FACC,WAIF,uCACC,kBACA,qBACA,gCACA,WACA,eAEA,wDACC,qBACA,sBACA,eAIF,sCACC,SAGC,4DAEC,iBACA,kBAEA,kFACC,YAGD,mEACC,oDAEA,kFACC,iBAIF,qEACC,WAEA,eAEA,uEACC,eAQN,gBACC,YAIA,2BACC,kCAGD,mBACC,YAIF,sCAEC,aAGD,eACC,WAGD,YACC,qBAIA,aACC,WACA,yBACA,YAGD,WACC,WACA,yBACA,YAMD,oBACC,iBAGD,iBACC,eAKD,iCACC,aACA,eACA,sBACA,SACA,gDACC,aACA,eACA,sBACA,sDACC,oBAIF,kGACC,cACA,YACA,gBAKA,iEACC,kBACA,UAED,+EACC,oBACA,eACA,wBACA,UAIF,wCACC,WAGD,iDACC,qBAGD,sDACC,kBACA,OACA,WACA,0BACA,eACA,gBACA,WAQF,oBACC,gBAGD,wBACC,iBAGD,oDACC,WACA,YACA,mBACA,wCAOD,oBACC,UACA,cACA,gBACA,uBAGD,2BACC,UAKD,oCAEC,cAKD,wEAEC,aAIF,gBACC,kBACA,QACA,QAEA,sBACC,YAGD,sBACC,iBAKF,WACC,WAEA,cACC,WACA,kBACA,4CACA,gBACA,mBAGD,cACC,4CACA,kBACA,gBACA,mBAKD,gBACC,kBACA,cACA,eACA,uBACA,gBAGD,wBACC,kBAEA,gCACC,kBAIF,sCACC,kBAGD,sDAEC,cACA,eACA,eAEA,0EACC,UACA,qBACA,uBACA,gBAIF,8BACC,eAGD,kCACC,mBACA,cAIF,2BACC,mBAMA,oBACC,mBACA,iBACA,WAGD,gCACC,kBAIA,gGACC,cAMH,SACC,gBAEA,0BACC,4CAID,YACC,mBAEA,uBACC,iBACA,2BACA,qBAMH,KACC,mBACA,mBAGD,SACC,aAGD,mBACC,mBAGD,eACC,gBAOA,+IACC,sBAEA,+KACC,aAGD,mKACC,WACA,YACA,kCACA,qBACA,kBAGD,mOACC,sCAGD,mNACC,sCAGD,mNACC,oCAMF,sBACC,aAGD,YACC,oBAGD,kBACC,kBAGD,yBACC,kBAGD,sBACC,kBAGD,oCACC,uBAIF,yCACC,kBAGD,wBACC,qBAGD,2BACC,wBAEA,gBACA,aACA,iBACA,sBAKD,WACC,kBACA,2BACA,WAGD,2DAGC,qBAIA,mCACC,qBACA,YACA,iBAGD,+EAEC,YAIF,yBACC,mCACC,YACA,gBACA,cACA,iDAIF,eACC,WAGD,SACC,iBAGD,QACC,qBACA,YACA,WACA,2BAEA,gBACC,kBAIF,qBACC,sBACA,qBACA,YACA,iBAGD,kBACC,qBACA,gBAIA,aACC,sCACA,mCAGD,WACC,oCAGD,mBACC,sCACA,oBAMF,8CACC,WACA,YAGD,wBACC,WACA,YACA,mBACA,kBACA,+DAIA,oBACC,iBACA,gBAEA,uBACC,cAGD,uBACC,kBAIF,0BACC,YACA,gCAGD,oDACC,yBAGD,wDACC,2BAGD,uBACC,cAKD,oBACC,0BAGD,oCACC,gBAIF,2BACC,aACA,eACA,mDAEA,8BACC,kBAGD,6BACC,WAIF,eACC,mBAEA,iBACC,qBACA,cAIF,SACC,UAGD,eACC,iBACA,mBACA,WAGD,UACI,+CAGJ,2BACE,GACE,YAGJ,mCACE,GACE","file":"settings.css"}
@@ -47,7 +47,7 @@ trait CheckServerResponseTrait {
* Common helper string in case a check could not fetch any results
*/
protected function serverConfigHelp(): string {
return $this->l10n->t('To allow this check to run you have to make sure that your Web server can connect to itself. Therefore it must be able to resolve and connect to at least one of its `trusted_domains` or the `overwrite.cli.url`. This failure may be the result of a server-side DNS mismatch or outbound firewall rule.');
return $this->l10n->t('To allow this check to run you have to make sure that your webserver can connect to itself. Therefor it must be able to resolve and connect to at least one its `trusted_domains` or the `overwrite.cli.url`.');
}
/**
@@ -31,7 +31,6 @@ use OC\DB\SchemaWrapper;
use OCP\DB\Events\AddMissingIndicesEvent;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\SetupCheck\ISetupCheck;
use OCP\SetupCheck\SetupResult;
@@ -40,7 +39,6 @@ class DatabaseHasMissingIndices implements ISetupCheck {
private IL10N $l10n,
private Connection $connection,
private IEventDispatcher $dispatcher,
private IURLGenerator $urlGenerator,
) {
}
@@ -92,18 +90,12 @@ class DatabaseHasMissingIndices implements ISetupCheck {
if (empty($missingIndices)) {
return SetupResult::success('None');
} else {
$processed = 0;
$list = $this->l10n->t('Missing indices:');
$list = '';
foreach ($missingIndices as $missingIndex) {
$processed++;
$list .= "\n " . $this->l10n->t('"%s" in table "%s"', [$missingIndex['indexName'], $missingIndex['tableName']]);
if (count($missingIndices) > $processed) {
$list .= ", ";
}
$list .= "\n".$this->l10n->t('Missing optional index "%s" in table "%s".', [$missingIndex['indexName'], $missingIndex['tableName']]);
}
return SetupResult::warning(
$this->l10n->t('Detected some missing optional indices. Occasionally new indices are added (by Nextcloud or installed applications) to improve database performance. Adding indices can sometimes take awhile and temporarily hurt performance so this is not done automatically during upgrades. Once the indices are added, queries to those tables should be faster. Use the command `occ db:add-missing-indices` to add them. ') . $list . '.',
$this->urlGenerator->linkToDocs('admin-long-running-migration-steps')
$this->l10n->t('The database is missing some indexes. Due to the fact that adding indexes on big tables could take some time they were not added automatically. By running "occ db:add-missing-indices" those missing indexes could be added manually while the instance keeps running. Once the indexes are added queries to those tables are usually much faster.').$list
);
}
}
@@ -64,19 +64,12 @@ class HttpsUrlGeneration implements ISetupCheck {
}
$generatedUrl = $this->urlGenerator->getAbsoluteURL('index.php');
if (!str_starts_with($generatedUrl, 'https://')) {
if (!\OC::$CLI) {
return SetupResult::warning(
$this->l10n->t('You are accessing your instance over a secure connection, however your instance is generating insecure URLs. This likely means that your instance is behind a reverse proxy and the Nextcloud `overwrite*` config values are not set correctly.'),
$this->urlGenerator->linkToDocs('admin-reverse-proxy')
);
/* We were called from CLI so we can't be 100% sure which scenario is applicable */
} else {
return SetupResult::info(
$this->l10n->t('Your instance is generating insecure URLs. If you access your instance over HTTPS, this likely means that your instance is behind a reverse proxy and the Nextcloud `overwrite*` config values are not set correctly.'),
$this->urlGenerator->linkToDocs('admin-reverse-proxy')
);
}
return SetupResult::warning(
$this->l10n->t('You are accessing your instance over a secure connection, however your instance is generating insecure URLs. This most likely means that you are behind a reverse proxy and the overwrite config variables are not set correctly.'),
$this->urlGenerator->linkToDocs('admin-reverse-proxy')
);
}
return SetupResult::success($this->l10n->t('You are accessing your instance over a secure connection, and your instance is generating secure URLs.'));
}
}
@@ -68,7 +68,7 @@ class JavaScriptModules implements ISetupCheck {
}
if ($noResponse) {
return SetupResult::warning($this->l10n->t('Unable to run check for JavaScript support. Please remedy or confirm manually if your webserver serves `.mjs` files using the JavaScript MIME type.') . "\n" . $this->serverConfigHelp());
return SetupResult::warning($this->l10n->t('Could not check for JavaScript support via any of your `trusted_domains` nor `overwrite.cli.url`. This may be the result of a server-side DNS mismatch or outbound firewall rule. Please check manually if your webserver serves `.mjs` files using the JavaScript MIME type.') . "\n" . $this->serverConfigHelp());
}
return SetupResult::error($this->l10n->t('Your webserver does not serve `.mjs` files using the JavaScript MIME type. This will break some apps by preventing browsers from executing the JavaScript files. You should configure your webserver to serve `.mjs` files with either the `text/javascript` or `application/javascript` MIME type.'));
@@ -53,16 +53,15 @@ class MemcacheConfigured implements ISetupCheck {
$memcacheLocalClass = $this->config->getSystemValue('memcache.local', null);
$caches = array_filter([$memcacheDistributedClass,$memcacheLockingClass,$memcacheLocalClass]);
if (in_array(\OC\Memcache\Memcached::class, array_map(fn (string $class) => ltrim($class, '\\'), $caches))) {
// wrong PHP module is installed
if (extension_loaded('memcache') && !extension_loaded('memcached')) {
if (extension_loaded('memcache')) {
return SetupResult::warning(
$this->l10n->t('Memcached is configured as distributed cache, but the wrong PHP module ("memcache") is installed. Please install the PHP module "memcached".')
$this->l10n->t('Memcached is configured as distributed cache, but the wrong PHP module "memcache" is installed. \\OC\\Memcache\\Memcached only supports "memcached" and not "memcache".'),
'https://code.google.com/p/memcached/wiki/PHPClientComparison'
);
}
// required PHP module is missing
if (!extension_loaded('memcached')) {
return SetupResult::warning(
$this->l10n->t('Memcached is configured as distributed cache, but the PHP module "memcached" is not installed. Please install the PHP module "memcached".')
$this->l10n->t('Memcached is configured as distributed cache, but the PHP module "memcached" is not installed.')
);
}
}
@@ -47,7 +47,7 @@ class PhpOutdated implements ISetupCheck {
public function run(): SetupResult {
if (PHP_VERSION_ID < 80200) {
return SetupResult::warning($this->l10n->t('You are currently running PHP %s. PHP 8.1 is now deprecated in Nextcloud 30. Nextcloud 31 may require at least PHP 8.2. Please upgrade to one of the officially supported PHP versions provided by the PHP Group as soon as possible.', [PHP_VERSION]), 'https://secure.php.net/supported-versions.php');
return SetupResult::warning($this->l10n->t('PHP %s detected. PHP >= 8.2 and <= 8.3 is suggested for optimal performance, security, and stability with this version of Nextcloud. Additionally, the next major release of Nextcloud will likely require >= PHP 8.2.', [PHP_VERSION]), 'https://secure.php.net/supported-versions.php');
}
return SetupResult::success($this->l10n->t('You are currently running PHP %s.', [PHP_VERSION]));
}
@@ -62,16 +62,14 @@ class SupportedDatabase implements ISetupCheck {
$row = $result->fetch();
$version = $row['Value'];
$versionlc = strtolower($version);
// we only care about X.Y not X.Y.Z differences
[$major, $minor, ] = explode('.', $versionlc);
$versionConcern = $major . '.' . $minor;
if (str_contains($versionlc, 'mariadb')) {
if (version_compare($versionConcern, '10.3', '<') || version_compare($versionConcern, '10.11', '>')) {
return SetupResult::warning($this->l10n->t('MariaDB version "%s" detected. MariaDB >=10.3 and <=10.11 is suggested for best performance, stability and functionality with this version of Nextcloud.', $version));
if (version_compare($versionlc, '10.2', '<')) {
return SetupResult::warning($this->l10n->t('MariaDB version "%s" is used. Nextcloud 21 and higher do not support this version and require MariaDB 10.2 or higher.', $version));
}
} else {
if (version_compare($versionConcern, '8.0', '<') || version_compare($versionConcern, '8.3', '>')) {
return SetupResult::warning($this->l10n->t('MySQL version "%s" detected. MySQL >=8.0 and <=8.3 is suggested for best performance, stability and functionality with this version of Nextcloud.', $version));
if (version_compare($versionlc, '8', '<')) {
return SetupResult::warning($this->l10n->t('MySQL version "%s" is used. Nextcloud 21 and higher do not support this version and require MySQL 8.0 or MariaDB 10.2 or higher.', $version));
}
}
} elseif ($databasePlatform instanceof PostgreSQLPlatform) {
@@ -79,12 +77,8 @@ class SupportedDatabase implements ISetupCheck {
$result->execute();
$row = $result->fetch();
$version = $row['server_version'];
$versionlc = strtolower($version);
// we only care about X not X.Y or X.Y.Z differences
[$major, ] = explode('.', $versionlc);
$versionConcern = $major;
if (version_compare($versionConcern, '12', '<') || version_compare($versionConcern, '16', '>')) {
return SetupResult::warning($this->l10n->t('PostgreSQL version "%s" detected. PostgreSQL >=12 and <=16 is suggested for best performance, stability and functionality with this version of Nextcloud.', $version));
if (version_compare(strtolower($version), '9.6', '<')) {
return SetupResult::warning($this->l10n->t('PostgreSQL version "%s" is used. Nextcloud 21 and higher do not support this version and require PostgreSQL 9.6 or higher.', $version));
}
} elseif ($databasePlatform instanceof OraclePlatform) {
$version = 'Oracle';
+3 -3
View File
@@ -190,9 +190,9 @@ export default {
.draggable__number {
border-radius: 20px;
border: 2px solid var(--color-primary-element);
color: var(--color-primary-element);
padding: 0px 7px;
border: 2px solid var(--color-primary-default);
color: var(--color-primary-default);
padding: 0px 7px;
margin-right: 3px;
}
@@ -160,17 +160,17 @@
<NcCheckboxRadioSwitch type="switch"
aria-controls="settings-sharing-privacy-user-enumeration"
:checked.sync="settings.allowShareDialogUserEnumeration">
{{ t('settings', 'Allow account name autocompletion in share dialog and allow access to the system address book') }}
{{ t('settings', 'Allow username autocompletion in share dialog and allow access to the system address book') }}
</NcCheckboxRadioSwitch>
<fieldset v-show="settings.allowShareDialogUserEnumeration" id="settings-sharing-privacy-user-enumeration" class="sharing__sub-section">
<em>
{{ t('settings', 'If autocompletion "same group" and "phone number integration" are enabled a match in either is enough to show the user.') }}
</em>
<NcCheckboxRadioSwitch :checked.sync="settings.restrictUserEnumerationToGroup">
{{ t('settings', 'Allow account name autocompletion to users within the same groups and limit system address books to users in the same groups') }}
{{ t('settings', 'Allow username autocompletion to users within the same groups and limit system address books to users in the same groups') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :checked.sync="settings.restrictUserEnumerationToPhone">
{{ t('settings', 'Allow account name autocompletion to users based on phone number integration') }}
{{ t('settings', 'Allow username autocompletion to users based on phone number integration') }}
</NcCheckboxRadioSwitch>
</fieldset>
@@ -220,9 +220,9 @@ export default {
usernameLabel() {
if (this.settings.newUserGenerateUserID) {
return t('settings', 'Account name will be autogenerated')
return t('settings', 'Username will be autogenerated')
}
return t('settings', 'Account name (required)')
return t('settings', 'Username (required)')
},
minPasswordLength() {
@@ -35,12 +35,8 @@
<strong>
{{ t('settings', 'Display name') }}
</strong>
</th>
<th class="header__cell header__cell--username"
data-cy-user-list-header-username
scope="col">
<span>
{{ t('settings', 'Account name') }}
<span class="header__subtitle">
{{ t('settings', 'Username') }}
</span>
</th>
<th class="header__cell"
@@ -81,7 +77,7 @@
data-cy-user-list-header-storage-location
scope="col">
<span v-if="showConfig.showUserBackend">
{{ t('settings', 'Account backend') }}
{{ t('settings', 'User backend') }}
</span>
<span v-if="showConfig.showStoragePath"
class="header__subtitle">
@@ -54,14 +54,13 @@
spellcheck="false"
@trailing-button-click="updateDisplayName" />
</template>
<strong v-else-if="!isObfuscated"
:title="user.displayname?.length > 20 ? user.displayname : null">
{{ user.displayname }}
</strong>
</td>
<td class="row__cell row__cell--username" data-cy-user-list-cell-username>
<span class="row__subtitle">{{ user.id }}</span>
<template v-else>
<strong v-if="!isObfuscated"
:title="user.displayname?.length > 20 ? user.displayname : null">
{{ user.displayname }}
</strong>
<span class="row__subtitle">{{ user.id }}</span>
</template>
</td>
<td data-cy-user-list-cell-password
@@ -66,10 +66,6 @@
}
}
&--username {
padding-left: calc(var(--default-grid-baseline) * 3);
}
&--avatar {
min-width: var(--avatar-cell-width);
width: var(--avatar-cell-width);
+6 -1
View File
@@ -1069,7 +1069,12 @@ class ShareByMailProvider implements IShareProvider {
$qb->innerJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid'));
$qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId())));
$qb->andWhere($qb->expr()->eq('f.storage', $qb->createNamedParameter($node->getMountPoint()->getNumericStorageId(), IQueryBuilder::PARAM_INT)));
if ($shallow) {
$qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId())));
} else {
$qb->andWhere($qb->expr()->like('f.path', $qb->createNamedParameter($this->dbConnection->escapeLikeParameter($node->getInternalPath()) . '/%')));
}
$qb->orderBy('id');
+2 -2
View File
@@ -71,6 +71,7 @@
--primary-invert-if-bright: no;
--primary-invert-if-dark: invert(100%);
--color-primary: #00679e;
--color-primary-default: #0082c9;
--color-primary-text: #ffffff;
--color-primary-hover: #3285b1;
--color-primary-light: #e5eff5;
@@ -84,7 +85,6 @@
--color-primary-element-light-hover: #dbe4ea;
--color-primary-element-light-text: #00293f;
--gradient-primary-background: linear-gradient(40deg, var(--color-primary) 0%, var(--color-primary-hover) 100%);
--image-background-default: url('/apps/theming/img/background/kamil-porembinski-clouds.jpg');
--color-background-plain: #00679e;
--color-background-plain-text: #ffffff;
--image-background: url('/apps/theming/img/background/kamil-porembinski-clouds.jpg');
}
+11 -8
View File
@@ -85,7 +85,6 @@ class Capabilities implements IPublicCapability {
* color-element-dark: string,
* logo: string,
* background: string,
* background-text: string,
* background-plain: bool,
* background-default: bool,
* logoheader: string,
@@ -95,13 +94,15 @@ class Capabilities implements IPublicCapability {
*/
public function getCapabilities() {
$color = $this->theming->getDefaultColorPrimary();
// Same as in DefaultTheme
if ($color === BackgroundService::DEFAULT_COLOR) {
$color = BackgroundService::DEFAULT_ACCESSIBLE_COLOR;
}
$colorText = $this->util->invertTextColor($color) ? '#000000' : '#ffffff';
$backgroundLogo = $this->config->getAppValue('theming', 'backgroundMime', '');
$backgroundColor = $this->theming->getColorBackground();
$backgroundText = $this->theming->getTextColorBackground();
$backgroundPlain = $backgroundLogo === 'backgroundColor' || ($backgroundLogo === '' && $backgroundColor !== BackgroundService::DEFAULT_COLOR);
$background = $backgroundPlain ? $backgroundColor : $this->url->getAbsoluteURL($this->theming->getBackground());
$backgroundPlain = $backgroundLogo === 'backgroundColor' || ($backgroundLogo === '' && $color !== '#0082c9');
$background = $backgroundPlain ? $color : $this->url->getAbsoluteURL($this->theming->getBackground());
$user = $this->userSession->getUser();
if ($user instanceof IUser) {
@@ -111,7 +112,10 @@ class Capabilities implements IPublicCapability {
* @see \OCA\Theming\Themes\CommonThemeTrait::generateUserBackgroundVariables()
*/
$color = $this->theming->getColorPrimary();
$colorText = $this->theming->getTextColorPrimary();
if ($color === BackgroundService::DEFAULT_COLOR) {
$color = BackgroundService::DEFAULT_ACCESSIBLE_COLOR;
}
$colorText = $this->util->invertTextColor($color) ? '#000000' : '#ffffff';
$backgroundImage = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'background_image', BackgroundService::BACKGROUND_DEFAULT);
if ($backgroundImage === BackgroundService::BACKGROUND_CUSTOM) {
@@ -122,7 +126,7 @@ class Capabilities implements IPublicCapability {
$background = $this->url->linkTo(Application::APP_ID, "img/background/$backgroundImage");
} elseif ($backgroundImage !== BackgroundService::BACKGROUND_DEFAULT) {
$backgroundPlain = true;
$background = $backgroundColor;
$background = $color;
}
}
@@ -138,7 +142,6 @@ class Capabilities implements IPublicCapability {
'color-element-dark' => $this->util->elementColor($color, false),
'logo' => $this->url->getAbsoluteURL($this->theming->getLogo()),
'background' => $background,
'background-text' => $backgroundText,
'background-plain' => $backgroundPlain,
'background-default' => !$this->util->isBackgroundThemed(),
'logoheader' => $this->url->getAbsoluteURL($this->theming->getLogo()),
+2 -7
View File
@@ -33,7 +33,7 @@ use Symfony\Component\Console\Output\OutputInterface;
class UpdateConfig extends Command {
public const SUPPORTED_KEYS = [
'name', 'url', 'imprintUrl', 'privacyUrl', 'slogan', 'color', 'primary_color', 'disable-user-theming'
'name', 'url', 'imprintUrl', 'privacyUrl', 'slogan', 'color', 'disable-user-theming'
];
private $themingDefaults;
@@ -128,13 +128,8 @@ class UpdateConfig extends Command {
$value = $this->imageManager->updateImage($key, $value);
$key = $key . 'Mime';
}
if ($key === 'color') {
$output->writeln('<warning>Using "color" is depreacted, use "primary_color" instead');
$key = 'primary_color';
}
if ($key === 'primary_color' && !preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) {
if ($key === 'color' && !preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) {
$output->writeln('<error>The given color is invalid: ' . $value . '</error>');
return 1;
}
@@ -50,11 +50,13 @@ use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\NotFoundResponse;
use OCP\Files\IAppData;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
use OCP\ITempManager;
use OCP\IURLGenerator;
use ScssPhp\ScssPhp\Compiler;
@@ -71,6 +73,8 @@ class ThemingController extends Controller {
private ThemingDefaults $themingDefaults;
private IL10N $l10n;
private IConfig $config;
private ITempManager $tempManager;
private IAppData $appData;
private IURLGenerator $urlGenerator;
private IAppManager $appManager;
private ImageManager $imageManager;
@@ -82,6 +86,8 @@ class ThemingController extends Controller {
IConfig $config,
ThemingDefaults $themingDefaults,
IL10N $l,
ITempManager $tempManager,
IAppData $appData,
IURLGenerator $urlGenerator,
IAppManager $appManager,
ImageManager $imageManager,
@@ -92,6 +98,8 @@ class ThemingController extends Controller {
$this->themingDefaults = $themingDefaults;
$this->l10n = $l;
$this->config = $config;
$this->tempManager = $tempManager;
$this->appData = $appData;
$this->urlGenerator = $urlGenerator;
$this->appManager = $appManager;
$this->imageManager = $imageManager;
@@ -143,12 +151,7 @@ class ThemingController extends Controller {
$error = $this->l10n->t('The given slogan is too long');
}
break;
case 'primary_color':
if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) {
$error = $this->l10n->t('The given color is invalid');
}
break;
case 'background_color':
case 'color':
if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) {
$error = $this->l10n->t('The given color is invalid');
}
@@ -58,6 +58,7 @@ class UserThemeController extends OCSController {
protected ?string $userId = null;
private IConfig $config;
private IUserSession $userSession;
private ThemesService $themesService;
private ThemingDefaults $themingDefaults;
private BackgroundService $backgroundService;
@@ -71,6 +72,7 @@ class UserThemeController extends OCSController {
BackgroundService $backgroundService) {
parent::__construct($appName, $request);
$this->config = $config;
$this->userSession = $userSession;
$this->themesService = $themesService;
$this->themingDefaults = $themingDefaults;
$this->backgroundService = $backgroundService;
@@ -184,8 +186,7 @@ class UserThemeController extends OCSController {
$this->backgroundService->deleteBackgroundImage();
return new JSONResponse([
'backgroundImage' => null,
'backgroundColor' => $this->themingDefaults->getColorBackground(),
'primaryColor' => $this->themingDefaults->getColorPrimary(),
'backgroundColor' => $this->themingDefaults->getColorPrimary(),
'version' => $currentVersion,
]);
}
@@ -240,8 +241,7 @@ class UserThemeController extends OCSController {
return new JSONResponse([
'backgroundImage' => $this->config->getUserValue($this->userId, Application::APP_ID, 'background_image', BackgroundService::BACKGROUND_DEFAULT),
'backgroundColor' => $this->themingDefaults->getColorBackground(),
'primaryColor' => $this->themingDefaults->getColorPrimary(),
'backgroundColor' => $this->themingDefaults->getColorPrimary(),
'version' => $currentVersion,
]);
}
+40 -55
View File
@@ -4,7 +4,6 @@
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Kesselberg <mail@danielkesselberg.de>
* @author Ferdinand Thiessen <opensource@fthiessen.de>
* @author Gary Kim <gary@garykim.dev>
* @author Jacob Neplokh <me@jacobneplokh.com>
* @author John Molakvoæ <skjnldsv@protonmail.com>
@@ -57,7 +56,6 @@ class ImageManager {
private ICacheFactory $cacheFactory,
private LoggerInterface $logger,
private ITempManager $tempManager,
private BackgroundService $backgroundService,
) {
}
@@ -79,11 +77,7 @@ class ImageManager {
case 'favicon':
return $this->urlGenerator->imagePath('core', 'logo/logo.png') . '?v=' . $cacheBusterCounter;
case 'background':
// Removing the background defines its mime as 'backgroundColor'
$mimeSetting = $this->config->getAppValue('theming', 'backgroundMime', '');
if ($mimeSetting !== 'backgroundColor') {
return $this->urlGenerator->linkTo(Application::APP_ID, 'img/background/' . BackgroundService::DEFAULT_BACKGROUND_IMAGE);
}
return $this->urlGenerator->linkTo(Application::APP_ID, 'img/background/' . BackgroundService::DEFAULT_BACKGROUND_IMAGE);
}
return '';
}
@@ -233,56 +227,47 @@ class ImageManager {
throw new \Exception('Unsupported image type: ' . $detectedMimeType);
}
if ($key === 'background') {
if ($this->shouldOptimizeBackgroundImage($detectedMimeType, filesize($tmpFile))) {
try {
// Optimize the image since some people may upload images that will be
// either to big or are not progressive rendering.
$newImage = @imagecreatefromstring(file_get_contents($tmpFile));
if ($newImage === false) {
throw new \Exception('Could not read background image, possibly corrupted.');
}
// Preserve transparency
imagesavealpha($newImage, true);
imagealphablending($newImage, true);
$imageWidth = imagesx($newImage);
$imageHeight = imagesy($newImage);
/** @var int */
$newWidth = min(4096, $imageWidth);
$newHeight = intval($imageHeight / ($imageWidth / $newWidth));
$outputImage = imagescale($newImage, $newWidth, $newHeight);
if ($outputImage === false) {
throw new \Exception('Could not scale uploaded background image.');
}
$newTmpFile = $this->tempManager->getTemporaryFile();
imageinterlace($outputImage, true);
// Keep jpeg images encoded as jpeg
if (str_contains($detectedMimeType, 'image/jpeg')) {
if (!imagejpeg($outputImage, $newTmpFile, 90)) {
throw new \Exception('Could not recompress background image as JPEG');
}
} else {
if (!imagepng($outputImage, $newTmpFile, 8)) {
throw new \Exception('Could not recompress background image as PNG');
}
}
$tmpFile = $newTmpFile;
imagedestroy($outputImage);
} catch (\Exception $e) {
if (isset($outputImage) && is_resource($outputImage) || $outputImage instanceof \GdImage) {
imagedestroy($outputImage);
}
$this->logger->debug($e->getMessage());
if ($key === 'background' && $this->shouldOptimizeBackgroundImage($detectedMimeType, filesize($tmpFile))) {
try {
// Optimize the image since some people may upload images that will be
// either to big or are not progressive rendering.
$newImage = @imagecreatefromstring(file_get_contents($tmpFile));
if ($newImage === false) {
throw new \Exception('Could not read background image, possibly corrupted.');
}
}
// For background images we need to announce it
$this->backgroundService->setGlobalBackground($tmpFile);
// Preserve transparency
imagesavealpha($newImage, true);
imagealphablending($newImage, true);
$newWidth = (imagesx($newImage) < 4096 ? imagesx($newImage) : 4096);
$newHeight = (int)(imagesy($newImage) / (imagesx($newImage) / $newWidth));
$outputImage = imagescale($newImage, $newWidth, $newHeight);
if ($outputImage === false) {
throw new \Exception('Could not scale uploaded background image.');
}
$newTmpFile = $this->tempManager->getTemporaryFile();
imageinterlace($outputImage, true);
// Keep jpeg images encoded as jpeg
if (str_contains($detectedMimeType, 'image/jpeg')) {
if (!imagejpeg($outputImage, $newTmpFile, 90)) {
throw new \Exception('Could not recompress background image as JPEG');
}
} else {
if (!imagepng($outputImage, $newTmpFile, 8)) {
throw new \Exception('Could not recompress background image as PNG');
}
}
$tmpFile = $newTmpFile;
imagedestroy($outputImage);
} catch (\Exception $e) {
if (is_resource($outputImage) || $outputImage instanceof \GdImage) {
imagedestroy($outputImage);
}
$this->logger->debug($e->getMessage());
}
}
$target->putContent(file_get_contents($tmpFile));
@@ -55,24 +55,14 @@ class BeforePreferenceListener implements IEventListener {
}
private function handleThemingValues(BeforePreferenceSetEvent|BeforePreferenceDeletedEvent $event): void {
$allowedKeys = ['shortcuts_disabled', 'primary_color'];
if (!in_array($event->getConfigKey(), $allowedKeys)) {
if ($event->getConfigKey() !== 'shortcuts_disabled') {
// Not allowed config key
return;
}
if ($event instanceof BeforePreferenceSetEvent) {
switch ($event->getConfigKey()) {
case 'shortcuts_disabled':
$event->setValid($event->getConfigValue() === 'yes');
break;
case 'primary_color':
$event->setValid(preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $event->getConfigValue()) === 1);
break;
default:
$event->setValid(false);
}
$event->setValid($event->getConfigValue() === 'yes');
return;
}
$event->setValid(true);
@@ -26,6 +26,7 @@ declare(strict_types=1);
namespace OCA\Theming\Listener;
use OCA\Theming\AppInfo\Application;
use OCA\Theming\Service\BackgroundService;
use OCA\Theming\Service\JSDataService;
use OCA\Theming\Service\ThemeInjectionService;
use OCP\AppFramework\Http\Events\BeforeLoginTemplateRenderedEvent;
@@ -80,6 +81,43 @@ class BeforeTemplateRenderedListener implements IEventListener {
$this->themeInjectionService->injectHeaders();
$user = $this->userSession->getUser();
if (!empty($user)) {
$userId = $user->getUID();
/** User background */
$this->initialState->provideInitialState(
'backgroundImage',
$this->config->getUserValue($userId, Application::APP_ID, 'background_image', BackgroundService::BACKGROUND_DEFAULT),
);
/** User color */
$this->initialState->provideInitialState(
'backgroundColor',
$this->config->getUserValue($userId, Application::APP_ID, 'background_color', BackgroundService::DEFAULT_COLOR),
);
/**
* Admin background. `backgroundColor` if disabled,
* mime type if defined and empty by default
*/
$this->initialState->provideInitialState(
'themingDefaultBackground',
$this->config->getAppValue('theming', 'backgroundMime', ''),
);
$this->initialState->provideInitialState(
'defaultShippedBackground',
BackgroundService::DEFAULT_BACKGROUND_IMAGE,
);
/** List of all shipped backgrounds */
$this->initialState->provideInitialState(
'shippedBackgrounds',
BackgroundService::SHIPPED_BACKGROUNDS,
);
}
// Making sure to inject just after core
\OCP\Util::addScript('theming', 'theming', 'core');
}
-1
View File
@@ -30,7 +30,6 @@ namespace OCA\Theming;
* @psalm-type ThemingBackground = array{
* backgroundImage: ?string,
* backgroundColor: string,
* primaryColor: string,
* version: int,
* }
*/
+41 -158
View File
@@ -8,7 +8,6 @@ declare(strict_types=1);
* @author Jan C. Borchardt <hey@jancborchardt.net>
* @author Julius Härtl <jus@bitgrid.net>
* @author Christopher Ng <chrng8@gmail.com>
* @author Ferdinand Thiessen <opensource@fthiessen.de>
*
* @license GNU AGPL version 3 or any later version
*
@@ -31,6 +30,7 @@ namespace OCA\Theming\Service;
use InvalidArgumentException;
use OC\User\NoUserException;
use OCA\Theming\AppInfo\Application;
use OCA\Theming\ThemingDefaults;
use OCP\Files\File;
use OCP\Files\IAppData;
use OCP\Files\IRootFolder;
@@ -41,186 +41,167 @@ use OCP\Files\SimpleFS\ISimpleFolder;
use OCP\IConfig;
use OCP\Lock\LockedException;
use OCP\PreConditionNotMetException;
use RuntimeException;
class BackgroundService {
public const DEFAULT_COLOR = '#00679e';
public const DEFAULT_BACKGROUND_COLOR = '#00679e';
// 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 = '#00679e';
/**
* One of our shipped background images is used
*/
public const BACKGROUND_SHIPPED = 'shipped';
/**
* A custom background image is used
*/
public const BACKGROUND_CUSTOM = 'custom';
/**
* The default background image is used
*/
public const BACKGROUND_DEFAULT = 'default';
/**
* Just a background color is used
*/
public const BACKGROUND_COLOR = 'color';
public const BACKGROUND_DISABLED = 'disabled';
public const DEFAULT_BACKGROUND_IMAGE = 'kamil-porembinski-clouds.jpg';
/**
* 'attribution': Name, artist and license
* 'description': Alternative text
* 'attribution_url': URL for attribution
* 'background_color': Cached mean color of the top part to calculate app menu colors and use as fallback
* 'primary_color': Recommended primary color for this theme / image
*/
public const SHIPPED_BACKGROUNDS = [
'hannah-maclean-soft-floral.jpg' => [
'attribution' => 'Soft floral (Hannah MacLean, CC0)',
'description' => 'Abstract background picture in yellow and white color whith a flower on it',
'attribution_url' => 'https://stocksnap.io/photo/soft-floral-XOYWCCW5PA',
'background_color' => '#e4d2c1',
'primary_color' => '#9f652f',
'theming' => self::THEMING_MODE_DARK,
'primary_color' => '#D8A06C',
],
'ted-moravec-morning-fog.jpg' => [
'attribution' => 'Morning fog (Ted Moravec, Public Domain)',
'description' => 'Background picture of a forest shrouded in fog',
'attribution_url' => 'https://flickr.com/photos/tmoravec/52392410261',
'background_color' => '#f6f7f6',
'primary_color' => '#114c3b',
'theming' => self::THEMING_MODE_DARK,
'primary_color' => '#38A084',
],
'stefanus-martanto-setyo-husodo-underwater-ocean.jpg' => [
'attribution' => 'Underwater ocean (Stefanus Martanto Setyo Husodo, CC0)',
'description' => 'Background picture of an underwater ocean',
'attribution_url' => 'https://stocksnap.io/photo/underwater-ocean-TJA9LBH4WS',
'background_color' => '#003351',
'primary_color' => '#04577e',
],
'zoltan-voros-rhythm-and-blues.jpg' => [
'attribution' => 'Rhythm and blues (Zoltán Vörös, CC BY)',
'description' => 'Abstract background picture of sand dunes during night',
'attribution_url' => 'https://flickr.com/photos/v923z/51634409289/',
'background_color' => '#1c2437',
'primary_color' => '#1c243c',
],
'anatoly-mikhaltsov-butterfly-wing-scale.jpg' => [
'attribution' => 'Butterfly wing scale (Anatoly Mikhaltsov, CC BY-SA)',
'description' => 'Background picture of a red-ish butterfly wing under microscope',
'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',
'background_color' => '#652e11',
'primary_color' => '#a53c17',
],
'bernie-cetonia-aurata-take-off-composition.jpg' => [
'attribution' => 'Cetonia aurata take off composition (Bernie, Public Domain)',
'description' => 'Montage of a cetonia aurata bug that takes off with white background',
'attribution_url' => 'https://commons.wikimedia.org/wiki/File:Cetonia_aurata_take_off_composition_05172009.jpg',
'background_color' => '#dee0d3',
'primary_color' => '#56633d',
'theming' => self::THEMING_MODE_DARK,
'primary_color' => '#869171',
],
'dejan-krsmanovic-ribbed-red-metal.jpg' => [
'attribution' => 'Ribbed red metal (Dejan Krsmanovic, CC BY)',
'description' => 'Abstract background picture of red ribbed metal with two horizontal white elements on top of it',
'attribution_url' => 'https://www.flickr.com/photos/dejankrsmanovic/42971456774/',
'background_color' => '#9b171c',
'primary_color' => '#9c4236',
],
'eduardo-neves-pedra-azul.jpg' => [
'attribution' => 'Pedra azul milky way (Eduardo Neves, CC BY-SA)',
'description' => 'Background picture of the milky way during night with a mountain in front of it',
'attribution_url' => 'https://commons.wikimedia.org/wiki/File:Pedra_Azul_Milky_Way.jpg',
'background_color' => '#1d242d',
'primary_color' => '#4f6071',
],
'european-space-agency-barents-bloom.jpg' => [
'attribution' => 'Barents bloom (European Space Agency, CC BY-SA)',
'description' => 'Abstract background picture of blooming barents in blue and green colors',
'attribution_url' => 'https://www.esa.int/ESA_Multimedia/Images/2016/08/Barents_bloom',
'background_color' => '#1c383d',
'primary_color' => '#396475',
],
'hannes-fritz-flippity-floppity.jpg' => [
'attribution' => 'Flippity floppity (Hannes Fritz, CC BY-SA)',
'description' => 'Abstract background picture of many pairs of flip flops hanging on a wall in multiple colors',
'attribution_url' => 'http://hannes.photos/flippity-floppity',
'background_color' => '#5b2d53',
'primary_color' => '#98415a',
],
'hannes-fritz-roulette.jpg' => [
'attribution' => 'Roulette (Hannes Fritz, CC BY-SA)',
'description' => 'Background picture of a rotating giant wheel during night',
'attribution_url' => 'http://hannes.photos/roulette',
'background_color' => '#000000',
'primary_color' => '#845334',
],
'hannes-fritz-sea-spray.jpg' => [
'attribution' => 'Sea spray (Hannes Fritz, CC BY-SA)',
'description' => 'Background picture of a stone coast with fog and sea behind it',
'attribution_url' => 'http://hannes.photos/sea-spray',
'background_color' => '#333f47',
'primary_color' => '#4f6071',
],
'kamil-porembinski-clouds.jpg' => [
'attribution' => 'Clouds (Kamil Porembiński, CC BY-SA)',
'description' => 'Background picture of white clouds on in front of a blue sky',
'attribution_url' => 'https://www.flickr.com/photos/paszczak000/8715851521/',
'background_color' => self::DEFAULT_BACKGROUND_COLOR,
'primary_color' => self::DEFAULT_COLOR,
],
'bernard-spragg-new-zealand-fern.jpg' => [
'attribution' => 'New zealand fern (Bernard Spragg, CC0)',
'description' => 'Abstract background picture of fern leafes',
'attribution_url' => 'https://commons.wikimedia.org/wiki/File:NZ_Fern.(Blechnum_chambersii)_(11263534936).jpg',
'background_color' => '#0c3c03',
'primary_color' => '#316b26',
],
'rawpixel-pink-tapioca-bubbles.jpg' => [
'attribution' => 'Pink tapioca bubbles (Rawpixel, CC BY)',
'description' => 'Abstract background picture of pink tapioca bubbles',
'attribution_url' => 'https://www.flickr.com/photos/byrawpixel/27665140298/in/photostream/',
'background_color' => '#c56e95',
'primary_color' => '#7b4e7e',
'theming' => self::THEMING_MODE_DARK,
'primary_color' => '#b17ab4',
],
'nasa-waxing-crescent-moon.jpg' => [
'attribution' => 'Waxing crescent moon (NASA, Public Domain)',
'description' => 'Background picture of glowing earth in foreground and moon in the background',
'attribution_url' => 'https://www.nasa.gov/image-feature/a-waxing-crescent-moon',
'background_color' => '#000002',
'primary_color' => '#005ac1',
],
'tommy-chau-already.jpg' => [
'attribution' => 'Cityscape (Tommy Chau, CC BY)',
'description' => 'Background picture of a skyscraper city during night',
'attribution_url' => 'https://www.flickr.com/photos/90975693@N05/16910999368',
'background_color' => '#35229f',
'primary_color' => '#6a2af4',
],
'tommy-chau-lion-rock-hill.jpg' => [
'attribution' => 'Lion rock hill (Tommy Chau, CC BY)',
'description' => 'Background picture of mountains during sunset or sunrise',
'attribution_url' => 'https://www.flickr.com/photos/90975693@N05/17136440246',
'background_color' => '#cb92b7',
'primary_color' => '#7f4f70',
'theming' => self::THEMING_MODE_DARK,
'primary_color' => '#c074a9',
],
'lali-masriera-yellow-bricks.jpg' => [
'attribution' => 'Yellow bricks (Lali Masriera, CC BY)',
'description' => 'Background picture of yellow bricks with some yellow tubes',
'attribution_url' => 'https://www.flickr.com/photos/visualpanic/3982464447',
'background_color' => '#c78a19',
'primary_color' => '#7f5700',
'theming' => self::THEMING_MODE_DARK,
'primary_color' => '#bc8210',
],
];
public function __construct(
private IRootFolder $rootFolder,
private IAppData $appData,
private IConfig $config,
private ?string $userId,
) {
private IRootFolder $rootFolder;
private IAppData $appData;
private IConfig $config;
private string $userId;
private ThemingDefaults $themingDefaults;
public function __construct(IRootFolder $rootFolder,
IAppData $appData,
IConfig $config,
?string $userId,
ThemingDefaults $themingDefaults) {
if ($userId === null) {
return;
}
$this->rootFolder = $rootFolder;
$this->config = $config;
$this->userId = $userId;
$this->appData = $appData;
$this->themingDefaults = $themingDefaults;
}
public function setDefaultBackground(): void {
$this->config->deleteUserValue($this->userId, Application::APP_ID, 'background_image');
$this->config->deleteUserValue($this->userId, Application::APP_ID, 'background_color');
$this->config->deleteUserValue($this->userId, Application::APP_ID, 'primary_color');
}
/**
@@ -232,9 +213,7 @@ class BackgroundService {
* @throws NoUserException
*/
public function setFileBackground($path): void {
if ($this->userId === null) {
throw new RuntimeException('No currently logged-in user');
}
$this->config->setUserValue($this->userId, Application::APP_ID, 'background_image', self::BACKGROUND_CUSTOM);
$userFolder = $this->rootFolder->getUserFolder($this->userId);
/** @var File $file */
@@ -245,46 +224,26 @@ class BackgroundService {
throw new InvalidArgumentException('Invalid image file');
}
$meanColor = $this->calculateMeanColor($image);
if ($meanColor !== false) {
$this->setColorBackground($meanColor);
}
$this->getAppDataFolder()->newFile('background.jpg', $file->fopen('r'));
$this->config->setUserValue($this->userId, Application::APP_ID, 'background_image', self::BACKGROUND_CUSTOM);
}
public function setShippedBackground($fileName): void {
if ($this->userId === null) {
throw new RuntimeException('No currently logged-in user');
}
if (!array_key_exists($fileName, self::SHIPPED_BACKGROUNDS)) {
throw new InvalidArgumentException('The given file name is invalid');
}
$this->setColorBackground(self::SHIPPED_BACKGROUNDS[$fileName]['background_color']);
$this->config->setUserValue($this->userId, Application::APP_ID, 'background_image', $fileName);
$this->config->setUserValue($this->userId, Application::APP_ID, 'primary_color', self::SHIPPED_BACKGROUNDS[$fileName]['primary_color']);
$this->setColorBackground(self::SHIPPED_BACKGROUNDS[$fileName]['primary_color']);
}
/**
* Set the background to color only
*/
public function setColorBackground(string $color): void {
if ($this->userId === null) {
throw new RuntimeException('No currently logged-in user');
}
if (!preg_match('/^#([0-9a-f]{3}|[0-9a-f]{6})$/i', $color)) {
throw new InvalidArgumentException('The given color is invalid');
}
$this->config->setUserValue($this->userId, Application::APP_ID, 'background_color', $color);
$this->config->setUserValue($this->userId, Application::APP_ID, 'background_image', self::BACKGROUND_COLOR);
}
public function deleteBackgroundImage(): void {
if ($this->userId === null) {
throw new RuntimeException('No currently logged-in user');
}
$this->config->setUserValue($this->userId, Application::APP_ID, 'background_image', self::BACKGROUND_COLOR);
$this->config->setUserValue($this->userId, Application::APP_ID, 'background_image', self::BACKGROUND_DISABLED);
}
public function getBackground(): ?ISimpleFile {
@@ -299,82 +258,6 @@ class BackgroundService {
return null;
}
/**
* Called when a new global background (backgroundMime) is uploaded (admin setting)
* This sets all necessary app config values
* @param resource|string $path
* @return string|null The fallback background color - if any
*/
public function setGlobalBackground($path): string|null {
$image = new \OCP\Image();
$handle = is_resource($path) ? $path : fopen($path, 'rb');
if ($handle && $image->loadFromFileHandle($handle) !== false) {
$meanColor = $this->calculateMeanColor($image);
if ($meanColor !== false) {
$this->config->setAppValue(Application::APP_ID, 'background_color', $meanColor);
return $meanColor;
}
}
return null;
}
/**
* Calculate mean color of an given image
* It only takes the upper part into account so that a matching text color can be derived for the app menu
*/
private function calculateMeanColor(\OCP\Image $image): false|string {
/**
* Small helper to ensure one channel is returned as 8byte hex
*/
function toHex(int $channel): string {
$hex = dechex($channel);
return match (strlen($hex)) {
0 => '00',
1 => '0'.$hex,
2 => $hex,
default => 'ff',
};
}
$tempImage = new \OCP\Image();
// Crop to only analyze top bar
$resource = $image->cropNew(0, 0, $image->width(), min(max(50, (int)($image->height() * 0.125)), $image->height()));
if ($resource === false) {
return false;
}
$tempImage->setResource($resource);
if (!$tempImage->preciseResize(100, 7)) {
return false;
}
$resource = $tempImage->resource();
if ($resource === false) {
return false;
}
$reds = [];
$greens = [];
$blues = [];
for ($y = 0; $y < 7; $y++) {
for ($x = 0; $x < 100; $x++) {
$value = imagecolorat($resource, $x, $y);
if ($value === false) {
continue;
}
$reds[] = ($value >> 16) & 0xFF;
$greens[] = ($value >> 8) & 0xFF;
$blues[] = $value & 0xFF;
}
}
$meanColor = '#' . toHex((int)(array_sum($reds) / count($reds)));
$meanColor .= toHex((int)(array_sum($greens) / count($greens)));
$meanColor .= toHex((int)(array_sum($blues) / count($blues)));
return $meanColor;
}
/**
* Storing the data in appdata/theming/users/USERID
*
+13 -15
View File
@@ -28,40 +28,38 @@ namespace OCA\Theming\Service;
use OCA\Theming\ThemingDefaults;
use OCA\Theming\Util;
use OCP\IConfig;
class JSDataService implements \JsonSerializable {
private ThemingDefaults $themingDefaults;
private Util $util;
private IConfig $appConfig;
private ThemesService $themesService;
public function __construct(
private ThemingDefaults $themingDefaults,
private Util $util,
private ThemesService $themesService,
ThemingDefaults $themingDefaults,
Util $util,
IConfig $appConfig,
ThemesService $themesService
) {
$this->themingDefaults = $themingDefaults;
$this->util = $util;
$this->appConfig = $appConfig;
$this->themesService = $themesService;
}
public function jsonSerialize(): array {
return [
'name' => $this->themingDefaults->getName(),
'slogan' => $this->themingDefaults->getSlogan(),
'url' => $this->themingDefaults->getBaseUrl(),
'slogan' => $this->themingDefaults->getSlogan(),
'color' => $this->themingDefaults->getColorPrimary(),
'defaultColor' => $this->themingDefaults->getDefaultColorPrimary(),
'imprintUrl' => $this->themingDefaults->getImprintUrl(),
'privacyUrl' => $this->themingDefaults->getPrivacyUrl(),
'primaryColor' => $this->themingDefaults->getColorPrimary(),
'backgroundColor' => $this->themingDefaults->getColorBackground(),
'defaultPrimaryColor' => $this->themingDefaults->getDefaultColorPrimary(),
'defaultBackgroundColor' => $this->themingDefaults->getDefaultColorBackground(),
'inverted' => $this->util->invertTextColor($this->themingDefaults->getColorPrimary()),
'cacheBuster' => $this->util->getCacheBuster(),
'enabledThemes' => $this->themesService->getEnabledThemes(),
// deprecated use primaryColor
'color' => $this->themingDefaults->getColorPrimary(),
'' => 'color is deprecated since Nextcloud 29, use primaryColor instead'
];
}
}
+1 -6
View File
@@ -30,7 +30,6 @@ namespace OCA\Theming\Settings;
use OCA\Theming\AppInfo\Application;
use OCA\Theming\Controller\ThemingController;
use OCA\Theming\ImageManager;
use OCA\Theming\Service\BackgroundService;
use OCA\Theming\ThemingDefaults;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
@@ -76,13 +75,9 @@ class Admin implements IDelegatedSettings {
'name' => $this->themingDefaults->getEntity(),
'url' => $this->themingDefaults->getBaseUrl(),
'slogan' => $this->themingDefaults->getSlogan(),
'primaryColor' => $this->themingDefaults->getDefaultColorPrimary(),
'backgroundColor' => $this->themingDefaults->getDefaultColorBackground(),
'color' => $this->themingDefaults->getDefaultColorPrimary(),
'logoMime' => $this->config->getAppValue(Application::APP_ID, 'logoMime', ''),
'allowedMimeTypes' => $allowedMimeTypes,
'backgroundURL' => $this->imageManager->getImageUrl('background'),
'defaultBackgroundURL' => $this->urlGenerator->linkTo(Application::APP_ID, 'img/background/' . BackgroundService::DEFAULT_BACKGROUND_IMAGE),
'defaultBackgroundColor' => BackgroundService::DEFAULT_BACKGROUND_COLOR,
'backgroundMime' => $this->config->getAppValue(Application::APP_ID, 'backgroundMime', ''),
'logoheaderMime' => $this->config->getAppValue(Application::APP_ID, 'logoheaderMime', ''),
'faviconMime' => $this->config->getAppValue(Application::APP_ID, 'faviconMime', ''),
-21
View File
@@ -26,7 +26,6 @@
namespace OCA\Theming\Settings;
use OCA\Theming\ITheme;
use OCA\Theming\Service\BackgroundService;
use OCA\Theming\Service\ThemesService;
use OCA\Theming\ThemingDefaults;
use OCP\App\IAppManager;
@@ -72,26 +71,6 @@ class Personal implements ISettings {
// Get the default app enforced by admin
$forcedDefaultApp = $this->appManager->getDefaultAppForUser(null, false);
/** List of all shipped backgrounds */
$this->initialStateService->provideInitialState('shippedBackgrounds', BackgroundService::SHIPPED_BACKGROUNDS);
/**
* Admin theming
*/
$this->initialStateService->provideInitialState('themingDefaults', [
/** URL of admin configured background image */
'backgroundImage' => $this->themingDefaults->getBackground(),
/** `backgroundColor` if disabled, mime type if defined and empty by default */
'backgroundMime' => $this->config->getAppValue('theming', 'backgroundMime', ''),
/** Admin configured background color */
'backgroundColor' => $this->themingDefaults->getDefaultColorBackground(),
/** Admin configured primary color */
'primaryColor' => $this->themingDefaults->getDefaultColorPrimary(),
/** Nextcloud default background image */
'defaultShippedBackground' => BackgroundService::DEFAULT_BACKGROUND_IMAGE,
]);
$this->initialStateService->provideInitialState('userBackgroundImage', $this->config->getUserValue($this->userId, 'theming', 'background_image', BackgroundService::BACKGROUND_DEFAULT));
$this->initialStateService->provideInitialState('themes', array_values($themes));
$this->initialStateService->provideInitialState('enforceTheme', $enforcedTheme);
$this->initialStateService->provideInitialState('isUserThemingDisabled', $this->themingDefaults->isUserThemingDisabled());
+43 -33
View File
@@ -6,7 +6,6 @@ declare(strict_types=1);
*
* @author Joas Schilling <coding@schilljs.com>
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Ferdinand Thiessen <opensource@fthiessen.de>
*
* @license GNU AGPL version 3 or any later version
*
@@ -29,12 +28,10 @@ namespace OCA\Theming\Themes;
use OCA\Theming\AppInfo\Application;
use OCA\Theming\ImageManager;
use OCA\Theming\Service\BackgroundService;
use OCA\Theming\ThemingDefaults;
use OCA\Theming\Util;
trait CommonThemeTrait {
public Util $util;
public ThemingDefaults $themingDefaults;
/**
* Generate primary-related variables
@@ -61,6 +58,7 @@ trait CommonThemeTrait {
'--primary-invert-if-dark' => $this->util->invertTextColor($colorPrimaryElement) ? 'no' : 'invert(100%)',
'--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,
@@ -90,33 +88,34 @@ trait CommonThemeTrait {
protected function generateGlobalBackgroundVariables(): array {
$backgroundDeleted = $this->config->getAppValue(Application::APP_ID, 'backgroundMime', '') === 'backgroundColor';
$hasCustomLogoHeader = $this->util->isLogoThemed();
$backgroundColor = $this->themingDefaults->getColorBackground();
$isPrimaryBright = $this->util->invertTextColor($this->primaryColor);
$variables = [];
// Default last fallback values
$variables = [
'--color-background-plain' => $backgroundColor,
'--color-background-plain-text' => $this->util->invertTextColor($backgroundColor) ? '#000000' : '#ffffff',
'--background-image-invert-if-bright' => $this->util->invertTextColor($backgroundColor) ? 'invert(100%)' : 'no',
];
$variables['--image-background-default'] = "url('" . $this->themingDefaults->getBackground() . "')";
$variables['--color-background-plain'] = $this->primaryColor;
// Register image variables only if custom-defined
foreach (ImageManager::SUPPORTED_IMAGE_KEYS as $image) {
if ($this->imageManager->hasImage($image)) {
$imageUrl = $this->imageManager->getImageUrl($image);
// --image-background is overridden by user theming if logged in
$variables["--image-$image"] = "url('" . $imageUrl . "')";
} elseif ($image === 'background') {
// Apply default background if nothing is configured
$variables['--image-background'] = "url('" . $this->themingDefaults->getBackground() . "')";
}
}
// If a background has been requested let's not define the background image
// If primary as background has been request or if we have a custom primary colour
// let's not define the background image
if ($backgroundDeleted) {
$variables['--image-background'] = 'none';
$variables['--color-background-plain'] = $this->primaryColor;
$variables['--image-background-plain'] = 'yes';
$variables['--image-background'] = 'no';
// If no background image is set, we need to check against the shown primary colour
$variables['--background-image-invert-if-bright'] = $isPrimaryBright ? 'invert(100%)' : 'no';
}
if ($hasCustomLogoHeader) {
// prevent inverting the logo on bright colors if customized
$variables['--image-logoheader-custom'] = 'true';
}
@@ -131,37 +130,48 @@ trait CommonThemeTrait {
if ($user !== null
&& !$this->themingDefaults->isUserThemingDisabled()
&& $this->appManager->isEnabledForUser(Application::APP_ID)) {
$adminBackgroundDeleted = $this->config->getAppValue(Application::APP_ID, 'backgroundMime', '') === 'backgroundColor';
$backgroundImage = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'background_image', BackgroundService::BACKGROUND_DEFAULT);
$backgroundColor = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'background_color', $this->themingDefaults->getColorBackground());
$currentVersion = (int)$this->config->getUserValue($user->getUID(), Application::APP_ID, 'userCacheBuster', '0');
$isBackgroundBright = $this->util->invertTextColor($backgroundColor);
$backgroundTextColor = $this->util->invertTextColor($backgroundColor) ? '#000000' : '#ffffff';
$isPrimaryBright = $this->util->invertTextColor($this->primaryColor);
$variables = [
'--color-background-plain' => $backgroundColor,
'--color-background-plain-text' => $backgroundTextColor,
'--background-image-invert-if-bright' => $isBackgroundBright ? 'invert(100%)' : 'no',
];
// Only use a background color without an image
if ($backgroundImage === BackgroundService::BACKGROUND_COLOR) {
// Might be defined already by admin theming, needs to be overridden
$variables['--image-background'] = 'none';
// The user removed the background
if ($backgroundImage === BackgroundService::BACKGROUND_DISABLED) {
return [
// Might be defined already by admin theming, needs to be overridden
'--image-background' => 'none',
'--color-background-plain' => $this->primaryColor,
// If no background image is set, we need to check against the shown primary colour
'--background-image-invert-if-bright' => $isPrimaryBright ? 'invert(100%)' : 'no',
];
}
// The user uploaded a custom background
if ($backgroundImage === BackgroundService::BACKGROUND_CUSTOM) {
$cacheBuster = substr(sha1($user->getUID() . '_' . $currentVersion), 0, 8);
$variables['--image-background'] = "url('" . $this->urlGenerator->linkToRouteAbsolute('theming.userTheme.getBackground') . "?v=$cacheBuster')";
return [
'--image-background' => "url('" . $this->urlGenerator->linkToRouteAbsolute('theming.userTheme.getBackground') . "?v=$cacheBuster')",
'--color-background-plain' => $this->primaryColor,
];
}
// The user is using the default background and admin removed the background image
if ($backgroundImage === BackgroundService::BACKGROUND_DEFAULT && $adminBackgroundDeleted) {
return [
// --image-background is not defined in this case
'--color-background-plain' => $this->primaryColor,
'--background-image-invert-if-bright' => $isPrimaryBright ? 'invert(100%)' : 'no',
];
}
// The user picked a shipped background
if (isset(BackgroundService::SHIPPED_BACKGROUNDS[$backgroundImage])) {
$variables['--image-background'] = "url('" . $this->urlGenerator->linkTo(Application::APP_ID, "img/background/$backgroundImage") . "')";
return [
'--image-background' => "url('" . $this->urlGenerator->linkTo(Application::APP_ID, "img/background/$backgroundImage") . "')",
'--color-background-plain' => $this->primaryColor,
'--background-image-invert-if-bright' => BackgroundService::SHIPPED_BACKGROUNDS[$backgroundImage]['theming'] ?? null === BackgroundService::THEMING_MODE_DARK ? 'invert(100%)' : 'no',
];
}
return $variables;
}
return [];
+6
View File
@@ -27,6 +27,7 @@ namespace OCA\Theming\Themes;
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;
@@ -69,6 +70,11 @@ class DefaultTheme implements ITheme {
$this->defaultPrimaryColor = $this->themingDefaults->getDefaultColorPrimary();
$this->primaryColor = $this->themingDefaults->getColorPrimary();
// Override primary colors (if set) to improve accessibility
if ($this->primaryColor === BackgroundService::DEFAULT_COLOR) {
$this->primaryColor = BackgroundService::DEFAULT_ACCESSIBLE_COLOR;
}
}
public function getId(): string {
+59 -88
View File
@@ -7,7 +7,6 @@
* @author Bjoern Schiessle <bjoern@schiessle.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Kesselberg <mail@danielkesselberg.de>
* @author Ferdinand Thiessen <opensource@fthiessen.de>
* @author Guillaume COMPAGNON <gcompagnon@outlook.com>
* @author Jan-Christoph Borchardt <hey@jancborchardt.net>
* @author Joachim Bauch <bauch@struktur.de>
@@ -56,13 +55,22 @@ use OCP\IUserSession;
class ThemingDefaults extends \OC_Defaults {
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;
private string $name;
private string $title;
private string $entity;
private string $productName;
private string $url;
private string $backgroundColor;
private string $primaryColor;
private string $color;
private string $docBaseUrl;
private string $iTunesAppId;
@@ -72,28 +80,43 @@ class ThemingDefaults extends \OC_Defaults {
/**
* ThemingDefaults constructor.
*
* @param IConfig $config
* @param IL10N $l
* @param ImageManager $imageManager
* @param IUserSession $userSession
* @param IURLGenerator $urlGenerator
* @param ICacheFactory $cacheFactory
* @param Util $util
* @param IAppManager $appManager
*/
public function __construct(
private IConfig $config,
private IL10N $l,
private IUserSession $userSession,
private IURLGenerator $urlGenerator,
private ICacheFactory $cacheFactory,
private Util $util,
private ImageManager $imageManager,
private IAppManager $appManager,
private INavigationManager $navigationManager,
private BackgroundService $backgroundService,
public function __construct(IConfig $config,
IL10N $l,
IUserSession $userSession,
IURLGenerator $urlGenerator,
ICacheFactory $cacheFactory,
Util $util,
ImageManager $imageManager,
IAppManager $appManager,
INavigationManager $navigationManager
) {
parent::__construct();
$this->config = $config;
$this->l = $l;
$this->imageManager = $imageManager;
$this->userSession = $userSession;
$this->urlGenerator = $urlGenerator;
$this->cacheFactory = $cacheFactory;
$this->util = $util;
$this->appManager = $appManager;
$this->navigationManager = $navigationManager;
$this->name = parent::getName();
$this->title = parent::getTitle();
$this->entity = parent::getEntity();
$this->productName = parent::getProductName();
$this->url = parent::getBaseUrl();
$this->primaryColor = parent::getColorPrimary();
$this->backgroundColor = parent::getColorBackground();
$this->color = parent::getColorPrimary();
$this->iTunesAppId = parent::getiTunesAppId();
$this->iOSClientUrl = parent::getiOSClientUrl();
$this->AndroidClientUrl = parent::getAndroidClientUrl();
@@ -201,8 +224,7 @@ class ThemingDefaults extends \OC_Defaults {
}
/**
* Color that is used for highlighting elements like important buttons
* If user theming is enabled then the user defined value is returned
* Color that is used for the header as well as for mail headers
*/
public function getColorPrimary(): string {
$user = $this->userSession->getUser();
@@ -216,66 +238,32 @@ class ThemingDefaults extends \OC_Defaults {
// user-defined primary color
if (!empty($user)) {
$userPrimaryColor = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'primary_color', '');
if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $userPrimaryColor)) {
return $userPrimaryColor;
$themingBackgroundColor = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'background_color', '');
// If the user selected a specific colour
if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $themingBackgroundColor)) {
return $themingBackgroundColor;
}
}
// 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;
}
/**
* Color that is used for the page background (e.g. the header)
* If user theming is enabled then the user defined value is returned
*/
public function getColorBackground(): string {
$user = $this->userSession->getUser();
// admin-defined background color
$defaultColor = $this->getDefaultColorBackground();
if ($this->isUserThemingDisabled()) {
return $defaultColor;
}
// user-defined background color
if (!empty($user)) {
$userPrimaryColor = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'background_color', '');
if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $userPrimaryColor)) {
return $userPrimaryColor;
}
}
// Finally, return the system global background color
return $defaultColor;
}
/**
* Return the default primary color - only taking admin setting into account
* Return the default color primary
*/
public function getDefaultColorPrimary(): string {
// try admin color
$defaultColor = $this->config->getAppValue(Application::APP_ID, 'primary_color', '');
if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $defaultColor)) {
return $defaultColor;
$color = $this->config->getAppValue(Application::APP_ID, 'color', '');
if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $color)) {
return BackgroundService::DEFAULT_COLOR;
}
// fall back to default primary color
return $this->primaryColor;
}
/**
* Default background color only taking admin setting into account
*/
public function getDefaultColorBackground(): string {
$defaultColor = $this->config->getAppValue(Application::APP_ID, 'background_color', '');
if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $defaultColor)) {
return $defaultColor;
}
return $this->backgroundColor;
return $color;
}
/**
@@ -356,7 +344,6 @@ class ThemingDefaults extends \OC_Defaults {
/**
* @return array scss variables to overwrite
* @deprecated since Nextcloud 22 - https://github.com/nextcloud/server/issues/9940
*/
public function getScssVariables() {
$cacheBuster = $this->config->getAppValue('theming', 'cachebuster', '0');
@@ -379,7 +366,7 @@ class ThemingDefaults extends \OC_Defaults {
$variables['image-login-background'] = "url('".$this->imageManager->getImageUrl('background')."')";
$variables['image-login-plain'] = 'false';
if ($this->config->getAppValue('theming', 'primary_color', '') !== '') {
if ($this->config->getAppValue('theming', 'color', '') !== '') {
$variables['color-primary'] = $this->getColorPrimary();
$variables['color-primary-text'] = $this->getTextColorPrimary();
$variables['color-primary-element'] = $this->util->elementColor($this->getColorPrimary());
@@ -478,7 +465,7 @@ class ThemingDefaults extends \OC_Defaults {
}
/**
* Revert admin settings to the default value
* Revert settings to the default value
*
* @param string $setting setting which should be reverted
* @return string default value
@@ -498,15 +485,8 @@ class ThemingDefaults extends \OC_Defaults {
case 'slogan':
$returnValue = $this->getSlogan();
break;
case 'primary_color':
$returnValue = BackgroundService::DEFAULT_COLOR;
break;
case 'background_color':
// If a background image is set we revert to the mean image color
if ($this->imageManager->hasImage('background')) {
$file = $this->imageManager->getImage('background');
$returnValue = $this->backgroundService->setGlobalBackground($file->read()) ?? '';
}
case 'color':
$returnValue = $this->getDefaultColorPrimary();
break;
case 'logo':
case 'logoheader':
@@ -521,16 +501,7 @@ class ThemingDefaults extends \OC_Defaults {
}
/**
* Color of text in the header menu
*
* @return string
*/
public function getTextColorBackground() {
return $this->util->invertTextColor($this->getColorBackground()) ? '#000000' : '#ffffff';
}
/**
* Color of text on primary buttons and other elements
* Color of text in the header and primary buttons
*
* @return string
*/
+1 -1
View File
@@ -93,7 +93,7 @@ class Util {
$contrast = $this->colorContrast($color, $blurredBackground);
// Min. element contrast is 3:1 but we need to keep hover states in mind -> min 3.2:1
$minContrast = $highContrast ? 5.6 : 3.2;
$minContrast = $highContrast ? 5.5 : 3.2;
while ($contrast < $minContrast && $iteration++ < 100) {
$hsl = Color::hexToHsl($color);
-4
View File
@@ -63,7 +63,6 @@
"color-element-dark",
"logo",
"background",
"background-text",
"background-plain",
"background-default",
"logoheader",
@@ -100,9 +99,6 @@
"background": {
"type": "string"
},
"background-text": {
"type": "string"
},
"background-plain": {
"type": "boolean"
},
+60 -117
View File
@@ -44,50 +44,31 @@
:placeholder="field.placeholder"
:type="field.type"
:value.sync="field.value"
@update:theming="refreshStyles" />
@update:theming="$emit('update:theming')" />
<!-- Primary color picker -->
<ColorPickerField :name="primaryColorPickerField.name"
:description="primaryColorPickerField.description"
:default-value="primaryColorPickerField.defaultValue"
:display-name="primaryColorPickerField.displayName"
:value.sync="primaryColorPickerField.value"
data-admin-theming-setting-primary-color
@update:theming="refreshStyles" />
<!-- Background color picker -->
<ColorPickerField name="background_color"
:description="t('theming', 'Instead of a background image you can also configure a plain background color. If you use a background image changing this color will influence the color of the app menu icons.')"
:default-value.sync="defaultBackgroundColor"
:display-name="t('theming', 'Background color')"
:value.sync="backgroundColor"
data-admin-theming-setting-background-color
@update:theming="refreshStyles" />
<ColorPickerField :name="colorPickerField.name"
:default-value="colorPickerField.defaultValue"
:display-name="colorPickerField.displayName"
:value.sync="colorPickerField.value"
@update:theming="$emit('update:theming')" />
<!-- Default background picker -->
<FileInputField :aria-label="t('theming', 'Upload new logo')"
data-admin-theming-setting-file="logo"
:display-name="t('theming', 'Logo')"
mime-name="logoMime"
:mime-value.sync="logoMime"
name="logo"
@update:theming="refreshStyles" />
<FileInputField :aria-label="t('theming', 'Upload new background and login image')"
data-admin-theming-setting-file="background"
:display-name="t('theming', 'Background and login image')"
mime-name="backgroundMime"
:mime-value.sync="backgroundMime"
name="background"
@uploaded="backgroundURL = $event"
@update:theming="refreshStyles" />
<FileInputField v-for="field in fileInputFields"
:key="field.name"
:aria-label="field.ariaLabel"
:data-admin-theming-setting-file="field.name"
:default-mime-value="field.defaultMimeValue"
:display-name="field.displayName"
:mime-name="field.mimeName"
:mime-value.sync="field.mimeValue"
:name="field.name"
@update:theming="$emit('update:theming')" />
<div class="admin-theming__preview" data-admin-theming-preview>
<div class="admin-theming__preview-logo" data-admin-theming-preview-logo />
</div>
</div>
</NcSettingsSection>
<NcSettingsSection :name="t('theming', 'Advanced options')">
<div class="admin-theming-advanced">
<TextField v-for="field in advancedTextFields"
@@ -99,7 +80,7 @@
:display-name="field.displayName"
:placeholder="field.placeholder"
:maxlength="field.maxlength"
@update:theming="refreshStyles" />
@update:theming="$emit('update:theming')" />
<FileInputField v-for="field in advancedFileInputFields"
:key="field.name"
:name="field.name"
@@ -108,7 +89,7 @@
:default-mime-value="field.defaultMimeValue"
:display-name="field.displayName"
:aria-label="field.ariaLabel"
@update:theming="refreshStyles" />
@update:theming="$emit('update:theming')" />
<CheckboxField :name="userThemingField.name"
:value="userThemingField.value"
:default-value="userThemingField.defaultValue"
@@ -116,7 +97,7 @@
:label="userThemingField.label"
:description="userThemingField.description"
data-admin-theming-setting-disable-user-theming
@update:theming="refreshStyles" />
@update:theming="$emit('update:theming')" />
<a v-if="!canThemeIcons"
:href="docUrlIcons"
rel="noreferrer noopener">
@@ -130,7 +111,6 @@
<script>
import { loadState } from '@nextcloud/initial-state'
import { refreshStyles } from './helpers/refreshStyles.js'
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
@@ -141,12 +121,9 @@ import TextField from './components/admin/TextField.vue'
import AppMenuSection from './components/admin/AppMenuSection.vue'
const {
defaultBackgroundURL,
backgroundMime,
backgroundURL,
backgroundColor,
canThemeIcons,
color,
docUrl,
docUrlIcons,
faviconMime,
@@ -156,7 +133,6 @@ const {
logoMime,
name,
notThemableErrorMessage,
primaryColor,
privacyPolicyUrl,
slogan,
url,
@@ -194,14 +170,32 @@ const textFields = [
},
]
const primaryColorPickerField = {
name: 'primary_color',
value: primaryColor,
const colorPickerField = {
name: 'color',
value: color,
defaultValue: '#0082c9',
displayName: t('theming', 'Primary color'),
description: t('theming', 'The primary color is used for highlighting elements like important buttons. It might get slightly adjusted depending on the current color schema.'),
displayName: t('theming', 'Color'),
}
const fileInputFields = [
{
name: 'logo',
mimeName: 'logoMime',
mimeValue: logoMime,
defaultMimeValue: '',
displayName: t('theming', 'Logo'),
ariaLabel: t('theming', 'Upload new logo'),
},
{
name: 'background',
mimeName: 'backgroundMime',
mimeValue: backgroundMime,
defaultMimeValue: '',
displayName: t('theming', 'Background and login image'),
ariaLabel: t('theming', 'Upload new background and login image'),
},
]
const advancedTextFields = [
{
name: 'imprintUrl',
@@ -264,17 +258,17 @@ export default {
TextField,
},
emits: [
'update:theming',
],
textFields,
data() {
return {
backgroundMime,
backgroundURL,
backgroundColor,
defaultBackgroundColor: '#0069c3',
logoMime,
textFields,
primaryColorPickerField,
colorPickerField,
fileInputFields,
advancedTextFields,
advancedFileInputFields,
userThemingField,
@@ -287,64 +281,6 @@ export default {
notThemableErrorMessage,
}
},
computed: {
cssBackgroundImage() {
if (this.backgroundURL) {
return `url('${this.backgroundURL}')`
}
return 'unset'
},
},
watch: {
backgroundMime() {
if (this.backgroundMime === '') {
// Reset URL to default value for preview
this.backgroundURL = defaultBackgroundURL
} else if (this.backgroundMime === 'backgroundColor') {
// Reset URL to empty image when only color is configured
this.backgroundURL = ''
}
},
async backgroundURL() {
// When the background is changed we need to emulate the background color change
if (this.backgroundURL !== '') {
const color = await this.calculateDefaultBackground()
this.defaultBackgroundColor = color
this.backgroundColor = color
}
},
},
async mounted() {
if (this.backgroundURL) {
this.defaultBackgroundColor = await this.calculateDefaultBackground()
}
},
methods: {
refreshStyles,
/**
* Same as on server - if a user uploads an image the mean color will be set as the background color
*/
calculateDefaultBackground() {
const toHex = (num) => `00${num.toString(16)}`.slice(-2)
return new Promise((resolve, reject) => {
const img = new Image()
img.src = this.backgroundURL
img.onload = () => {
const context = document.createElement('canvas').getContext('2d')
context.imageSmoothingEnabled = true
context.drawImage(img, 0, 0, 1, 1)
resolve('#' + [...context.getImageData(0, 0, 1, 1).data.slice(0, 3)].map(toHex).join(''))
}
img.onerror = reject
})
},
},
}
</script>
@@ -364,8 +300,15 @@ export default {
background-position: center;
text-align: center;
margin-top: 10px;
background-color: v-bind('backgroundColor');
background-image: v-bind('cssBackgroundImage');
/* This is basically https://github.com/nextcloud/server/blob/master/core/css/guest.css
But without the user variables. That way the admin can preview the render as guest*/
/* As guest, there is no user color color-background-plain */
background-color: var(--color-primary-element-default);
/* As guest, there is no user background (--image-background)
1. Empty background if defined
2. Else default background
3. Finally default gradient (should not happened, the background is always defined anyway) */
background-image: var(--image-background-plain, var(--image-background-default));
&-logo {
width: 20%;
@@ -53,27 +53,20 @@
</div>
</NcSettingsSection>
<NcSettingsSection :name="t('theming', 'Primary color')"
:description="isUserThemingDisabled
? t('theming', 'Customization has been disabled by your administrator')
: t('theming', 'Set a primary color to highlight important elements. The color used for elements such as primary buttons might differ a bit as it gets adjusted to fulfill accessibility requirements.')">
<UserPrimaryColor v-if="!isUserThemingDisabled"
ref="primaryColor"
@refresh-styles="refreshGlobalStyles" />
<NcSettingsSection :name="t('theming', 'Background and color')"
class="background"
data-user-theming-background-disabled>
<template v-if="isUserThemingDisabled">
<p>{{ t('theming', 'Customization has been disabled by your administrator') }}</p>
</template>
<template v-else>
<p>{{ t('theming', 'The background can be set to an image from the default set, a custom uploaded image, or a plain color. The primary color will automatically be adapted based on this and used for elements like folder icons, primary buttons and highlights.') }}</p>
<BackgroundSettings class="background__grid" @update:background="refreshGlobalStyles" />
</template>
</NcSettingsSection>
<NcSettingsSection class="background"
:name="t('theming', 'Background and color')"
:description="isUserThemingDisabled
? t('theming', 'Customization has been disabled by your administrator')
: t('theming', 'The background can be set to an image from the default set, a custom uploaded image, or a plain color.')">
<BackgroundSettings v-if="!isUserThemingDisabled"
class="background__grid"
@update:background="refreshGlobalStyles" />
</NcSettingsSection>
<NcSettingsSection :name="t('theming', 'Keyboard shortcuts')"
:description="t('theming', 'In some cases keyboard shortcuts can interfere with accessibility tools. In order to allow focusing on your tool correctly you can disable all keyboard shortcuts here. This will also disable all available shortcuts in apps.')">
<NcSettingsSection :name="t('theming', 'Keyboard shortcuts')">
<p>{{ t('theming', 'In some cases keyboard shortcuts can interfere with accessibility tools. In order to allow focusing on your tool correctly you can disable all keyboard shortcuts here. This will also disable all available shortcuts in apps.') }}</p>
<NcCheckboxRadioSwitch class="theming__preview-toggle"
:checked.sync="shortcutsDisabled"
type="switch"
@@ -89,17 +82,13 @@
<script>
import { generateOcsUrl } from '@nextcloud/router'
import { loadState } from '@nextcloud/initial-state'
import { refreshStyles } from './helpers/refreshStyles'
import axios from '@nextcloud/axios'
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
import BackgroundSettings from './components/BackgroundSettings.vue'
import ItemPreview from './components/ItemPreview.vue'
import UserAppMenuSection from './components/UserAppMenuSection.vue'
import UserPrimaryColor from './components/UserPrimaryColor.vue'
const availableThemes = loadState('theming', 'themes', [])
const enforceTheme = loadState('theming', 'enforceTheme', '')
@@ -108,7 +97,7 @@ const shortcutsDisabled = loadState('theming', 'shortcutsDisabled', false)
const isUserThemingDisabled = loadState('theming', 'isUserThemingDisabled')
export default {
name: 'UserTheming',
name: 'UserThemes',
components: {
ItemPreview,
@@ -116,7 +105,6 @@ export default {
NcSettingsSection,
BackgroundSettings,
UserAppMenuSection,
UserPrimaryColor,
},
data() {
@@ -185,9 +173,20 @@ export default {
methods: {
// Refresh server-side generated theming CSS
async refreshGlobalStyles() {
await refreshStyles()
this.$nextTick(() => this.$refs.primaryColor.reload())
refreshGlobalStyles() {
[...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)
})
},
updateBackground(data) {
this.background = (data.type === 'custom' || data.type === 'default') ? data.type : data.value
this.refreshGlobalStyles()
},
changeTheme({ enabled, id }) {
+2
View File
@@ -22,6 +22,7 @@
import { getRequestToken } from '@nextcloud/auth'
import Vue from 'vue'
import { refreshStyles } from './helpers/refreshStyles.js'
import App from './AdminTheming.vue'
// eslint-disable-next-line camelcase
@@ -33,3 +34,4 @@ Vue.prototype.t = t
const View = Vue.extend(App)
const theming = new View()
theming.$mount('#admin-theming')
theming.$on('update:theming', refreshStyles)
@@ -32,34 +32,15 @@
'background background__filepicker': true,
'background--active': backgroundImage === 'custom'
}"
:data-color-bright="invertTextColor(Theming.color)"
data-user-theming-background-custom
tabindex="0"
@click="pickFile">
{{ t('theming', 'Custom background') }}
<ImageEdit v-if="backgroundImage !== 'custom'" :size="20" />
<ImageEdit v-if="backgroundImage !== 'custom'" :size="26" />
<Check :size="44" />
</button>
<!-- Custom color picker -->
<NcColorPicker v-model="Theming.backgroundColor" @update:value="debouncePickColor">
<button :class="{
'icon-loading': loading === 'color',
'background background__color': true,
'background--active': backgroundImage === 'color'
}"
:aria-pressed="backgroundImage === 'color'"
:data-color="Theming.backgroundColor"
:data-color-bright="invertTextColor(Theming.backgroundColor)"
:style="{ backgroundColor: Theming.backgroundColor, '--border-color': Theming.backgroundColor}"
data-user-theming-background-color
tabindex="0"
@click="backgroundImage !== 'color' && debouncePickColor(Theming.backgroundColor)">
{{ t('theming', 'Plain background') /* TRANSLATORS: Background using a single color */ }}
<ColorPalette v-if="backgroundImage !== 'color'" :size="20" />
<Check :size="44" />
</button>
</NcColorPicker>
<!-- Default background -->
<button :aria-pressed="backgroundImage === 'default'"
:class="{
@@ -67,8 +48,8 @@
'background background__default': true,
'background--active': backgroundImage === 'default'
}"
:data-color-bright="invertTextColor(Theming.defaultBackgroundColor)"
:style="{ '--border-color': Theming.defaultBackgroundColor }"
:data-color-bright="invertTextColor(Theming.defaultColor)"
:style="{ '--border-color': Theming.defaultColor }"
data-user-theming-background-default
tabindex="0"
@click="setDefault">
@@ -76,6 +57,31 @@
<Check :size="44" />
</button>
<!-- Custom color picker -->
<div class="background-color"
data-user-theming-background-color>
<NcColorPicker v-model="Theming.color"
@input="debouncePickColor">
<NcButton type="ternary">
{{ t('theming', 'Change color') }}
</NcButton>
</NcColorPicker>
</div>
<!-- Remove background -->
<button :aria-pressed="isBackgroundDisabled"
:class="{
'background background__delete': true,
'background--active': isBackgroundDisabled
}"
data-user-theming-background-clear
tabindex="0"
@click="removeBackground">
{{ t('theming', 'No background') }}
<Close v-if="!isBackgroundDisabled" :size="32" />
<Check :size="44" />
</button>
<!-- Background set selection -->
<button v-for="shippedBackground in shippedBackgrounds"
:key="shippedBackground.name"
@@ -87,7 +93,7 @@
'icon-loading': loading === shippedBackground.name,
'background--active': backgroundImage === shippedBackground.name
}"
:data-color-bright="invertTextColor(shippedBackground.details.background_color)"
:data-color-bright="shippedBackground.details.theming === 'dark'"
:data-user-theming-background-shipped="shippedBackground.name"
:style="{ backgroundImage: 'url(' + shippedBackground.preview + ')', '--border-color': shippedBackground.details.primary_color }"
tabindex="0"
@@ -106,20 +112,17 @@ import { Palette } from 'node-vibrant/lib/color.js'
import axios from '@nextcloud/axios'
import debounce from 'debounce'
import NcColorPicker from '@nextcloud/vue/dist/Components/NcColorPicker.js'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import Vibrant from 'node-vibrant'
import Check from 'vue-material-design-icons/Check.vue'
import Close from 'vue-material-design-icons/Close.vue'
import ImageEdit from 'vue-material-design-icons/ImageEdit.vue'
import ColorPalette from 'vue-material-design-icons/Palette.vue'
const backgroundImage = loadState('theming', 'backgroundImage')
const shippedBackgroundList = loadState('theming', 'shippedBackgrounds')
const backgroundImage = loadState('theming', 'userBackgroundImage')
const {
backgroundImage: defaultBackgroundImage,
backgroundColor: defaultBackgroundColor,
backgroundMime: defaultBackgroundMime,
defaultShippedBackground,
} = loadState('theming', 'themingDefaults')
const themingDefaultBackground = loadState('theming', 'themingDefaultBackground')
const defaultShippedBackground = loadState('theming', 'defaultShippedBackground')
const prefixWithBaseUrl = (url) => generateFilePath('theming', '', 'img/background/') + url
@@ -128,8 +131,9 @@ export default {
components: {
Check,
ColorPalette,
Close,
ImageEdit,
NcButton,
NcColorPicker,
},
@@ -146,12 +150,7 @@ export default {
computed: {
shippedBackgrounds() {
return Object.keys(shippedBackgroundList)
.filter((background) => {
// If the admin did not changed the global background
// let's hide the default background to not show it twice
return background !== defaultShippedBackground || !this.isGlobalBackgroundDefault
})
.map((fileName) => {
.map(fileName => {
return {
name: fileName,
url: prefixWithBaseUrl(fileName),
@@ -159,18 +158,27 @@ export default {
details: shippedBackgroundList[fileName],
}
})
.filter(background => {
// If the admin did not changed the global background
// let's hide the default background to not show it twice
if (!this.isGlobalBackgroundDeleted && !this.isGlobalBackgroundDefault) {
return background.name !== defaultShippedBackground
}
return true
})
},
isGlobalBackgroundDefault() {
return defaultBackgroundMime === ''
return !!themingDefaultBackground
},
isGlobalBackgroundDeleted() {
return defaultBackgroundMime === 'backgroundColor'
return themingDefaultBackground === 'backgroundColor'
},
cssDefaultBackgroundImage() {
return `url('${defaultBackgroundImage}')`
isBackgroundDisabled() {
return this.backgroundImage === 'disabled'
|| !this.backgroundImage
},
},
@@ -218,7 +226,7 @@ export default {
async update(data) {
// Update state
this.backgroundImage = data.backgroundImage
this.Theming.backgroundColor = data.backgroundColor
this.Theming.color = data.backgroundColor
// Notify parent and reload style
this.$emit('update:background')
@@ -249,12 +257,12 @@ export default {
this.update(result.data)
},
async pickColor(color) {
async pickColor(event) {
this.loading = 'color'
const { data } = await axios.post(generateUrl('/apps/theming/background/color'), { color: color || '#0082c9' })
this.update(data)
const color = event?.target?.dataset?.color || this.Theming?.color || '#0082c9'
const result = await axios.post(generateUrl('/apps/theming/background/color'), { color })
this.update(result.data)
},
debouncePickColor: debounce(function(...args) {
this.pickColor(...args)
}, 200),
@@ -351,26 +359,21 @@ export default {
height: 96px;
margin: 8px;
text-align: center;
word-wrap: break-word;
hyphens: auto;
border: 2px solid var(--color-main-background);
border-radius: var(--border-radius-large);
background-position: center center;
background-size: cover;
&__filepicker {
background-color: var(--color-main-text);
background-color: var(--color-background-dark);
&.background--active {
color: var(--color-background-plain-text);
color: white;
background-image: var(--image-background);
}
}
&__default {
background-color: var(--color-background-plain);
background-image: linear-gradient(to bottom, rgba(23, 23, 23, 0.5), rgba(23, 23, 23, 0.5)), v-bind(cssDefaultBackgroundImage);
background-color: var(--color-primary-default);
background-image: linear-gradient(to bottom, rgba(23, 23, 23, 0.5), rgba(23, 23, 23, 0.5)), var(--image-background-plain, var(--image-background-default));
}
&__filepicker, &__default, &__color {
@@ -1,156 +0,0 @@
<template>
<div class="primary-color__wrapper">
<NcColorPicker v-model="primaryColor"
data-user-theming-primary-color
@update:value="debouncedOnUpdate">
<button ref="trigger"
class="color-container primary-color__trigger"
:style="{ 'background-color': primaryColor }"
data-user-theming-primary-color-trigger>
{{ t('theming', 'Primary color') }}
<NcLoadingIcon v-if="loading" />
<IconColorPalette v-else :size="20" />
</button>
</NcColorPicker>
<NcButton type="tertiary" :disabled="isdefaultPrimaryColor" @click="onReset">
<template #icon>
<IconUndo :size="20" />
</template>
{{ t('theming', 'Reset primary color') }}
</NcButton>
</div>
</template>
<script lang="ts">
import { showError } from '@nextcloud/dialogs'
import { loadState } from '@nextcloud/initial-state'
import { translate as t } from '@nextcloud/l10n'
import { generateOcsUrl } from '@nextcloud/router'
import { colord } from 'colord'
import { debounce } from 'debounce'
import { defineComponent } from 'vue'
import axios from '@nextcloud/axios'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcColorPicker from '@nextcloud/vue/dist/Components/NcColorPicker.js'
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import IconColorPalette from 'vue-material-design-icons/Palette.vue'
import IconUndo from 'vue-material-design-icons/UndoVariant.vue'
const { primaryColor, defaultPrimaryColor } = loadState('theming', 'data', { primaryColor: '#0082c9', defaultPrimaryColor: '#0082c9' })
export default defineComponent({
name: 'UserPrimaryColor',
components: {
IconColorPalette,
IconUndo,
NcButton,
NcColorPicker,
NcLoadingIcon,
},
emits: ['refresh-styles'],
data() {
return {
primaryColor,
loading: false,
}
},
computed: {
isdefaultPrimaryColor() {
return colord(this.primaryColor).isEqual(colord(defaultPrimaryColor))
},
debouncedOnUpdate() {
return debounce(this.onUpdate, 500)
},
},
methods: {
t,
/**
* Global styles are reloaded so we might need to update the current value
*/
reload() {
const trigger = this.$refs.trigger as HTMLButtonElement
const newColor = window.getComputedStyle(trigger).backgroundColor
if (newColor.toLowerCase() !== this.primaryColor) {
this.primaryColor = newColor
}
},
onReset() {
this.primaryColor = defaultPrimaryColor
this.onUpdate(null)
},
async onUpdate(value: string | null) {
this.loading = true
const url = generateOcsUrl('apps/provisioning_api/api/v1/config/users/{appId}/{configKey}', {
appId: 'theming',
configKey: 'primary_color',
})
try {
if (value) {
await axios.post(url, {
configValue: value,
})
} else {
await axios.delete(url)
}
this.$emit('refresh-styles')
} catch (e) {
console.error('Could not update primary color', e)
showError(t('theming', 'Could not set primary color'))
}
this.loading = false
},
},
})
</script>
<style scoped lang="scss">
.primary-color {
&__wrapper {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 12px;
}
&__trigger {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
background-color: var(--color-primary);
color: var(--color-primary-text);
width: 350px;
max-width: 100vw;
height: 96px;
word-wrap: break-word;
hyphens: auto;
border: 2px solid var(--color-main-background);
border-radius: var(--border-radius-large);
&:active {
background-color: var(--color-primary-hover) !important;
}
&:hover,
&:focus,
&:focus-visible {
border-color: var(--color-main-background) !important;
outline: 2px solid var(--color-main-text) !important;
}
}
}
</style>
@@ -26,35 +26,27 @@
<div class="field__row">
<NcColorPicker :value.sync="localValue"
:advanced-fields="true"
data-admin-theming-setting-primary-color-picker
@update:value="debounceSave">
<NcButton :id="id"
class="field__button"
type="primary"
:aria-label="t('theming', 'Select a custom color')"
data-admin-theming-setting-color-picker>
<NcButton type="secondary"
:id="id">
<template #icon>
<NcLoadingIcon v-if="loading"
:appearance="calculatedTextColor === '#ffffff' ? 'light' : 'dark'"
:size="20" />
<Palette v-else :size="20" />
<Palette :size="20" />
</template>
{{ value }}
{{ t('theming', 'Change color') }}
</NcButton>
</NcColorPicker>
<div class="field__color-preview" data-admin-theming-setting-color />
<div class="field__color-preview" data-admin-theming-setting-primary-color />
<NcButton v-if="value !== defaultValue"
type="tertiary"
:aria-label="t('theming', 'Reset to default')"
data-admin-theming-setting-color-reset
data-admin-theming-setting-primary-color-reset
@click="undo">
<template #icon>
<Undo :size="20" />
</template>
</NcButton>
</div>
<div v-if="description" class="description">
{{ description }}
</div>
<NcNoteCard v-if="errorMessage"
type="error"
@@ -66,10 +58,8 @@
<script>
import { debounce } from 'debounce'
import { colord } from 'colord'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcColorPicker from '@nextcloud/vue/dist/Components/NcColorPicker.js'
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
import Undo from 'vue-material-design-icons/UndoVariant.vue'
import Palette from 'vue-material-design-icons/Palette.vue'
@@ -82,7 +72,6 @@ export default {
components: {
NcButton,
NcColorPicker,
NcLoadingIcon,
NcNoteCard,
Undo,
Palette,
@@ -97,18 +86,10 @@ export default {
type: String,
required: true,
},
description: {
type: String,
default: '',
},
value: {
type: String,
required: true,
},
textColor: {
type: String,
default: null,
},
defaultValue: {
type: String,
required: true,
@@ -119,33 +100,9 @@ export default {
},
},
emits: ['update:theming'],
data() {
return {
loading: false,
}
},
computed: {
calculatedTextColor() {
const color = colord(this.value)
return color.isLight() ? '#000000' : '#ffffff'
},
usedTextColor() {
if (this.textColor) {
return this.textColor
}
return this.calculatedTextColor
},
},
methods: {
debounceSave: debounce(async function() {
this.loading = true
await this.save()
this.$emit('update:theming')
this.loading = false
}, 200),
},
}
@@ -153,20 +110,12 @@ export default {
<style lang="scss" scoped>
@import './shared/field.scss';
.description {
color: var(--color-text-maxcontrast);
}
.field {
&__button {
background-color: v-bind('value') !important;
color: v-bind('usedTextColor') !important;
}
&__color-preview {
width: var(--default-clickable-area);
border-radius: var(--border-radius-large);
background-color: v-bind('value');
background-color: var(--color-primary-default);
}
}
</style>
@@ -126,7 +126,7 @@ export default {
},
defaultMimeValue: {
type: String,
default: '',
required: true,
},
displayName: {
type: String,
@@ -182,10 +182,9 @@ export default {
const url = generateUrl('/apps/theming/ajax/uploadImage')
try {
this.showLoading = true
const { data } = await axios.post(url, formData)
await axios.post(url, formData)
this.showLoading = false
this.$emit('update:mime-value', file.type)
this.$emit('uploaded', data.data.url)
this.handleSuccess()
} catch (e) {
this.showLoading = false
+5 -15
View File
@@ -20,24 +20,14 @@
*
*/
/**
* Refresh server-side generated theming CSS
* This resolves when all themes are reloaded
*/
export async function refreshStyles() {
const themes = [...document.head.querySelectorAll('link.theme')]
const promises = themes.map((theme) => new Promise((resolve) => {
export const refreshStyles = () => {
// 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()
resolve()
}
newTheme.onload = () => theme.remove()
document.head.append(newTheme)
}))
// Wait until all themes are loaded
await Promise.allSettled(promises)
})
}
@@ -64,14 +64,10 @@ export default {
this.reset()
const url = generateUrl('/apps/theming/ajax/undoChanges')
try {
const { data } = await axios.post(url, {
await axios.post(url, {
setting: this.name,
})
if (data.data.value) {
this.$emit('update:defaultValue', data.data.value)
}
this.$emit('update:value', data.data.value || this.defaultValue)
this.$emit('update:value', this.defaultValue)
this.handleSuccess()
} catch (e) {
this.errorMessage = e.response.data.data?.message
+1 -1
View File
@@ -23,7 +23,7 @@ import { getRequestToken } from '@nextcloud/auth'
import Vue from 'vue'
import { refreshStyles } from './helpers/refreshStyles.js'
import App from './UserTheming.vue'
import App from './UserThemes.vue'
// eslint-disable-next-line camelcase
__webpack_nonce__ = btoa(getRequestToken())
+11 -28
View File
@@ -37,7 +37,6 @@ use OCP\Files\IAppData;
use OCP\IConfig;
use OCP\IURLGenerator;
use OCP\IUserSession;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
/**
@@ -46,16 +45,16 @@ use Test\TestCase;
* @package OCA\Theming\Tests
*/
class CapabilitiesTest extends TestCase {
/** @var ThemingDefaults|MockObject */
/** @var ThemingDefaults|\PHPUnit\Framework\MockObject\MockObject */
protected $theming;
/** @var IURLGenerator|MockObject */
/** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
protected $url;
/** @var IConfig|MockObject */
/** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
protected $config;
/** @var Util|MockObject */
/** @var Util|\PHPUnit\Framework\MockObject\MockObject */
protected $util;
protected IUserSession $userSession;
@@ -67,22 +66,16 @@ class CapabilitiesTest extends TestCase {
parent::setUp();
$this->theming = $this->createMock(ThemingDefaults::class);
$this->url = $this->createMock(IURLGenerator::class);
$this->url = $this->getMockBuilder(IURLGenerator::class)->getMock();
$this->config = $this->createMock(IConfig::class);
$this->util = $this->createMock(Util::class);
$this->userSession = $this->createMock(IUserSession::class);
$this->capabilities = new Capabilities(
$this->theming,
$this->util,
$this->url,
$this->config,
$this->userSession,
);
$this->capabilities = new Capabilities($this->theming, $this->util, $this->url, $this->config, $this->userSession);
}
public function dataGetCapabilities() {
return [
['name', 'url', 'slogan', '#FFFFFF', '#000000', 'logo', 'background', '#fff', '#000', 'http://absolute/', true, [
['name', 'url', 'slogan', '#FFFFFF', '#000000', 'logo', 'background', 'http://absolute/', true, [
'name' => 'name',
'url' => 'url',
'slogan' => 'slogan',
@@ -93,13 +86,12 @@ class CapabilitiesTest extends TestCase {
'color-element-dark' => '#FFFFFF',
'logo' => 'http://absolute/logo',
'background' => 'http://absolute/background',
'background-text' => '#000',
'background-plain' => false,
'background-default' => false,
'logoheader' => 'http://absolute/logo',
'favicon' => 'http://absolute/logo',
]],
['name1', 'url2', 'slogan3', '#01e4a0', '#ffffff', 'logo5', 'background6', '#fff', '#000', 'http://localhost/', false, [
['name1', 'url2', 'slogan3', '#01e4a0', '#ffffff', 'logo5', 'background6', 'http://localhost/', false, [
'name' => 'name1',
'url' => 'url2',
'slogan' => 'slogan3',
@@ -110,13 +102,12 @@ class CapabilitiesTest extends TestCase {
'color-element-dark' => '#01e4a0',
'logo' => 'http://localhost/logo5',
'background' => 'http://localhost/background6',
'background-text' => '#000',
'background-plain' => false,
'background-default' => true,
'logoheader' => 'http://localhost/logo5',
'favicon' => 'http://localhost/logo5',
]],
['name1', 'url2', 'slogan3', '#000000', '#ffffff', 'logo5', 'backgroundColor', '#000000', '#ffffff', 'http://localhost/', true, [
['name1', 'url2', 'slogan3', '#000000', '#ffffff', 'logo5', 'backgroundColor', 'http://localhost/', true, [
'name' => 'name1',
'url' => 'url2',
'slogan' => 'slogan3',
@@ -127,13 +118,12 @@ class CapabilitiesTest extends TestCase {
'color-element-dark' => '#4d4d4d',
'logo' => 'http://localhost/logo5',
'background' => '#000000',
'background-text' => '#ffffff',
'background-plain' => true,
'background-default' => false,
'logoheader' => 'http://localhost/logo5',
'favicon' => 'http://localhost/logo5',
]],
['name1', 'url2', 'slogan3', '#000000', '#ffffff', 'logo5', 'backgroundColor', '#000000', '#ffffff', 'http://localhost/', false, [
['name1', 'url2', 'slogan3', '#000000', '#ffffff', 'logo5', 'backgroundColor', 'http://localhost/', false, [
'name' => 'name1',
'url' => 'url2',
'slogan' => 'slogan3',
@@ -144,7 +134,6 @@ class CapabilitiesTest extends TestCase {
'color-element-dark' => '#4d4d4d',
'logo' => 'http://localhost/logo5',
'background' => '#000000',
'background-text' => '#ffffff',
'background-plain' => true,
'background-default' => true,
'logoheader' => 'http://localhost/logo5',
@@ -166,7 +155,7 @@ class CapabilitiesTest extends TestCase {
* @param bool $backgroundThemed
* @param string[] $expected
*/
public function testGetCapabilities($name, $url, $slogan, $color, $textColor, $logo, $background, $backgroundColor, $backgroundTextColor, $baseUrl, $backgroundThemed, array $expected) {
public function testGetCapabilities($name, $url, $slogan, $color, $textColor, $logo, $background, $baseUrl, $backgroundThemed, array $expected) {
$this->config->expects($this->once())
->method('getAppValue')
->willReturn($background);
@@ -179,12 +168,6 @@ class CapabilitiesTest extends TestCase {
$this->theming->expects($this->once())
->method('getSlogan')
->willReturn($slogan);
$this->theming->expects($this->once())
->method('getColorBackground')
->willReturn($backgroundColor);
$this->theming->expects($this->once())
->method('getTextColorBackground')
->willReturn($backgroundTextColor);
$this->theming->expects($this->atLeast(1))
->method('getDefaultColorPrimary')
->willReturn($color);
@@ -42,11 +42,13 @@ use OCP\App\IAppManager;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Files\IAppData;
use OCP\Files\NotFoundException;
use OCP\Files\SimpleFS\ISimpleFile;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
use OCP\ITempManager;
use OCP\IURLGenerator;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
@@ -62,13 +64,17 @@ class ThemingControllerTest extends TestCase {
private $l10n;
/** @var ThemingController */
private $themingController;
/** @var ITempManager */
private $tempManager;
/** @var IAppManager|MockObject */
private $appManager;
/** @var IAppData|MockObject */
private $appData;
/** @var ImageManager|MockObject */
private $imageManager;
/** @var IURLGenerator|MockObject */
private $urlGenerator;
/** @var ThemesService|MockObject */
/** @var ThemeService|MockObject */
private $themesService;
protected function setUp(): void {
@@ -76,7 +82,9 @@ class ThemingControllerTest extends TestCase {
$this->config = $this->createMock(IConfig::class);
$this->themingDefaults = $this->createMock(ThemingDefaults::class);
$this->l10n = $this->createMock(L10N::class);
$this->appData = $this->createMock(IAppData::class);
$this->appManager = $this->createMock(IAppManager::class);
$this->tempManager = \OC::$server->getTempManager();
$this->urlGenerator = $this->createMock(IURLGenerator::class);
$this->imageManager = $this->createMock(ImageManager::class);
$this->themesService = $this->createMock(ThemesService::class);
@@ -94,6 +102,8 @@ class ThemingControllerTest extends TestCase {
$this->config,
$this->themingDefaults,
$this->l10n,
$this->tempManager,
$this->appData,
$this->urlGenerator,
$this->appManager,
$this->imageManager,
@@ -154,12 +164,9 @@ class ThemingControllerTest extends TestCase {
['url', str_repeat('a', 501), 'The given web address is not a valid URL'],
['url', 'javascript:alert(1)', 'The given web address is not a valid URL'],
['slogan', str_repeat('a', 501), 'The given slogan is too long'],
['primary_color', '0082C9', 'The given color is invalid'],
['primary_color', '#0082Z9', 'The given color is invalid'],
['primary_color', 'Nextcloud', 'The given color is invalid'],
['background_color', '0082C9', 'The given color is invalid'],
['background_color', '#0082Z9', 'The given color is invalid'],
['background_color', 'Nextcloud', 'The given color is invalid'],
['color', '0082C9', 'The given color is invalid'],
['color', '#0082Z9', 'The given color is invalid'],
['color', 'Nextcloud', 'The given color is invalid'],
['imprintUrl', '0082C9', 'The given legal notice address is not a valid URL'],
['imprintUrl', '0082C9', 'The given legal notice address is not a valid URL'],
['imprintUrl', 'javascript:foo', 'The given legal notice address is not a valid URL'],
+1 -6
View File
@@ -28,7 +28,6 @@
namespace OCA\Theming\Tests;
use OCA\Theming\ImageManager;
use OCA\Theming\Service\BackgroundService;
use OCP\Files\IAppData;
use OCP\Files\NotFoundException;
use OCP\Files\SimpleFS\ISimpleFile;
@@ -58,8 +57,6 @@ class ImageManagerTest extends TestCase {
private $tempManager;
/** @var ISimpleFolder|MockObject */
private $rootFolder;
/** @var BackgroundService|MockObject */
private $backgroundService;
protected function setUp(): void {
parent::setUp();
@@ -70,15 +67,13 @@ class ImageManagerTest extends TestCase {
$this->logger = $this->createMock(LoggerInterface::class);
$this->tempManager = $this->createMock(ITempManager::class);
$this->rootFolder = $this->createMock(ISimpleFolder::class);
$this->backgroundService = $this->createMock(BackgroundService::class);
$this->imageManager = new ImageManager(
$this->config,
$this->appData,
$this->urlGenerator,
$this->cacheFactory,
$this->logger,
$this->tempManager,
$this->backgroundService,
$this->tempManager
);
$this->appData
->expects($this->any())
+4 -10
View File
@@ -30,7 +30,6 @@ namespace OCA\Theming\Tests\Settings;
use OCA\Theming\AppInfo\Application;
use OCA\Theming\ImageManager;
use OCA\Theming\ITheme;
use OCA\Theming\Service\BackgroundService;
use OCA\Theming\Service\ThemesService;
use OCA\Theming\Settings\Personal;
use OCA\Theming\Themes\DarkHighContrastTheme;
@@ -117,23 +116,18 @@ class PersonalTest extends TestCase {
->with('enforce_theme', '')
->willReturn($enforcedTheme);
$this->config->expects($this->any())
$this->config->expects($this->once())
->method('getUserValue')
->willReturnMap([
['admin', 'core', 'apporder', '[]', '[]'],
['admin', 'theming', 'background_image', BackgroundService::BACKGROUND_DEFAULT],
]);
->with('admin', 'core', 'apporder')
->willReturn('[]');
$this->appManager->expects($this->once())
->method('getDefaultAppForUser')
->willReturn('forcedapp');
$this->initialStateService->expects($this->exactly(7))
$this->initialStateService->expects($this->exactly(4))
->method('provideInitialState')
->withConsecutive(
['shippedBackgrounds', BackgroundService::SHIPPED_BACKGROUNDS],
['themingDefaults'],
['userBackgroundImage'],
['themes', $themesState],
['enforceTheme', $enforcedTheme],
['isUserThemingDisabled', false],
@@ -81,14 +81,6 @@ class DarkHighContrastThemeTest extends AccessibleThemeTestCase {
->expects($this->any())
->method('getDefaultColorPrimary')
->willReturn('#0082c9');
$this->themingDefaults
->expects($this->any())
->method('getColorBackground')
->willReturn('#0082c9');
$this->themingDefaults
->expects($this->any())
->method('getDefaultColorBackground')
->willReturn('#0082c9');
$this->themingDefaults
->expects($this->any())
@@ -78,14 +78,6 @@ class DarkThemeTest extends AccessibleThemeTestCase {
->expects($this->any())
->method('getDefaultColorPrimary')
->willReturn('#0082c9');
$this->themingDefaults
->expects($this->any())
->method('getColorBackground')
->willReturn('#0082c9');
$this->themingDefaults
->expects($this->any())
->method('getDefaultColorBackground')
->willReturn('#0082c9');
$this->themingDefaults
->expects($this->any())
+2 -14
View File
@@ -69,27 +69,15 @@ class DefaultThemeTest extends AccessibleThemeTestCase {
$this->imageManager
);
$defaultBackground = BackgroundService::SHIPPED_BACKGROUNDS[BackgroundService::DEFAULT_BACKGROUND_IMAGE];
$this->themingDefaults
->expects($this->any())
->method('getColorPrimary')
->willReturn($defaultBackground['primary_color']);
$this->themingDefaults
->expects($this->any())
->method('getColorBackground')
->willReturn($defaultBackground['background_color']);
->willReturn('#0082c9');
$this->themingDefaults
->expects($this->any())
->method('getDefaultColorPrimary')
->willReturn($defaultBackground['primary_color']);
$this->themingDefaults
->expects($this->any())
->method('getDefaultColorBackground')
->willReturn($defaultBackground['background_color']);
->willReturn('#0082c9');
$this->themingDefaults
->expects($this->any())
@@ -94,16 +94,6 @@ class DyslexiaFontTest extends TestCase {
->method('getDefaultColorPrimary')
->willReturn('#0082c9');
$this->themingDefaults
->expects($this->any())
->method('getColorBackground')
->willReturn('#0082c9');
$this->themingDefaults
->expects($this->any())
->method('getDefaultColorBackground')
->willReturn('#0082c9');
$this->l10n
->expects($this->any())
->method('t')
@@ -81,14 +81,6 @@ class HighContrastThemeTest extends AccessibleThemeTestCase {
->expects($this->any())
->method('getDefaultColorPrimary')
->willReturn('#0082c9');
$this->themingDefaults
->expects($this->any())
->method('getColorBackground')
->willReturn('#0082c9');
$this->themingDefaults
->expects($this->any())
->method('getDefaultColorBackground')
->willReturn('#0082c9');
$this->themingDefaults
->expects($this->any())
+103 -67
View File
@@ -78,8 +78,6 @@ class ThemingDefaultsTest extends TestCase {
private $imageManager;
/** @var INavigationManager|\PHPUnit\Framework\MockObject\MockObject */
private $navigationManager;
/** @var BackgroundService|\PHPUnit\Framework\MockObject\MockObject */
private $backgroundService;
protected function setUp(): void {
parent::setUp();
@@ -93,7 +91,6 @@ class ThemingDefaultsTest extends TestCase {
$this->imageManager = $this->createMock(ImageManager::class);
$this->appManager = $this->createMock(IAppManager::class);
$this->navigationManager = $this->createMock(INavigationManager::class);
$this->backgroundService = $this->createMock(BackgroundService::class);
$this->defaults = new \OC_Defaults();
$this->urlGenerator
->expects($this->any())
@@ -108,8 +105,7 @@ class ThemingDefaultsTest extends TestCase {
$this->util,
$this->imageManager,
$this->appManager,
$this->navigationManager,
$this->backgroundService,
$this->navigationManager
);
}
@@ -432,7 +428,7 @@ class ThemingDefaultsTest extends TestCase {
->method('getAppValue')
->willReturnMap([
['theming', 'disable-user-theming', 'no', 'no'],
['theming', 'primary_color', '', $this->defaults->getColorPrimary()],
['theming', 'color', '', $this->defaults->getColorPrimary()],
]);
$this->assertEquals($this->defaults->getColorPrimary(), $this->template->getColorPrimary());
@@ -444,57 +440,13 @@ class ThemingDefaultsTest extends TestCase {
->method('getAppValue')
->willReturnMap([
['theming', 'disable-user-theming', 'no', 'no'],
['theming', 'primary_color', '', '#fff'],
['theming', 'color', '', '#fff'],
]);
$this->assertEquals('#fff', $this->template->getColorPrimary());
}
public function dataGetColorPrimary() {
return [
'with fallback default' => [
'disableTheming' => 'no',
'primaryColor' => '',
'userPrimaryColor' => '',
'expected' => BackgroundService::DEFAULT_COLOR,
],
'with custom admin primary' => [
'disableTheming' => 'no',
'primaryColor' => '#aaa',
'userPrimaryColor' => '',
'expected' => '#aaa',
],
'with custom invalid admin primary' => [
'disableTheming' => 'no',
'primaryColor' => 'invalid',
'userPrimaryColor' => '',
'expected' => BackgroundService::DEFAULT_COLOR,
],
'with custom invalid user primary' => [
'disableTheming' => 'no',
'primaryColor' => '',
'userPrimaryColor' => 'invalid-name',
'expected' => BackgroundService::DEFAULT_COLOR,
],
'with custom user primary' => [
'disableTheming' => 'no',
'primaryColor' => '',
'userPrimaryColor' => '#bbb',
'expected' => '#bbb',
],
'with disabled user theming primary' => [
'disableTheming' => 'yes',
'primaryColor' => '#aaa',
'userPrimaryColor' => '#bbb',
'expected' => '#aaa',
],
];
}
/**
* @dataProvider dataGetColorPrimary
*/
public function testGetColorPrimary(string $disableTheming, string $primaryColor, string $userPrimaryColor, string $expected) {
public function testGetColorPrimaryWithDefaultBackground() {
$user = $this->createMock(IUser::class);
$this->userSession->expects($this->any())
->method('getUser')
@@ -503,19 +455,98 @@ class ThemingDefaultsTest extends TestCase {
->method('getUID')
->willReturn('user');
$this->config
->expects($this->any())
->expects($this->exactly(2))
->method('getAppValue')
->willReturnMap([
['theming', 'disable-user-theming', 'no', $disableTheming],
['theming', 'primary_color', '', $primaryColor],
['theming', 'disable-user-theming', 'no', 'no'],
['theming', 'color', '', ''],
]);
$this->config
->expects($this->any())
->expects($this->once())
->method('getUserValue')
->with('user', 'theming', 'primary_color', '')
->willReturn($userPrimaryColor);
->with('user', 'theming', 'background_color')
->willReturn('');
$this->assertEquals($expected, $this->template->getColorPrimary());
$this->assertEquals(BackgroundService::DEFAULT_COLOR, $this->template->getColorPrimary());
}
public function testGetColorPrimaryWithCustomBackground() {
$backgroundIndex = 2;
$background = array_values(BackgroundService::SHIPPED_BACKGROUNDS)[$backgroundIndex];
$user = $this->createMock(IUser::class);
$this->userSession->expects($this->any())
->method('getUser')
->willReturn($user);
$user->expects($this->any())
->method('getUID')
->willReturn('user');
$this->config
->expects($this->once())
->method('getUserValue')
->with('user', 'theming', 'background_color', '')
->willReturn($background['primary_color']);
$this->config
->expects($this->exactly(2))
->method('getAppValue')
->willReturnMap([
['theming', 'color', '', ''],
['theming', 'disable-user-theming', 'no', 'no'],
]);
$this->assertEquals($background['primary_color'], $this->template->getColorPrimary());
}
public function testGetColorPrimaryWithCustomBackgroundColor() {
$user = $this->createMock(IUser::class);
$this->userSession->expects($this->any())
->method('getUser')
->willReturn($user);
$user->expects($this->any())
->method('getUID')
->willReturn('user');
$this->config
->expects($this->once())
->method('getUserValue')
->with('user', 'theming', 'background_color', '')
->willReturn('#fff');
$this->config
->expects($this->exactly(2))
->method('getAppValue')
->willReturnMap([
['theming', 'color', '', ''],
['theming', 'disable-user-theming', 'no', 'no'],
]);
$this->assertEquals('#fff', $this->template->getColorPrimary());
}
public function testGetColorPrimaryWithInvalidCustomBackgroundColor() {
$user = $this->createMock(IUser::class);
$this->userSession->expects($this->any())
->method('getUser')
->willReturn($user);
$user->expects($this->any())
->method('getUID')
->willReturn('user');
$this->config
->expects($this->once())
->method('getUserValue')
->with('user', 'theming', 'background_color', '')
->willReturn('nextcloud');
$this->config
->expects($this->exactly(3))
->method('getAppValue')
->willReturnMap([
['theming', 'color', '', ''],
['theming', 'disable-user-theming', 'no', 'no'],
]);
$this->assertEquals($this->template->getDefaultColorPrimary(), $this->template->getColorPrimary());
}
public function testSet() {
@@ -615,22 +646,27 @@ class ThemingDefaultsTest extends TestCase {
$this->assertSame($this->defaults->getSlogan(), $this->template->undo('slogan'));
}
public function testUndoPrimaryColor() {
public function testUndoColor() {
$this->config
->expects($this->once())
->method('deleteAppValue')
->with('theming', 'primary_color');
->with('theming', 'color');
$this->config
->expects($this->once())
->expects($this->exactly(2))
->method('getAppValue')
->with('theming', 'cachebuster', '0')
->willReturn('15');
->withConsecutive(
['theming', 'cachebuster', '0'],
['theming', 'color', null],
)->willReturnOnConsecutiveCalls(
'15',
$this->defaults->getColorPrimary(),
);
$this->config
->expects($this->once())
->method('setAppValue')
->with('theming', 'cachebuster', 16);
$this->assertSame($this->defaults->getColorPrimary(), $this->template->undo('primary_color'));
$this->assertSame($this->defaults->getColorPrimary(), $this->template->undo('color'));
}
public function testUndoDefaultAction() {
@@ -728,8 +764,8 @@ class ThemingDefaultsTest extends TestCase {
['theming', 'backgroundMime', '', 'jpeg'],
['theming', 'logoheaderMime', '', 'jpeg'],
['theming', 'faviconMime', '', 'jpeg'],
['theming', 'primary_color', '', $this->defaults->getColorPrimary()],
['theming', 'primary_color', $this->defaults->getColorPrimary(), $this->defaults->getColorPrimary()],
['theming', 'color', '', $this->defaults->getColorPrimary()],
['theming', 'color', $this->defaults->getColorPrimary(), $this->defaults->getColorPrimary()],
]);
$this->util->expects($this->any())->method('invertTextColor')->with($this->defaults->getColorPrimary())->willReturn(false);
+1 -1
View File
@@ -319,7 +319,7 @@ class LDAP implements ILDAPWrapper {
private function preFunctionCall(string $functionName, array $args): void {
$this->curArgs = $args;
if(strcasecmp($functionName, 'ldap_bind') === 0 || strcasecmp($functionName, 'ldap_exop_passwd') === 0) {
if(strcasecmp($functionName, 'ldap_bind') === 0) {
// The arguments are not key value pairs
// \OCA\User_LDAP\LDAP::bind passes 3 arguments, the 3rd being the pw
// Remove it via direct array access for now, although a better solution could be found mebbe?
+17 -2
View File
@@ -1,7 +1,22 @@
<?php
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
* @copyright Copyright (c) 2017 Morris Jobke <hey@morrisjobke.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 <http://www.gnu.org/licenses/>.
*
*/
$expectedFiles = [
+17 -3
View File
@@ -1,8 +1,22 @@
<?php
/**
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @copyright Copyright (c) 2016, ownCloud, Inc.
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
if (!isset($argv[1])) {
+19 -2
View File
@@ -1,7 +1,24 @@
<?php
/**
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
* @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
*
* @author Lukas Reschke <lukas@statuscode.ch>
*
* @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/>.
*
*/
/**

Some files were not shown because too many files have changed in this diff Show More