From eb7c36443fcb5ee7c4d62252636c2ddeccfd63f4 Mon Sep 17 00:00:00 2001 From: Florian Schade Date: Wed, 29 May 2024 15:48:49 +0200 Subject: [PATCH] enhancement: introduce theme processing (#9133) * enhancement: introduce theme processing * enhancement: introduce theme processing * enhancement: add theme processing tests and changelog * Update services/web/pkg/config/config.go Co-authored-by: Michael Barz * fix: ci findings * Apply suggestions from code review Co-authored-by: Martin * enhancement: use the theme assets from web instead of having them inside the oCis repo (license clash Apache vs. AGPLv3) * fix: golangci tagalign order * fix: rename UnifiedRoleUploader to UnifiedRoleEditorLite * fix: some typos Co-authored-by: Michael Barz * enhancement: export supported theme logo upload filetypes * chore: bump reva * fix: allow init func --------- Co-authored-by: Michael Barz Co-authored-by: Martin --- .golangci.yml | 6 + .../enhancement-theme-processing.md | 15 + go.mod | 3 +- go.sum | 11 +- ocis-pkg/capabilities/defaults.go | 35 + services/frontend/pkg/revaconfig/config.go | 2 + services/web/README.md | 24 +- services/web/assets/themes/.keep | 0 .../web/assets/themes/owncloud/theme.json | 198 +++++ services/web/pkg/config/config.go | 5 +- .../web/pkg/config/defaults/defaultconfig.go | 5 +- services/web/pkg/server/http/server.go | 34 +- services/web/pkg/service/v0/branding.go | 205 ----- services/web/pkg/service/v0/instrument.go | 10 - services/web/pkg/service/v0/logging.go | 10 - services/web/pkg/service/v0/option.go | 14 +- services/web/pkg/service/v0/service.go | 39 +- services/web/pkg/theme/export_test.go | 3 + services/web/pkg/theme/kv.go | 98 +++ services/web/pkg/theme/kv_test.go | 137 ++++ services/web/pkg/theme/service.go | 197 +++++ services/web/pkg/theme/service_test.go | 74 ++ services/web/pkg/theme/theme.go | 61 ++ services/web/pkg/theme/theme_test.go | 30 + .../http/services/owncloud/ocdav/status.go | 4 +- .../services/owncloud/ocs/config/config.go | 3 +- .../cloud/capabilities/capabilities.go | 40 +- .../handlers/cloud/capabilities/uploads.go | 6 +- .../data => pkg/owncloud/ocs}/capabilities.go | 16 +- vendor/github.com/tidwall/sjson/LICENSE | 21 + vendor/github.com/tidwall/sjson/README.md | 278 +++++++ vendor/github.com/tidwall/sjson/logo.png | Bin 0 -> 16874 bytes vendor/github.com/tidwall/sjson/sjson.go | 737 ++++++++++++++++++ vendor/modules.txt | 6 +- 34 files changed, 2040 insertions(+), 287 deletions(-) create mode 100644 changelog/unreleased/enhancement-theme-processing.md create mode 100644 ocis-pkg/capabilities/defaults.go create mode 100644 services/web/assets/themes/.keep create mode 100644 services/web/assets/themes/owncloud/theme.json delete mode 100644 services/web/pkg/service/v0/branding.go create mode 100644 services/web/pkg/theme/export_test.go create mode 100644 services/web/pkg/theme/kv.go create mode 100644 services/web/pkg/theme/kv_test.go create mode 100644 services/web/pkg/theme/service.go create mode 100644 services/web/pkg/theme/service_test.go create mode 100644 services/web/pkg/theme/theme.go create mode 100644 services/web/pkg/theme/theme_test.go rename vendor/github.com/cs3org/reva/v2/{internal/http/services/owncloud/ocs/data => pkg/owncloud/ocs}/capabilities.go (96%) create mode 100644 vendor/github.com/tidwall/sjson/LICENSE create mode 100644 vendor/github.com/tidwall/sjson/README.md create mode 100644 vendor/github.com/tidwall/sjson/logo.png create mode 100644 vendor/github.com/tidwall/sjson/sjson.go diff --git a/.golangci.yml b/.golangci.yml index 7ff6dabf7b..2d4cf9f695 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -48,10 +48,16 @@ linters-settings: align: false sort: true order: + - json - yaml + - mapstructure - env - desc - introductionVersion + - deprecationVersion + - removalVersion + - deprecationInfo + - deprecationReplacement gocyclo: min-complexity: 35 # there are some func unfortunately who need this - should we refactor them? gomoddirectives: diff --git a/changelog/unreleased/enhancement-theme-processing.md b/changelog/unreleased/enhancement-theme-processing.md new file mode 100644 index 0000000000..a826910d0a --- /dev/null +++ b/changelog/unreleased/enhancement-theme-processing.md @@ -0,0 +1,15 @@ +Enhancement: Theme Processing and Logo Customization + +We have made significant improvements to the theme processing in Infinite Scale. +The changes include: + +- Enhanced the way themes are composed. Now, the final theme is a combination of the built-in theme and the custom theme provided by the administrator via `WEB_ASSET_THEMES_PATH` and `WEB_UI_THEME_PATH`. +- Introduced a new mechanism to load custom assets. This is particularly useful when a single asset, such as a logo, needs to be overwritten. +- Fixed the logo customization option. Previously, small theme changes would copy the entire theme. Now, only the changed keys are considered, making the process more efficient. +- Default themes are now part of ocis. This change simplifies the theme management process for web. + +These changes enhance the robustness of the theme handling in Infinite Scale and provide a better user experience. + + +https://github.com/owncloud/ocis/pull/9133 +https://github.com/owncloud/ocis/issues/8966 diff --git a/go.mod b/go.mod index 1cbafca599..c605e15260 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/cenkalti/backoff v2.2.1+incompatible github.com/coreos/go-oidc/v3 v3.10.0 github.com/cs3org/go-cs3apis v0.0.0-20231023073225-7748710e0781 - github.com/cs3org/reva/v2 v2.19.2-0.20240525160759-56879111e06a + github.com/cs3org/reva/v2 v2.19.2-0.20240529081036-419196f2342e github.com/dhowden/tag v0.0.0-20230630033851-978a0926ee25 github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e github.com/egirna/icap-client v0.1.1 @@ -88,6 +88,7 @@ require ( github.com/test-go/testify v1.1.4 github.com/thejerf/suture/v4 v4.0.5 github.com/tidwall/gjson v1.17.1 + github.com/tidwall/sjson v1.2.5 github.com/tus/tusd v1.13.0 github.com/unrolled/secure v1.14.0 github.com/urfave/cli/v2 v2.27.2 diff --git a/go.sum b/go.sum index 782c705907..347a8aa4b0 100644 --- a/go.sum +++ b/go.sum @@ -1025,12 +1025,8 @@ github.com/crewjam/saml v0.4.14 h1:g9FBNx62osKusnFzs3QTN5L9CVA/Egfgm+stJShzw/c= github.com/crewjam/saml v0.4.14/go.mod h1:UVSZCf18jJkk6GpWNVqcyQJMD5HsRugBPf4I1nl2mME= github.com/cs3org/go-cs3apis v0.0.0-20231023073225-7748710e0781 h1:BUdwkIlf8IS2FasrrPg8gGPHQPOrQ18MS1Oew2tmGtY= github.com/cs3org/go-cs3apis v0.0.0-20231023073225-7748710e0781/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= -github.com/cs3org/reva/v2 v2.19.2-0.20240522140613-fae5c105158f h1:gQNgViSZs4DogTJNQ7yiWGPS+HbYwhccSw6OjYQYxlk= -github.com/cs3org/reva/v2 v2.19.2-0.20240522140613-fae5c105158f/go.mod h1:BOlJApKFrWRiaOoBCRxCTG5bghTTMlYaEZrRxOzKaS8= -github.com/cs3org/reva/v2 v2.19.2-0.20240525160759-56879111e06a h1:Xv2TmFIxhk642vN3NO03XSZl9NO0TVy7/QAC0CUiySc= -github.com/cs3org/reva/v2 v2.19.2-0.20240525160759-56879111e06a/go.mod h1:BOlJApKFrWRiaOoBCRxCTG5bghTTMlYaEZrRxOzKaS8= -github.com/cs3org/reva/v2 v2.19.7 h1:0hMIbCZq1VBSxAslEvDDkj1Si8RqxbxR0wx3JvIRXTE= -github.com/cs3org/reva/v2 v2.19.7/go.mod h1:GRUrOp5HbFVwZTgR9bVrMZ/MvVy+Jhxw1PdMmhhKP9E= +github.com/cs3org/reva/v2 v2.19.2-0.20240529081036-419196f2342e h1:nooOncj0aaM6S8TuNSLY4d7KEb8xSzO9/LfqL0loV60= +github.com/cs3org/reva/v2 v2.19.2-0.20240529081036-419196f2342e/go.mod h1:BOlJApKFrWRiaOoBCRxCTG5bghTTMlYaEZrRxOzKaS8= github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= @@ -2032,6 +2028,7 @@ github.com/thanhpk/randstr v1.0.6 h1:psAOktJFD4vV9NEVb3qkhRSMvYh4ORRaj1+w/hn4B+o github.com/thanhpk/randstr v1.0.6/go.mod h1:M/H2P1eNLZzlDwAzpkkkUvoyNNMbzRGhESZuEQk3r0U= github.com/thejerf/suture/v4 v4.0.5 h1:F1E/4FZwXWqvlWDKEUo6/ndLtxGAUzMmNqkrMknZbAA= github.com/thejerf/suture/v4 v4.0.5/go.mod h1:gu9Y4dXNUWFrByqRt30Rm9/UZ0wzRSt9AJS6xu/ZGxU= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -2039,6 +2036,8 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjMdEfbuOBNXSVF2cMFGgQTPdKCbwM= github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns= diff --git a/ocis-pkg/capabilities/defaults.go b/ocis-pkg/capabilities/defaults.go new file mode 100644 index 0000000000..a04bcfd0eb --- /dev/null +++ b/ocis-pkg/capabilities/defaults.go @@ -0,0 +1,35 @@ +package capabilities + +import ( + "sync/atomic" + + "github.com/cs3org/reva/v2/pkg/owncloud/ocs" +) + +// allow the consuming part to change defaults, e.g., tests +var defaultCapabilities atomic.Pointer[ocs.Capabilities] + +func init() { //nolint:gochecknoinits + ResetDefault() +} + +// ResetDefault resets the default [Capabilities] to the default values. +func ResetDefault() { + defaultCapabilities.Store( + &ocs.Capabilities{ + Theme: &ocs.CapabilitiesTheme{ + Logo: &ocs.CapabilitiesThemeLogo{ + PermittedFileTypes: map[string]string{ + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".png": "image/png", + ".gif": "image/gif", + }, + }, + }, + }, + ) +} + +// Default returns the default [Capabilities]. +func Default() *ocs.Capabilities { return defaultCapabilities.Load() } diff --git a/services/frontend/pkg/revaconfig/config.go b/services/frontend/pkg/revaconfig/config.go index 1a94ae323e..c1fce126d1 100644 --- a/services/frontend/pkg/revaconfig/config.go +++ b/services/frontend/pkg/revaconfig/config.go @@ -9,6 +9,7 @@ import ( "path/filepath" "strconv" + "github.com/owncloud/ocis/v2/ocis-pkg/capabilities" "github.com/owncloud/ocis/v2/ocis-pkg/config/defaults" "github.com/owncloud/ocis/v2/ocis-pkg/log" "github.com/owncloud/ocis/v2/ocis-pkg/version" @@ -290,6 +291,7 @@ func FrontendConfigFromStruct(cfg *config.Config, logger log.Logger) (map[string "share_jail": true, "max_quota": cfg.MaxQuota, }, + "theme": capabilities.Default().Theme, "search": map[string]interface{}{ "property": map[string]interface{}{ "name": map[string]interface{}{ diff --git a/services/web/README.md b/services/web/README.md index 718ce5ca2d..426a9c53c2 100644 --- a/services/web/README.md +++ b/services/web/README.md @@ -43,6 +43,28 @@ This feature is useful for organizations that want to provide third party or cus It's important to note that the feature at the moment is only capable of providing static (js, mjs, e.g.) web applications and does not support injection of dynamic web applications (custom dynamic backends). +### Loading Themes + +Web themes are loaded, if added in the Infinite Scale source code, at build-time from +`/services/web/assets/themes`. +This cannot be manipulated at runtime. + +Additionally, the administrator can provide custom themes by storing it in the path defined by the environment +variable `WEB_ASSET_THEMES_PATH`. + +With the theme root directory defined, the system needs to know which theme to use. +This can be done by setting the `WEB_UI_THEME_PATH` environment variable. + +The final theme is composed of the built-in and the custom theme provided by the +administrator via `WEB_ASSET_THEMES_PATH` and `WEB_UI_THEME_PATH`. + +For example, Infinite Scale by default contains a built-in ownCloud theme. +If the administrator provides a custom theme via the `WEB_ASSET_THEMES_PATH` directory like, +`WEB_ASSET_THEMES_PATH/owncloud/themes.json`, this one will be used instead of the built-in one. + +Some theme keys are mandatory, like the `common.shareRoles` settings. +Such mandatory keys are injected automatically at runtime if not provided. + ### Loading Applications Web applications are loaded, if added in the Infinite Scale source code, at build-time from @@ -57,7 +79,7 @@ but can be redefined with any path set manually. The final list of available applications is composed of the built-in and the custom applications provided by the administrator via `WEB_ASSET_APPS_PATH`. -For example, if Infinite Scale would contain a built-in extension named `image-viewer-dfx` and the administrator provides a custom application named `image-viewer-obj` via the `WEB_ASSET_APPS_PATH` directory, the user will be able to access both +For example, if Infinite Scale contains a built-in extension named `image-viewer-dfx` and the administrator provides a custom application named `image-viewer-obj` via the `WEB_ASSET_APPS_PATH` directory, the user will be able to access both applications from the WebUI. ### Application Structure diff --git a/services/web/assets/themes/.keep b/services/web/assets/themes/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/services/web/assets/themes/owncloud/theme.json b/services/web/assets/themes/owncloud/theme.json new file mode 100644 index 0000000000..80383368de --- /dev/null +++ b/services/web/assets/themes/owncloud/theme.json @@ -0,0 +1,198 @@ +{ + "common": { + "name": "ownCloud", + "slogan": "ownCloud – A safe home for all your data", + "logo": "themes/owncloud/assets/logo.svg", + "urls": { + "accessDeniedHelp": "", + "imprint": "", + "privacy": "" + } + }, + "clients": { + "android": {}, + "desktop": {}, + "ios": {}, + "web": { + "defaults": { + "appBanner": {}, + "logo": { + "topbar": "themes/owncloud/assets/logo.svg", + "favicon": "themes/owncloud/assets/favicon.jpg", + "login": "themes/owncloud/assets/logo.svg" + }, + "loginPage": { + "backgroundImg": "themes/owncloud/assets/loginBackground.jpg" + }, + "designTokens": { + "breakpoints": { + "xsmall-max": "", + "small-default": "", + "small-max": "", + "medium-default": "", + "medium-max": "", + "large-default": "", + "large-max": "", + "xlarge": "" + }, + "fontSizes": { + "default": "", + "large": "", + "medium": "" + }, + "sizes": { + "form-check-default": "", + "height-small": "", + "height-table-row": "", + "icon-default": "", + "max-height-logo": "", + "max-width-logo": "", + "width-medium": "", + "tiles-default": "", + "tiles-resize-step": "" + }, + "spacing": { + "xsmall": "", + "small": "", + "medium": "", + "large": "", + "xlarge": "", + "xxlarge": "" + } + } + }, + "themes": [ + { + "isDark": false, + "name": "Light Theme", + "designTokens": { + "colorPalette": { + "background-accentuate": "rgba(255, 255, 5, 0.1)", + "background-default": "#ffffff", + "background-highlight": "#edf3fa", + "background-muted": "#f8f8f8", + "background-secondary": "#ffffff", + "background-hover": "rgb(236, 236, 236)", + "color-components-apptopbar-background": "transparent", + "color-components-apptopbar-border": "#ceddee", + "border": "#ecebee", + "input-bg": "#ffffff", + "input-border": "#ceddee", + "input-text-default": "#041e42", + "input-text-muted": "#4c5f79", + "swatch-brand-default": "#041e42", + "swatch-brand-hover": "#223959", + "swatch-brand-contrast": "#ffffff", + "swatch-danger-contrast": "#ffffff", + "swatch-danger-default": "rgb(197, 48, 48)", + "swatch-danger-hover": "#b12b2b", + "swatch-danger-muted": "rgb(204, 117, 117)", + "swatch-inverse-default": "#ffffff", + "swatch-inverse-hover": "#ffffff", + "swatch-inverse-muted": "#bfbfbf", + "swatch-passive-default": "#4c5f79", + "swatch-passive-hover": "#43536b", + "swatch-passive-hover-outline": "#f7fafd", + "swatch-passive-muted": "#283e5d", + "swatch-passive-contrast": "#ffffff", + "swatch-primary-default": "#4a76ac", + "swatch-primary-hover": "#80a7d7", + "swatch-primary-muted": "#2c588e", + "swatch-primary-muted-hover": "rgb(36, 75, 119)", + "swatch-primary-gradient": "#4e85c8", + "swatch-primary-gradient-hover": "rgb(59, 118, 194)", + "swatch-primary-contrast": "#ffffff", + "swatch-success-default": "rgb(3, 84, 63)", + "swatch-success-hover": "#023b2c", + "swatch-success-muted": "rgb(83, 150, 10)", + "swatch-success-contrast": "#ffffff", + "swatch-warning-default": "rgb(183, 76, 27)", + "swatch-warning-hover": "#a04318", + "swatch-warning-muted": "rgba(183, 76, 27, .5)", + "swatch-warning-contrast": "#ffffff", + "text-default": "#041e42", + "text-inverse": "#ffffff", + "text-muted": "#4c5f79", + "icon-folder": "#4d7eaf", + "icon-archive": "#fbbe54", + "icon-image": "#ee6b3b", + "icon-spreadsheet": "#15c286", + "icon-document": "#3b44a6", + "icon-video": "#045459", + "icon-audio": "#700460", + "icon-presentation": "#ee6b3b", + "icon-pdf": "#ec0d47", + "icon-medical": "#0984db" + } + } + }, + { + "isDark": true, + "name": "Dark Theme", + "designTokens": { + "colorPalette": { + "background-accentuate": "#696969", + "background-default": "#292929", + "background-highlight": "#383838", + "background-muted": "#383838", + "background-secondary": "#4f4f4f", + "background-hover": "#383838", + "color-components-apptopbar-background": "transparent", + "color-components-apptopbar-border": "#ceddee", + "border": "#383838", + "input-bg": "#4f4f4f", + "input-border": "#696969", + "input-text-default": "#dadcdf", + "input-text-muted": "#bdbfc3", + "swatch-brand-default": "#212121", + "swatch-brand-hover": "#ffffff", + "swatch-brand-contrast": "#dadcdf", + "swatch-inverse-default": "", + "swatch-inverse-hover": "", + "swatch-inverse-muted": "#696969", + "swatch-passive-default": "#c2c2c2", + "swatch-passive-hover": "", + "swatch-passive-hover-outline": "#3B3B3B", + "swatch-passive-muted": "#bdbfc3", + "swatch-passive-contrast": "#000000", + "swatch-primary-default": "#73b0f2", + "swatch-primary-hover": "#7bafef", + "swatch-primary-muted": "", + "swatch-primary-muted-hover": "#2282f7", + "swatch-primary-gradient": "#4e85c8", + "swatch-primary-gradient-hover": "#76a1d5", + "swatch-primary-contrast": "#dadcdf", + "swatch-success-background": "rgba(0, 188, 140, 0)", + "swatch-success-default": "rgb(0, 188, 140)", + "swatch-success-hover": "#00f0b4", + "swatch-success-muted": "rgba(0, 188, 140, .5)", + "swatch-success-contrast": "#000000", + "swatch-warning-background": "rgba(0,0,0,0)", + "swatch-warning-default": "rgb(232, 191, 73)", + "swatch-warning-hover": "#eed077", + "swatch-warning-muted": "rgba(232, 178, 19, .5)", + "swatch-danger-default": "rgb(255, 72, 53)", + "swatch-danger-hover": "#ff7566", + "swatch-danger-muted": "rgba(255, 72, 53, .5)", + "swatch-danger-contrast": "#dadcdf", + "swatch-warning-contrast": "#000000", + "text-default": "#dadcdf", + "text-inverse": "#000000", + "text-muted": "#c2c2c2", + "icon-folder": "rgb(44, 101, 255)", + "icon-archive": "rgb(255, 207, 1)", + "icon-image": "rgb(255, 111, 0)", + "icon-spreadsheet": "rgb(0, 182, 87)", + "icon-document": "rgb(44, 101, 255)", + "icon-video": "rgb(0, 187, 219)", + "icon-audio": "rgb(208, 67, 236)", + "icon-presentation": "rgb(255, 64, 6)", + "icon-pdf": "rgb(225, 5, 14)", + "icon-medical": "rgb(9,132,219)" + } + } + } + ] + } + } +} diff --git a/services/web/pkg/config/config.go b/services/web/pkg/config/config.go index d11462058e..bf87636702 100644 --- a/services/web/pkg/config/config.go +++ b/services/web/pkg/config/config.go @@ -32,8 +32,9 @@ type Config struct { // Asset defines the available asset configuration. type Asset struct { DeprecatedPath string `yaml:"path" env:"WEB_ASSET_PATH" desc:"Serve ownCloud Web assets from a path on the filesystem instead of the builtin assets." introductionVersion:"pre5.0" deprecationVersion:"5.1.0" removalVersion:"6.0.0" deprecationInfo:"The WEB_ASSET_PATH is deprecated and will be removed in the future." deprecationReplacement:"Use WEB_ASSET_CORE_PATH instead."` - CorePath string `yaml:"core_path" env:"WEB_ASSET_CORE_PATH" desc:"Serve ownCloud Web assets from a path on the filesystem instead of the builtin assets." introductionVersion:"5.1"` - AppsPath string `yaml:"apps_path" env:"WEB_ASSET_APPS_PATH" desc:"Serve ownCloud Web apps assets from a path on the filesystem instead of the builtin assets." introductionVersion:"5.1"` + CorePath string `yaml:"core_path" env:"WEB_ASSET_CORE_PATH" desc:"Serve ownCloud Web assets from a path on the filesystem instead of the builtin assets. If not defined, the root directory derives from $OCIS_BASE_DATA_PATH:/web/assets/core" introductionVersion:"5.1"` + ThemesPath string `yaml:"themes_path" env:"OCIS_ASSET_THEMES_PATH;WEB_ASSET_THEMES_PATH" desc:"Serve ownCloud themes from a path on the filesystem instead of the builtin assets. If not defined, the root directory derives from $OCIS_BASE_DATA_PATH:/web/assets/themes" introductionVersion:"5.1"` + AppsPath string `yaml:"apps_path" env:"WEB_ASSET_APPS_PATH" desc:"Serve ownCloud Web apps assets from a path on the filesystem instead of the builtin assets. If not defined, the root directory derives from $OCIS_BASE_DATA_PATH:/web/assets/apps" introductionVersion:"5.1"` } // CustomStyle references additional css to be loaded into ownCloud Web. diff --git a/services/web/pkg/config/defaults/defaultconfig.go b/services/web/pkg/config/defaults/defaultconfig.go index 8fad92be50..71f12720e0 100644 --- a/services/web/pkg/config/defaults/defaultconfig.go +++ b/services/web/pkg/config/defaults/defaultconfig.go @@ -80,8 +80,9 @@ func DefaultConfig() *config.Config { Name: "web", }, Asset: config.Asset{ - CorePath: filepath.Join(defaults.BaseDataPath(), "web/assets/core"), - AppsPath: filepath.Join(defaults.BaseDataPath(), "web/assets/apps"), + CorePath: filepath.Join(defaults.BaseDataPath(), "web/assets/core"), + AppsPath: filepath.Join(defaults.BaseDataPath(), "web/assets/apps"), + ThemesPath: filepath.Join(defaults.BaseDataPath(), "web/assets/themes"), }, GatewayAddress: "com.owncloud.api.gateway", Web: config.Web{ diff --git a/services/web/pkg/server/http/server.go b/services/web/pkg/server/http/server.go index 6b4d6c32ac..012c3ccf41 100644 --- a/services/web/pkg/server/http/server.go +++ b/services/web/pkg/server/http/server.go @@ -56,15 +56,10 @@ func Server(opts ...Option) (http.Service, error) { return http.Service{}, err } - coreFS := fsx.NewFallbackFS( - fsx.NewBasePathFs(fsx.NewOsFs(), options.Config.Asset.CorePath), - fsx.NewBasePathFs(fsx.FromIOFS(web.Assets), "assets/core"), - ) appsFS := fsx.NewFallbackFS( fsx.NewReadOnlyFs(fsx.NewBasePathFs(fsx.NewOsFs(), options.Config.Asset.AppsPath)), fsx.NewBasePathFs(fsx.FromIOFS(web.Assets), "assets/apps"), ) - // build and inject the list of applications into the config for _, application := range apps.List(options.Logger, options.Config.Apps, appsFS.Secondary().IOFS(), appsFS.Primary().IOFS()) { options.Config.Web.Config.ExternalApps = append( @@ -73,10 +68,31 @@ func Server(opts ...Option) (http.Service, error) { ) } - handle := svc.NewService( + coreFS := fsx.NewFallbackFS( + fsx.NewBasePathFs(fsx.NewOsFs(), options.Config.Asset.CorePath), + fsx.NewBasePathFs(fsx.FromIOFS(web.Assets), "assets/core"), + ) + themeFS := fsx.NewFallbackFS( + fsx.NewBasePathFs(fsx.NewOsFs(), options.Config.Asset.ThemesPath), + fsx.NewBasePathFs(fsx.FromIOFS(web.Assets), "assets/themes"), + ) + // oCis is Apache licensed, and the ownCloud branding is AGPLv3. + // we are not allowed to have the ownCloud branding as part of the oCIS repository, + // as workaround we layer the embedded core fs on top of the theme fs to provide the ownCloud branding. + // each asset that is part of the embedded core fs (coreFS secondary fs) + // is downloaded at build time from the ownCloud web repository, + // web is licensed under AGPLv3 too, and is allowed to contain the ownCloud branding. + // themeFS = themeFS.Primary (rw) < themeFS.Secondary (ro) < coreFS.Secondary (ro) + themeFS = fsx.NewFallbackFS( + themeFS, + fsx.NewBasePathFs(coreFS.Secondary(), "themes"), + ) + + handle, err := svc.NewService( svc.Logger(options.Logger), - svc.CoreFS(coreFS), + svc.CoreFS(coreFS.IOFS()), svc.AppFS(appsFS.IOFS()), + svc.ThemeFS(themeFS), svc.AppsHTTPEndpoint(_customAppsEndpoint), svc.Config(options.Config), svc.GatewaySelector(gatewaySelector), @@ -103,6 +119,10 @@ func Server(opts ...Option) (http.Service, error) { svc.TraceProvider(options.TraceProvider), ) + if err != nil { + return http.Service{}, err + } + { handle = svc.NewInstrument(handle, options.Metrics) handle = svc.NewLogging(handle, options.Logger) diff --git a/services/web/pkg/service/v0/branding.go b/services/web/pkg/service/v0/branding.go deleted file mode 100644 index 47da430608..0000000000 --- a/services/web/pkg/service/v0/branding.go +++ /dev/null @@ -1,205 +0,0 @@ -package svc - -import ( - "encoding/json" - "errors" - "io" - "net/http" - "path" - "path/filepath" - - permissionsapi "github.com/cs3org/go-cs3apis/cs3/permissions/v1beta1" - rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" - revactx "github.com/cs3org/reva/v2/pkg/ctx" - "github.com/spf13/afero" -) - -var ( - errInvalidThemeConfig = errors.New("invalid themes config") - _themesConfigPath = filepath.FromSlash("themes/owncloud/theme.json") - _allowedExtensionMediatypes = map[string]string{ - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".png": "image/png", - ".gif": "image/gif", - } -) - -// UploadLogo implements the endpoint to upload a custom logo for the oCIS instance. -func (p Web) UploadLogo(w http.ResponseWriter, r *http.Request) { - gatewayClient, err := p.gatewaySelector.Next() - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - - user := revactx.ContextMustGetUser(r.Context()) - rsp, err := gatewayClient.CheckPermission(r.Context(), &permissionsapi.CheckPermissionRequest{ - Permission: "Logo.Write", - SubjectRef: &permissionsapi.SubjectReference{ - Spec: &permissionsapi.SubjectReference_UserId{ - UserId: user.Id, - }, - }, - }) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - if rsp.Status.Code != rpc.Code_CODE_OK { - w.WriteHeader(http.StatusForbidden) - return - } - - file, fileHeader, err := r.FormFile("logo") - if err != nil { - if errors.Is(err, http.ErrMissingFile) { - w.WriteHeader(http.StatusBadRequest) - } - w.WriteHeader(http.StatusInternalServerError) - return - } - defer file.Close() - - mediatype := fileHeader.Header.Get("Content-Type") - if !allowedFiletype(fileHeader.Filename, mediatype) { - w.WriteHeader(http.StatusBadRequest) - return - } - - fp := filepath.Join("branding", filepath.Join("/", fileHeader.Filename)) - err = afero.WriteReader(p.coreFS, fp, file) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - - err = p.updateLogoThemeConfig(fp) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusOK) -} - -// ResetLogo implements the endpoint to reset the instance logo. -// The config will be changed back to use the embedded logo asset. -func (p Web) ResetLogo(w http.ResponseWriter, r *http.Request) { - gatewayClient, err := p.gatewaySelector.Next() - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - - user := revactx.ContextMustGetUser(r.Context()) - rsp, err := gatewayClient.CheckPermission(r.Context(), &permissionsapi.CheckPermissionRequest{ - Permission: "Logo.Write", - SubjectRef: &permissionsapi.SubjectReference{ - Spec: &permissionsapi.SubjectReference_UserId{ - UserId: user.Id, - }, - }, - }) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - if rsp.Status.Code != rpc.Code_CODE_OK { - w.WriteHeader(http.StatusForbidden) - return - } - - f, err := p.coreFS.Secondary().Open(_themesConfigPath) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - defer f.Close() - - originalPath, err := p.getLogoPath(f) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - - if err := p.updateLogoThemeConfig(originalPath); err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } -} - -func (p Web) getLogoPath(r io.Reader) (string, error) { - // This decoding of the themes.json file is not optimal. If we need to decode it for other - // usecases as well we should consider decoding to a struct. - var m map[string]interface{} - _ = json.NewDecoder(r).Decode(&m) - - logoCfg, ok := extractMap(m, "clients", "web", "defaults", "logo") - if !ok { - return "", errInvalidThemeConfig - } - - logoPath, ok := logoCfg["login"].(string) - if !ok { - return "", errInvalidThemeConfig - } - - return logoPath, nil -} - -func (p Web) updateLogoThemeConfig(logoPath string) error { - f, err := p.coreFS.Open(_themesConfigPath) - if err == nil { - defer f.Close() - } - - // This decoding of the themes.json file is not optimal. If we need to decode it for other - // usecases as well we should consider decoding to a struct. - var m map[string]interface{} - _ = json.NewDecoder(f).Decode(&m) - - // change logo in common part - commonCfg, ok := extractMap(m, "common") - if !ok { - return errInvalidThemeConfig - } - commonCfg["logo"] = logoPath - - logoCfg, ok := extractMap(m, "clients", "web", "defaults", "logo") - if !ok { - return errInvalidThemeConfig - } - - logoCfg["login"] = logoPath - logoCfg["topbar"] = logoPath - - dst, err := p.coreFS.Create(_themesConfigPath) - if err != nil { - return err - } - defer dst.Close() - - return json.NewEncoder(dst).Encode(m) -} - -func allowedFiletype(filename, mediatype string) bool { - ext := path.Ext(filename) - - // Check if we allow that extension and if the mediatype matches the extension - mt, ok := _allowedExtensionMediatypes[ext] - return ok && mt == mediatype -} - -// extractMap extracts embedded map[string]interface{} by the keys chain -func extractMap(data map[string]interface{}, keys ...string) (map[string]interface{}, bool) { - last := data - var ok bool - for _, key := range keys { - last, ok = last[key].(map[string]interface{}) - if !ok { - return nil, false - } - } - return last, true -} diff --git a/services/web/pkg/service/v0/instrument.go b/services/web/pkg/service/v0/instrument.go index e01a3b1dc1..2d89f069d6 100644 --- a/services/web/pkg/service/v0/instrument.go +++ b/services/web/pkg/service/v0/instrument.go @@ -28,13 +28,3 @@ func (i instrument) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (i instrument) Config(w http.ResponseWriter, r *http.Request) { i.next.Config(w, r) } - -// UploadLogo implements the Service interface. -func (i instrument) UploadLogo(w http.ResponseWriter, r *http.Request) { - i.next.UploadLogo(w, r) -} - -// ResetLogo implements the Service interface. -func (i instrument) ResetLogo(w http.ResponseWriter, r *http.Request) { - i.next.ResetLogo(w, r) -} diff --git a/services/web/pkg/service/v0/logging.go b/services/web/pkg/service/v0/logging.go index dbdb3abb91..9e2c9e31ea 100644 --- a/services/web/pkg/service/v0/logging.go +++ b/services/web/pkg/service/v0/logging.go @@ -28,13 +28,3 @@ func (l logging) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (l logging) Config(w http.ResponseWriter, r *http.Request) { l.next.Config(w, r) } - -// UploadLogo implements the Service interface. -func (l logging) UploadLogo(w http.ResponseWriter, r *http.Request) { - l.next.UploadLogo(w, r) -} - -// ResetLogo implements the Service interface. -func (l logging) ResetLogo(w http.ResponseWriter, r *http.Request) { - l.next.ResetLogo(w, r) -} diff --git a/services/web/pkg/service/v0/option.go b/services/web/pkg/service/v0/option.go index 066897c159..ea025e5faa 100644 --- a/services/web/pkg/service/v0/option.go +++ b/services/web/pkg/service/v0/option.go @@ -23,9 +23,10 @@ type Options struct { Middleware []func(http.Handler) http.Handler GatewaySelector pool.Selectable[gateway.GatewayAPIClient] TraceProvider trace.TracerProvider - AppFS fs.FS AppsHTTPEndpoint string - CoreFS *fsx.FallbackFS + CoreFS fs.FS + AppFS fs.FS + ThemeFS *fsx.FallbackFS } // newOptions initializes the available default options. @@ -81,6 +82,13 @@ func AppFS(val fs.FS) Option { } } +// ThemeFS provides a function to set the themeFS option. +func ThemeFS(val *fsx.FallbackFS) Option { + return func(o *Options) { + o.ThemeFS = val + } +} + // AppsHTTPEndpoint provides a function to set the appsHTTPEndpoint option. func AppsHTTPEndpoint(val string) Option { return func(o *Options) { @@ -89,7 +97,7 @@ func AppsHTTPEndpoint(val string) Option { } // CoreFS provides a function to set the coreFS option. -func CoreFS(val *fsx.FallbackFS) Option { +func CoreFS(val fs.FS) Option { return func(o *Options) { o.CoreFS = val } diff --git a/services/web/pkg/service/v0/service.go b/services/web/pkg/service/v0/service.go index 191b2e239c..e8a8b9eb4a 100644 --- a/services/web/pkg/service/v0/service.go +++ b/services/web/pkg/service/v0/service.go @@ -23,6 +23,7 @@ import ( "github.com/owncloud/ocis/v2/ocis-pkg/x/io/fsx" "github.com/owncloud/ocis/v2/services/web/pkg/assets" "github.com/owncloud/ocis/v2/services/web/pkg/config" + "github.com/owncloud/ocis/v2/services/web/pkg/theme" ) // ErrConfigInvalid is returned when the config parse is invalid. @@ -30,14 +31,12 @@ var ErrConfigInvalid = `Invalid or missing config` // Service defines the service handlers. type Service interface { - ServeHTTP(http.ResponseWriter, *http.Request) - Config(http.ResponseWriter, *http.Request) - UploadLogo(http.ResponseWriter, *http.Request) - ResetLogo(http.ResponseWriter, *http.Request) + ServeHTTP(w http.ResponseWriter, r *http.Request) + Config(w http.ResponseWriter, r *http.Request) } // NewService returns a service implementation for Service. -func NewService(opts ...Option) Service { +func NewService(opts ...Option) (Service, error) { options := newOptions(opts...) m := chi.NewMux() @@ -57,9 +56,19 @@ func NewService(opts ...Option) Service { config: options.Config, mux: m, coreFS: options.CoreFS, + themeFS: options.ThemeFS, gatewaySelector: options.GatewaySelector, } + themeService, err := theme.NewService( + theme.ServiceOptions{}. + WithThemeFS(options.ThemeFS). + WithGatewaySelector(options.GatewaySelector), + ) + if err != nil { + return svc, err + } + m.Route(options.Config.HTTP.Root, func(r chi.Router) { r.Get("/config.json", svc.Config) r.Route("/branding/logo", func(r chi.Router) { @@ -67,8 +76,16 @@ func NewService(opts ...Option) Service { account.Logger(options.Logger), account.JWTSecret(options.Config.TokenManager.JWTSecret), )) - r.Post("/", svc.UploadLogo) - r.Delete("/", svc.ResetLogo) + r.Post("/", themeService.LogoUpload) + r.Delete("/", themeService.LogoReset) + }) + r.Route("/themes", func(r chi.Router) { + r.Get("/{id}/theme.json", themeService.Get) + r.Mount("/", svc.Static( + options.ThemeFS.IOFS(), + path.Join(svc.config.HTTP.Root, "/themes"), + options.Config.HTTP.CacheTTL, + )) }) r.Mount(options.AppsHTTPEndpoint, svc.Static( options.AppFS, @@ -76,18 +93,17 @@ func NewService(opts ...Option) Service { options.Config.HTTP.CacheTTL, )) r.Mount("/", svc.Static( - svc.coreFS.IOFS(), + svc.coreFS, svc.config.HTTP.Root, options.Config.HTTP.CacheTTL, )) }) - _ = chi.Walk(m, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { options.Logger.Debug().Str("method", method).Str("route", route).Int("middlewares", len(middlewares)).Msg("serving endpoint") return nil }) - return svc + return svc, nil } // Web defines the handlers for the web service. @@ -95,7 +111,8 @@ type Web struct { logger log.Logger config *config.Config mux *chi.Mux - coreFS *fsx.FallbackFS + coreFS fs.FS + themeFS *fsx.FallbackFS gatewaySelector pool.Selectable[gateway.GatewayAPIClient] } diff --git a/services/web/pkg/theme/export_test.go b/services/web/pkg/theme/export_test.go new file mode 100644 index 0000000000..b816367e3d --- /dev/null +++ b/services/web/pkg/theme/export_test.go @@ -0,0 +1,3 @@ +package theme + +var IsFiletypePermitted = isFiletypePermitted diff --git a/services/web/pkg/theme/kv.go b/services/web/pkg/theme/kv.go new file mode 100644 index 0000000000..c01da84a62 --- /dev/null +++ b/services/web/pkg/theme/kv.go @@ -0,0 +1,98 @@ +package theme + +import ( + "bytes" + "encoding/json" + + "dario.cat/mergo" + "github.com/spf13/afero" + "github.com/tidwall/sjson" +) + +// KV is a generic key-value map. +type KV map[string]any + +// MergeKV merges the given key-value maps. +func MergeKV(values ...KV) (KV, error) { + var kv KV + + for _, v := range values { + err := mergo.Merge(&kv, v, mergo.WithOverride) + if err != nil { + return nil, err + } + } + + return kv, nil +} + +// PatchKV injects the given values into to v. +func PatchKV(v any, values KV) error { + bv, err := json.Marshal(v) + if err != nil { + return err + } + + nv := string(bv) + + for k, val := range values { + var err error + switch val { + // if the value is nil, we delete the key + case nil: + nv, err = sjson.Delete(nv, k) + default: + nv, err = sjson.Set(nv, k, val) + } + + if err != nil { + return err + } + } + + return json.Unmarshal([]byte(nv), v) +} + +// LoadKV loads a key-value map from the given file system. +func LoadKV(fsys afero.Fs, p string) (KV, error) { + f, err := fsys.Open(p) + if err != nil { + return nil, err + } + defer f.Close() + + var kv KV + err = json.NewDecoder(f).Decode(&kv) + if err != nil { + return nil, err + } + + return kv, nil +} + +// WriteKV writes the given key-value map to the file system. +func WriteKV(fsys afero.Fs, p string, kv KV) error { + data, err := json.Marshal(kv) + if err != nil { + return err + } + + return afero.WriteReader(fsys, p, bytes.NewReader(data)) +} + +// UpdateKV updates the key-value map at the given path with the given values. +func UpdateKV(fsys afero.Fs, p string, values KV) error { + var kv KV + + existing, err := LoadKV(fsys, p) + if err == nil { + kv = existing + } + + err = PatchKV(&kv, values) + if err != nil { + return err + } + + return WriteKV(fsys, p, kv) +} diff --git a/services/web/pkg/theme/kv_test.go b/services/web/pkg/theme/kv_test.go new file mode 100644 index 0000000000..cf89841456 --- /dev/null +++ b/services/web/pkg/theme/kv_test.go @@ -0,0 +1,137 @@ +package theme_test + +import ( + "encoding/json" + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + + "github.com/owncloud/ocis/v2/ocis-pkg/x/io/fsx" + "github.com/owncloud/ocis/v2/services/web/pkg/theme" +) + +func TestMergeKV(t *testing.T) { + left := theme.KV{ + "left": "left", + "both": "left", + } + right := theme.KV{ + "right": "right", + "both": "right", + } + + result, err := theme.MergeKV(left, right) + assert.Nil(t, err) + assert.Equal(t, result, theme.KV{ + "left": "left", + "right": "right", + "both": "right", + }) +} + +func TestPatchKV(t *testing.T) { + in := theme.KV{ + "a": theme.KV{ + "value": "a", + }, + "b": theme.KV{ + "value": "b", + }, + } + err := theme.PatchKV(&in, theme.KV{ + "b.value": "b-new", + "c.value": "c-new", + }) + assert.Nil(t, err) + assert.Equal(t, in, theme.KV{ + "a": map[string]interface{}{ + "value": "a", + }, + "b": map[string]interface{}{ + "value": "b-new", + }, + "c": map[string]interface{}{ + "value": "c-new", + }, + }) +} + +func TestLoadKV(t *testing.T) { + in := theme.KV{ + "a": map[string]interface{}{ + "value": "a", + }, + "b": map[string]interface{}{ + "value": "b", + }, + } + b, err := json.Marshal(in) + assert.Nil(t, err) + + fsys := fsx.NewMemMapFs() + assert.Nil(t, afero.WriteFile(fsys, "some.json", b, 0644)) + + out, err := theme.LoadKV(fsys, "some.json") + assert.Nil(t, err) + assert.Equal(t, in, out) +} + +func TestWriteKV(t *testing.T) { + in := theme.KV{ + "a": map[string]interface{}{ + "value": "a", + }, + "b": map[string]interface{}{ + "value": "b", + }, + } + + fsys := fsx.NewMemMapFs() + assert.Nil(t, theme.WriteKV(fsys, "some.json", in)) + + f, err := fsys.Open("some.json") + assert.Nil(t, err) + + var out theme.KV + assert.Nil(t, json.NewDecoder(f).Decode(&out)) + assert.Equal(t, in, out) +} + +func TestUpdateKV(t *testing.T) { + fileKV := theme.KV{ + "a": map[string]interface{}{ + "value": "a", + }, + "b": map[string]interface{}{ + "value": "b", + }, + } + + wb, err := json.Marshal(fileKV) + assert.Nil(t, err) + + fsys := fsx.NewMemMapFs() + assert.Nil(t, afero.WriteFile(fsys, "some.json", wb, 0644)) + assert.Nil(t, theme.UpdateKV(fsys, "some.json", theme.KV{ + "b.value": "b-new", + "c.value": "c-new", + })) + + f, err := fsys.Open("some.json") + assert.Nil(t, err) + + var out theme.KV + assert.Nil(t, json.NewDecoder(f).Decode(&out)) + assert.Equal(t, out, theme.KV{ + "a": map[string]interface{}{ + "value": "a", + }, + "b": map[string]interface{}{ + "value": "b-new", + }, + "c": map[string]interface{}{ + "value": "c-new", + }, + }) +} diff --git a/services/web/pkg/theme/service.go b/services/web/pkg/theme/service.go new file mode 100644 index 0000000000..b844df09ab --- /dev/null +++ b/services/web/pkg/theme/service.go @@ -0,0 +1,197 @@ +package theme + +import ( + "encoding/json" + "net/http" + + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + permissionsapi "github.com/cs3org/go-cs3apis/cs3/permissions/v1beta1" + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + "github.com/pkg/errors" + "github.com/spf13/afero" + + revactx "github.com/cs3org/reva/v2/pkg/ctx" + "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" + + "github.com/owncloud/ocis/v2/ocis-pkg/x/io/fsx" + "github.com/owncloud/ocis/v2/ocis-pkg/x/path/filepathx" +) + +// ServiceOptions defines the options to configure the Service. +type ServiceOptions struct { + themeFS *fsx.FallbackFS + gatewaySelector pool.Selectable[gateway.GatewayAPIClient] +} + +// WithThemeFS sets the theme filesystem. +func (o ServiceOptions) WithThemeFS(fSys *fsx.FallbackFS) ServiceOptions { + o.themeFS = fSys + return o +} + +// WithGatewaySelector sets the gateway selector. +func (o ServiceOptions) WithGatewaySelector(gws pool.Selectable[gateway.GatewayAPIClient]) ServiceOptions { + o.gatewaySelector = gws + return o +} + +// validate validates the input parameters. +func (o ServiceOptions) validate() error { + if o.themeFS == nil { + return errors.New("themeFS is required") + } + + if o.gatewaySelector == nil { + return errors.New("gatewaySelector is required") + } + + return nil +} + +// Service defines the http service. +type Service struct { + themeFS *fsx.FallbackFS + gatewaySelector pool.Selectable[gateway.GatewayAPIClient] +} + +// NewService initializes a new Service. +func NewService(options ServiceOptions) (Service, error) { + if err := options.validate(); err != nil { + return Service{}, err + } + + return Service(options), nil +} + +// Get renders the theme, the theme is a merge of the default theme, the base theme, and the branding theme. +func (s Service) Get(w http.ResponseWriter, r *http.Request) { + // there is no guarantee that the theme exists, its optional; therefore, we ignore the error + baseTheme, _ := LoadKV(s.themeFS, filepathx.JailJoin(r.PathValue("id"), _themeFileName)) + + // there is no guarantee that the theme exists, its optional; therefore, we ignore the error here too + brandingTheme, _ := LoadKV(s.themeFS, filepathx.JailJoin(_brandingRoot, _themeFileName)) + + // merge the themes, the order is important, the last one wins and overrides the previous ones + // themeDefaults: contains all the default values, this is guaranteed to exist + // baseTheme: contains the base theme from the theme fs, there is no guarantee that it exists + // brandingTheme: contains the branding theme from the theme fs, there is no guarantee that it exists + // mergedTheme = themeDefaults < baseTheme < brandingTheme + mergedTheme, err := MergeKV(themeDefaults, baseTheme, brandingTheme) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + b, err := json.Marshal(mergedTheme) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + _, err = w.Write(b) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// LogoUpload implements the endpoint to upload a custom logo for the oCIS instance. +func (s Service) LogoUpload(w http.ResponseWriter, r *http.Request) { + gatewayClient, err := s.gatewaySelector.Next() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + user := revactx.ContextMustGetUser(r.Context()) + rsp, err := gatewayClient.CheckPermission(r.Context(), &permissionsapi.CheckPermissionRequest{ + Permission: "Logo.Write", + SubjectRef: &permissionsapi.SubjectReference{ + Spec: &permissionsapi.SubjectReference_UserId{ + UserId: user.Id, + }, + }, + }) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + if rsp.Status.Code != rpc.Code_CODE_OK { + w.WriteHeader(http.StatusForbidden) + return + } + + file, fileHeader, err := r.FormFile("logo") + if err != nil { + if errors.Is(err, http.ErrMissingFile) { + w.WriteHeader(http.StatusBadRequest) + } + w.WriteHeader(http.StatusInternalServerError) + return + } + defer file.Close() + + if !isFiletypePermitted(fileHeader.Filename, fileHeader.Header.Get("Content-Type")) { + w.WriteHeader(http.StatusBadRequest) + return + } + + fp := filepathx.JailJoin(_brandingRoot, fileHeader.Filename) + err = afero.WriteReader(s.themeFS, fp, file) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + err = UpdateKV(s.themeFS, filepathx.JailJoin(_brandingRoot, _themeFileName), KV{ + "common.logo": filepathx.JailJoin("themes", fp), + "clients.web.defaults.logo.topbar": filepathx.JailJoin("themes", fp), + "clients.web.defaults.logo.login": filepathx.JailJoin("themes", fp), + }) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} + +// LogoReset implements the endpoint to reset the instance logo. +// The config will be changed back to use the embedded logo asset. +func (s Service) LogoReset(w http.ResponseWriter, r *http.Request) { + gatewayClient, err := s.gatewaySelector.Next() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + user := revactx.ContextMustGetUser(r.Context()) + rsp, err := gatewayClient.CheckPermission(r.Context(), &permissionsapi.CheckPermissionRequest{ + Permission: "Logo.Write", + SubjectRef: &permissionsapi.SubjectReference{ + Spec: &permissionsapi.SubjectReference_UserId{ + UserId: user.Id, + }, + }, + }) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + if rsp.Status.Code != rpc.Code_CODE_OK { + w.WriteHeader(http.StatusForbidden) + return + } + + err = UpdateKV(s.themeFS, filepathx.JailJoin(_brandingRoot, _themeFileName), KV{ + "common.logo": nil, + "clients.web.defaults.logo.topbar": nil, + "clients.web.defaults.logo.login": nil, + }) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} diff --git a/services/web/pkg/theme/service_test.go b/services/web/pkg/theme/service_test.go new file mode 100644 index 0000000000..9b8efcc037 --- /dev/null +++ b/services/web/pkg/theme/service_test.go @@ -0,0 +1,74 @@ +package theme_test + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/tidwall/gjson" + + "github.com/owncloud/ocis/v2/ocis-pkg/x/io/fsx" + "github.com/owncloud/ocis/v2/services/graph/mocks" + "github.com/owncloud/ocis/v2/services/graph/pkg/unifiedrole" + "github.com/owncloud/ocis/v2/services/web/pkg/theme" +) + +func TestNewService(t *testing.T) { + t.Run("fails if the options are invalid", func(t *testing.T) { + _, err := theme.NewService(theme.ServiceOptions{}) + assert.Error(t, err) + }) + + t.Run("success if the options are valid", func(t *testing.T) { + _, err := theme.NewService( + theme.ServiceOptions{}. + WithThemeFS(fsx.NewFallbackFS(fsx.NewMemMapFs(), fsx.NewMemMapFs())). + WithGatewaySelector(mocks.NewSelectable[gateway.GatewayAPIClient](t)), + ) + assert.NoError(t, err) + }) +} + +func TestService_Get(t *testing.T) { + primaryFS := fsx.NewMemMapFs() + fallbackFS := fsx.NewFallbackFS(primaryFS, fsx.NewMemMapFs()) + + add := func(filename string, content interface{}) { + b, err := json.Marshal(content) + assert.Nil(t, err) + + assert.Nil(t, afero.WriteFile(primaryFS, filename, b, 0644)) + } + + // baseTheme + add("base/theme.json", map[string]interface{}{ + "base": "base", + }) + // brandingTheme + add("_branding/theme.json", map[string]interface{}{ + "_branding": "_branding", + }) + + service, _ := theme.NewService( + theme.ServiceOptions{}. + WithThemeFS(fallbackFS). + WithGatewaySelector(mocks.NewSelectable[gateway.GatewayAPIClient](t)), + ) + r := httptest.NewRequest(http.MethodGet, "/", nil) + r.SetPathValue("id", "base") + + w := httptest.NewRecorder() + service.Get(w, r) + + jsonData := gjson.Parse(w.Body.String()) + // baseTheme + assert.Equal(t, jsonData.Get("base").String(), "base") + // brandingTheme + assert.Equal(t, jsonData.Get("_branding").String(), "_branding") + // themeDefaults + assert.Equal(t, jsonData.Get("common.shareRoles."+unifiedrole.UnifiedRoleViewerID+".name").String(), "UnifiedRoleViewer") +} diff --git a/services/web/pkg/theme/theme.go b/services/web/pkg/theme/theme.go new file mode 100644 index 0000000000..a6f8a57b90 --- /dev/null +++ b/services/web/pkg/theme/theme.go @@ -0,0 +1,61 @@ +package theme + +import ( + "path" + + "github.com/owncloud/ocis/v2/ocis-pkg/capabilities" + "github.com/owncloud/ocis/v2/services/graph/pkg/unifiedrole" +) + +var ( + _brandingRoot = "_branding" + _themeFileName = "theme.json" +) + +// themeDefaults contains the default values for the theme. +// all rendered themes get the default values from here. +var themeDefaults = KV{ + "common": KV{ + "shareRoles": KV{ + unifiedrole.UnifiedRoleViewerID: KV{ + "name": "UnifiedRoleViewer", + "iconName": "eye", + }, + unifiedrole.UnifiedRoleSpaceViewerID: KV{ + "label": "UnifiedRoleSpaceViewer", + "iconName": "eye", + }, + unifiedrole.UnifiedRoleFileEditorID: KV{ + "label": "UnifiedRoleFileEditor", + "iconName": "pencil", + }, + unifiedrole.UnifiedRoleEditorID: KV{ + "label": "UnifiedRoleEditor", + "iconName": "pencil", + }, + unifiedrole.UnifiedRoleSpaceEditorID: KV{ + "label": "UnifiedRoleSpaceEditor", + "iconName": "pencil", + }, + unifiedrole.UnifiedRoleManagerID: KV{ + "label": "UnifiedRoleManager", + "iconName": "user-star", + }, + unifiedrole.UnifiedRoleEditorLiteID: KV{ + "label": "UnifiedRoleEditorLite", + "iconName": "upload", + }, + unifiedrole.UnifiedRoleSecureViewerID: KV{ + "label": "UnifiedRoleSecureView", + "iconName": "shield", + }, + }, + }, +} + +// isFiletypePermitted checks if the given file extension is allowed. +func isFiletypePermitted(filename string, givenMime string) bool { + // Check if we allow that extension and if the mediatype matches the extension + extensionMime, ok := capabilities.Default().Theme.Logo.PermittedFileTypes[path.Ext(filename)] + return ok && extensionMime == givenMime +} diff --git a/services/web/pkg/theme/theme_test.go b/services/web/pkg/theme/theme_test.go new file mode 100644 index 0000000000..716476fc65 --- /dev/null +++ b/services/web/pkg/theme/theme_test.go @@ -0,0 +1,30 @@ +package theme_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/owncloud/ocis/v2/services/web/pkg/theme" +) + +// TestAllowedLogoFileTypes is here to ensure that a certain set of bare minimum file types are allowed for logos. +func TestAllowedLogoFileTypes(t *testing.T) { + type test struct { + filename string + mimetype string + allowed bool + } + + tests := []test{ + {filename: "foo.jpg", mimetype: "image/jpeg", allowed: true}, + {filename: "foo.jpeg", mimetype: "image/jpeg", allowed: true}, + {filename: "foo.png", mimetype: "image/png", allowed: true}, + {filename: "foo.gif", mimetype: "image/gif", allowed: true}, + {filename: "foo.tiff", mimetype: "image/tiff", allowed: false}, + } + + for _, tc := range tests { + assert.Equal(t, theme.IsFiletypePermitted(tc.filename, tc.mimetype), tc.allowed) + } +} diff --git a/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/status.go b/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/status.go index 282a97055b..6eb2e22c96 100644 --- a/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/status.go +++ b/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/status.go @@ -22,13 +22,13 @@ import ( "encoding/json" "net/http" - "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/data" "github.com/cs3org/reva/v2/pkg/appctx" + "github.com/cs3org/reva/v2/pkg/owncloud/ocs" ) func (s *svc) doStatus(w http.ResponseWriter, r *http.Request) { log := appctx.GetLogger(r.Context()) - status := &data.Status{ + status := &ocs.Status{ Installed: true, Maintenance: false, NeedsDBUpgrade: false, diff --git a/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/config/config.go b/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/config/config.go index 031eecf0dc..7a180259bf 100644 --- a/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/config/config.go +++ b/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/config/config.go @@ -20,6 +20,7 @@ package config import ( "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/data" + "github.com/cs3org/reva/v2/pkg/owncloud/ocs" "github.com/cs3org/reva/v2/pkg/sharedconf" "github.com/cs3org/reva/v2/pkg/storage/cache" ) @@ -28,7 +29,7 @@ import ( type Config struct { Prefix string `mapstructure:"prefix"` Config data.ConfigData `mapstructure:"config"` - Capabilities data.CapabilitiesData `mapstructure:"capabilities"` + Capabilities ocs.CapabilitiesData `mapstructure:"capabilities"` GatewaySvc string `mapstructure:"gatewaysvc"` StorageregistrySvc string `mapstructure:"storage_registry_svc"` DefaultUploadProtocol string `mapstructure:"default_upload_protocol"` diff --git a/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/handlers/cloud/capabilities/capabilities.go b/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/handlers/cloud/capabilities/capabilities.go index 330afc11c4..3e1afa6b2b 100644 --- a/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/handlers/cloud/capabilities/capabilities.go +++ b/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/handlers/cloud/capabilities/capabilities.go @@ -22,13 +22,13 @@ import ( "net/http" "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/config" - "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/data" "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/response" + "github.com/cs3org/reva/v2/pkg/owncloud/ocs" ) // Handler renders the capability endpoint type Handler struct { - c data.CapabilitiesData + c ocs.CapabilitiesData defaultUploadProtocol string userAgentChunkingMap map[string]string } @@ -41,13 +41,13 @@ func (h *Handler) Init(c *config.Config) { // capabilities if h.c.Capabilities == nil { - h.c.Capabilities = &data.Capabilities{} + h.c.Capabilities = &ocs.Capabilities{} } // core if h.c.Capabilities.Core == nil { - h.c.Capabilities.Core = &data.CapabilitiesCore{} + h.c.Capabilities.Core = &ocs.CapabilitiesCore{} } if h.c.Capabilities.Core.PollInterval == 0 { h.c.Capabilities.Core.PollInterval = 60 @@ -58,7 +58,7 @@ func (h *Handler) Init(c *config.Config) { // h.c.Capabilities.Core.SupportURLSigning is boolean if h.c.Capabilities.Core.Status == nil { - h.c.Capabilities.Core.Status = &data.Status{} + h.c.Capabilities.Core.Status = &ocs.Status{} } // h.c.Capabilities.Core.Status.Installed is boolean // h.c.Capabilities.Core.Status.Maintenance is boolean @@ -85,7 +85,7 @@ func (h *Handler) Init(c *config.Config) { // checksums if h.c.Capabilities.Checksums == nil { - h.c.Capabilities.Checksums = &data.CapabilitiesChecksums{} + h.c.Capabilities.Checksums = &ocs.CapabilitiesChecksums{} } if h.c.Capabilities.Checksums.SupportedTypes == nil { h.c.Capabilities.Checksums.SupportedTypes = []string{"SHA256"} @@ -97,7 +97,7 @@ func (h *Handler) Init(c *config.Config) { // files if h.c.Capabilities.Files == nil { - h.c.Capabilities.Files = &data.CapabilitiesFiles{} + h.c.Capabilities.Files = &ocs.CapabilitiesFiles{} } if h.c.Capabilities.Files.BlacklistedFiles == nil { @@ -108,17 +108,17 @@ func (h *Handler) Init(c *config.Config) { // h.c.Capabilities.Files.Favorites is boolean if h.c.Capabilities.Files.Archivers == nil { - h.c.Capabilities.Files.Archivers = []*data.CapabilitiesArchiver{} + h.c.Capabilities.Files.Archivers = []*ocs.CapabilitiesArchiver{} } if h.c.Capabilities.Files.AppProviders == nil { - h.c.Capabilities.Files.AppProviders = []*data.CapabilitiesAppProvider{} + h.c.Capabilities.Files.AppProviders = []*ocs.CapabilitiesAppProvider{} } // dav if h.c.Capabilities.Dav == nil { - h.c.Capabilities.Dav = &data.CapabilitiesDav{} + h.c.Capabilities.Dav = &ocs.CapabilitiesDav{} } if h.c.Capabilities.Dav.Trashbin == "" { h.c.Capabilities.Dav.Trashbin = "1.0" @@ -130,24 +130,24 @@ func (h *Handler) Init(c *config.Config) { // sharing if h.c.Capabilities.FilesSharing == nil { - h.c.Capabilities.FilesSharing = &data.CapabilitiesFilesSharing{} + h.c.Capabilities.FilesSharing = &ocs.CapabilitiesFilesSharing{} } // h.c.Capabilities.FilesSharing.APIEnabled is boolean if h.c.Capabilities.FilesSharing.Public == nil { - h.c.Capabilities.FilesSharing.Public = &data.CapabilitiesFilesSharingPublic{} + h.c.Capabilities.FilesSharing.Public = &ocs.CapabilitiesFilesSharingPublic{} } // h.c.Capabilities.FilesSharing.IsPublic.Enabled is boolean h.c.Capabilities.FilesSharing.Public.Enabled = true if h.c.Capabilities.FilesSharing.Public.Password == nil { - h.c.Capabilities.FilesSharing.Public.Password = &data.CapabilitiesFilesSharingPublicPassword{} + h.c.Capabilities.FilesSharing.Public.Password = &ocs.CapabilitiesFilesSharingPublicPassword{} } if h.c.Capabilities.FilesSharing.Public.Password.EnforcedFor == nil { - h.c.Capabilities.FilesSharing.Public.Password.EnforcedFor = &data.CapabilitiesFilesSharingPublicPasswordEnforcedFor{} + h.c.Capabilities.FilesSharing.Public.Password.EnforcedFor = &ocs.CapabilitiesFilesSharingPublicPasswordEnforcedFor{} } // h.c.Capabilities.FilesSharing.IsPublic.Password.EnforcedFor.ReadOnly is boolean @@ -158,7 +158,7 @@ func (h *Handler) Init(c *config.Config) { // h.c.Capabilities.FilesSharing.IsPublic.Password.Enforced is boolean if h.c.Capabilities.FilesSharing.Public.ExpireDate == nil { - h.c.Capabilities.FilesSharing.Public.ExpireDate = &data.CapabilitiesFilesSharingPublicExpireDate{} + h.c.Capabilities.FilesSharing.Public.ExpireDate = &ocs.CapabilitiesFilesSharingPublicExpireDate{} } // h.c.Capabilities.FilesSharing.IsPublic.ExpireDate.Enabled is boolean @@ -169,7 +169,7 @@ func (h *Handler) Init(c *config.Config) { // h.c.Capabilities.FilesSharing.IsPublic.SupportsUploadOnly is boolean if h.c.Capabilities.FilesSharing.User == nil { - h.c.Capabilities.FilesSharing.User = &data.CapabilitiesFilesSharingUser{} + h.c.Capabilities.FilesSharing.User = &ocs.CapabilitiesFilesSharingUser{} } // h.c.Capabilities.FilesSharing.User.SendMail is boolean @@ -182,7 +182,7 @@ func (h *Handler) Init(c *config.Config) { // h.c.Capabilities.FilesSharing.ShareWithMembershipGroupsOnly is boolean if h.c.Capabilities.FilesSharing.UserEnumeration == nil { - h.c.Capabilities.FilesSharing.UserEnumeration = &data.CapabilitiesFilesSharingUserEnumeration{} + h.c.Capabilities.FilesSharing.UserEnumeration = &ocs.CapabilitiesFilesSharingUserEnumeration{} } // h.c.Capabilities.FilesSharing.UserEnumeration.Enabled is boolean @@ -192,7 +192,7 @@ func (h *Handler) Init(c *config.Config) { h.c.Capabilities.FilesSharing.DefaultPermissions = 31 } if h.c.Capabilities.FilesSharing.Federation == nil { - h.c.Capabilities.FilesSharing.Federation = &data.CapabilitiesFilesSharingFederation{} + h.c.Capabilities.FilesSharing.Federation = &ocs.CapabilitiesFilesSharingFederation{} } // h.c.Capabilities.FilesSharing.Federation.Outgoing is boolean @@ -205,7 +205,7 @@ func (h *Handler) Init(c *config.Config) { // notifications // if h.c.Capabilities.Notifications == nil { - // h.c.Capabilities.Notifications = &data.CapabilitiesNotifications{} + // h.c.Capabilities.Notifications = &ocs.CapabilitiesNotifications{} // } // if h.c.Capabilities.Notifications.Endpoints == nil { // h.c.Capabilities.Notifications.Endpoints = []string{"list", "get", "delete"} @@ -214,7 +214,7 @@ func (h *Handler) Init(c *config.Config) { // version if h.c.Version == nil { - h.c.Version = &data.Version{ + h.c.Version = &ocs.Version{ // TODO get from build env Major: 10, Minor: 0, diff --git a/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/handlers/cloud/capabilities/uploads.go b/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/handlers/cloud/capabilities/uploads.go index ffa09b2983..1184a30b47 100644 --- a/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/handlers/cloud/capabilities/uploads.go +++ b/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/handlers/cloud/capabilities/uploads.go @@ -21,7 +21,7 @@ package capabilities import ( "strings" - "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/data" + "github.com/cs3org/reva/v2/pkg/owncloud/ocs" ) type chunkProtocol string @@ -32,7 +32,7 @@ var ( chunkTUS chunkProtocol = "tus" ) -func (h *Handler) getCapabilitiesForUserAgent(userAgent string) data.CapabilitiesData { +func (h *Handler) getCapabilitiesForUserAgent(userAgent string) ocs.CapabilitiesData { if userAgent != "" { for k, v := range h.userAgentChunkingMap { // we could also use a regexp for pattern matching @@ -47,7 +47,7 @@ func (h *Handler) getCapabilitiesForUserAgent(userAgent string) data.Capabilitie return h.c } -func setCapabilitiesForChunkProtocol(cp chunkProtocol, c *data.CapabilitiesData) { +func setCapabilitiesForChunkProtocol(cp chunkProtocol, c *ocs.CapabilitiesData) { switch cp { case chunkV1: // 2.7+ will use Chunking V1 if "capabilities > files > bigfilechunking" is "true" AND "capabilities > dav > chunking" is not there diff --git a/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/data/capabilities.go b/vendor/github.com/cs3org/reva/v2/pkg/owncloud/ocs/capabilities.go similarity index 96% rename from vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/data/capabilities.go rename to vendor/github.com/cs3org/reva/v2/pkg/owncloud/ocs/capabilities.go index 82e2fdf2b9..1f4bcdab6f 100644 --- a/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/data/capabilities.go +++ b/vendor/github.com/cs3org/reva/v2/pkg/owncloud/ocs/capabilities.go @@ -16,7 +16,7 @@ // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. -package data +package ocs import ( "encoding/xml" @@ -42,7 +42,7 @@ func (c ocsBool) MarshalXML(e *xml.Encoder, start xml.StartElement) error { return e.EncodeElement("0", start) } -// CapabilitiesData TODO document +// CapabilitiesData holds the capabilities data type CapabilitiesData struct { Capabilities *Capabilities `json:"capabilities" xml:"capabilities"` Version *Version `json:"version" xml:"version"` @@ -59,6 +59,7 @@ type Capabilities struct { Graph *CapabilitiesGraph `json:"graph,omitempty" xml:"graph,omitempty" mapstructure:"graph"` PasswordPolicy *CapabilitiesPasswordPolicy `json:"password_policy,omitempty" xml:"password_policy,omitempty" mapstructure:"password_policy"` Search *CapabilitiesSearch `json:"search,omitempty" xml:"search,omitempty" mapstructure:"search"` + Theme *CapabilitiesTheme `json:"theme,omitempty" xml:"theme,omitempty" mapstructure:"theme"` Notifications *CapabilitiesNotifications `json:"notifications,omitempty" xml:"notifications,omitempty"` } @@ -290,6 +291,17 @@ type CapabilitiesNotifications struct { Endpoints []string `json:"ocs-endpoints,omitempty" xml:"ocs-endpoints>element,omitempty" mapstructure:"endpoints"` } +// CapabilitiesTheme holds theming capabilities +type CapabilitiesTheme struct { + Logo *CapabilitiesThemeLogo `json:"logo" xml:"logo" mapstructure:"logo"` +} + +// CapabilitiesThemeLogo holds theming logo capabilities +type CapabilitiesThemeLogo struct { + // xml marshal, unmarshal does not support map[string]string, needs a custom type with MarshalXML and UnmarshalXML implementations + PermittedFileTypes map[string]string `json:"permitted_file_types" xml:"-" mapstructure:"permitted_file_types"` +} + // Version holds version information type Version struct { Major int `json:"major" xml:"major"` diff --git a/vendor/github.com/tidwall/sjson/LICENSE b/vendor/github.com/tidwall/sjson/LICENSE new file mode 100644 index 0000000000..89593c7c84 --- /dev/null +++ b/vendor/github.com/tidwall/sjson/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Josh Baker + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/vendor/github.com/tidwall/sjson/README.md b/vendor/github.com/tidwall/sjson/README.md new file mode 100644 index 0000000000..4598424efa --- /dev/null +++ b/vendor/github.com/tidwall/sjson/README.md @@ -0,0 +1,278 @@ +

+SJSON +
+GoDoc +

+ +

set a json value quickly

+ +SJSON is a Go package that provides a [very fast](#performance) and simple way to set a value in a json document. +For quickly retrieving json values check out [GJSON](https://github.com/tidwall/gjson). + +For a command line interface check out [JJ](https://github.com/tidwall/jj). + +Getting Started +=============== + +Installing +---------- + +To start using SJSON, install Go and run `go get`: + +```sh +$ go get -u github.com/tidwall/sjson +``` + +This will retrieve the library. + +Set a value +----------- +Set sets the value for the specified path. +A path is in dot syntax, such as "name.last" or "age". +This function expects that the json is well-formed and validated. +Invalid json will not panic, but it may return back unexpected results. +Invalid paths may return an error. + +```go +package main + +import "github.com/tidwall/sjson" + +const json = `{"name":{"first":"Janet","last":"Prichard"},"age":47}` + +func main() { + value, _ := sjson.Set(json, "name.last", "Anderson") + println(value) +} +``` + +This will print: + +```json +{"name":{"first":"Janet","last":"Anderson"},"age":47} +``` + +Path syntax +----------- + +A path is a series of keys separated by a dot. +The dot and colon characters can be escaped with ``\``. + +```json +{ + "name": {"first": "Tom", "last": "Anderson"}, + "age":37, + "children": ["Sara","Alex","Jack"], + "fav.movie": "Deer Hunter", + "friends": [ + {"first": "James", "last": "Murphy"}, + {"first": "Roger", "last": "Craig"} + ] +} +``` +``` +"name.last" >> "Anderson" +"age" >> 37 +"children.1" >> "Alex" +"friends.1.last" >> "Craig" +``` + +The `-1` key can be used to append a value to an existing array: + +``` +"children.-1" >> appends a new value to the end of the children array +``` + +Normally number keys are used to modify arrays, but it's possible to force a numeric object key by using the colon character: + +```json +{ + "users":{ + "2313":{"name":"Sara"}, + "7839":{"name":"Andy"} + } +} +``` + +A colon path would look like: + +``` +"users.:2313.name" >> "Sara" +``` + +Supported types +--------------- + +Pretty much any type is supported: + +```go +sjson.Set(`{"key":true}`, "key", nil) +sjson.Set(`{"key":true}`, "key", false) +sjson.Set(`{"key":true}`, "key", 1) +sjson.Set(`{"key":true}`, "key", 10.5) +sjson.Set(`{"key":true}`, "key", "hello") +sjson.Set(`{"key":true}`, "key", []string{"hello", "world"}) +sjson.Set(`{"key":true}`, "key", map[string]interface{}{"hello":"world"}) +``` + +When a type is not recognized, SJSON will fallback to the `encoding/json` Marshaller. + + +Examples +-------- + +Set a value from empty document: +```go +value, _ := sjson.Set("", "name", "Tom") +println(value) + +// Output: +// {"name":"Tom"} +``` + +Set a nested value from empty document: +```go +value, _ := sjson.Set("", "name.last", "Anderson") +println(value) + +// Output: +// {"name":{"last":"Anderson"}} +``` + +Set a new value: +```go +value, _ := sjson.Set(`{"name":{"last":"Anderson"}}`, "name.first", "Sara") +println(value) + +// Output: +// {"name":{"first":"Sara","last":"Anderson"}} +``` + +Update an existing value: +```go +value, _ := sjson.Set(`{"name":{"last":"Anderson"}}`, "name.last", "Smith") +println(value) + +// Output: +// {"name":{"last":"Smith"}} +``` + +Set a new array value: +```go +value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.2", "Sara") +println(value) + +// Output: +// {"friends":["Andy","Carol","Sara"] +``` + +Append an array value by using the `-1` key in a path: +```go +value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.-1", "Sara") +println(value) + +// Output: +// {"friends":["Andy","Carol","Sara"] +``` + +Append an array value that is past the end: +```go +value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.4", "Sara") +println(value) + +// Output: +// {"friends":["Andy","Carol",null,null,"Sara"] +``` + +Delete a value: +```go +value, _ := sjson.Delete(`{"name":{"first":"Sara","last":"Anderson"}}`, "name.first") +println(value) + +// Output: +// {"name":{"last":"Anderson"}} +``` + +Delete an array value: +```go +value, _ := sjson.Delete(`{"friends":["Andy","Carol"]}`, "friends.1") +println(value) + +// Output: +// {"friends":["Andy"]} +``` + +Delete the last array value: +```go +value, _ := sjson.Delete(`{"friends":["Andy","Carol"]}`, "friends.-1") +println(value) + +// Output: +// {"friends":["Andy"]} +``` + +## Performance + +Benchmarks of SJSON alongside [encoding/json](https://golang.org/pkg/encoding/json/), +[ffjson](https://github.com/pquerna/ffjson), +[EasyJSON](https://github.com/mailru/easyjson), +and [Gabs](https://github.com/Jeffail/gabs) + +``` +Benchmark_SJSON-8 3000000 805 ns/op 1077 B/op 3 allocs/op +Benchmark_SJSON_ReplaceInPlace-8 3000000 449 ns/op 0 B/op 0 allocs/op +Benchmark_JSON_Map-8 300000 21236 ns/op 6392 B/op 150 allocs/op +Benchmark_JSON_Struct-8 300000 14691 ns/op 1789 B/op 24 allocs/op +Benchmark_Gabs-8 300000 21311 ns/op 6752 B/op 150 allocs/op +Benchmark_FFJSON-8 300000 17673 ns/op 3589 B/op 47 allocs/op +Benchmark_EasyJSON-8 1500000 3119 ns/op 1061 B/op 13 allocs/op +``` + +JSON document used: + +```json +{ + "widget": { + "debug": "on", + "window": { + "title": "Sample Konfabulator Widget", + "name": "main_window", + "width": 500, + "height": 500 + }, + "image": { + "src": "Images/Sun.png", + "hOffset": 250, + "vOffset": 250, + "alignment": "center" + }, + "text": { + "data": "Click Here", + "size": 36, + "style": "bold", + "vOffset": 100, + "alignment": "center", + "onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;" + } + } +} +``` + +Each operation was rotated though one of the following search paths: + +``` +widget.window.name +widget.image.hOffset +widget.text.onMouseUp +``` + +*These benchmarks were run on a MacBook Pro 15" 2.8 GHz Intel Core i7 using Go 1.7 and can be be found [here](https://github.com/tidwall/sjson-benchmarks)*. + +## Contact +Josh Baker [@tidwall](http://twitter.com/tidwall) + +## License + +SJSON source code is available under the MIT [License](/LICENSE). diff --git a/vendor/github.com/tidwall/sjson/logo.png b/vendor/github.com/tidwall/sjson/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b5aa257b6b5a72bf7d759c23d94c5928cd8eb318 GIT binary patch literal 16874 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 literal 0 HcmV?d00001 diff --git a/vendor/github.com/tidwall/sjson/sjson.go b/vendor/github.com/tidwall/sjson/sjson.go new file mode 100644 index 0000000000..a55eef3fdb --- /dev/null +++ b/vendor/github.com/tidwall/sjson/sjson.go @@ -0,0 +1,737 @@ +// Package sjson provides setting json values. +package sjson + +import ( + jsongo "encoding/json" + "sort" + "strconv" + "unsafe" + + "github.com/tidwall/gjson" +) + +type errorType struct { + msg string +} + +func (err *errorType) Error() string { + return err.msg +} + +// Options represents additional options for the Set and Delete functions. +type Options struct { + // Optimistic is a hint that the value likely exists which + // allows for the sjson to perform a fast-track search and replace. + Optimistic bool + // ReplaceInPlace is a hint to replace the input json rather than + // allocate a new json byte slice. When this field is specified + // the input json will not longer be valid and it should not be used + // In the case when the destination slice doesn't have enough free + // bytes to replace the data in place, a new bytes slice will be + // created under the hood. + // The Optimistic flag must be set to true and the input must be a + // byte slice in order to use this field. + ReplaceInPlace bool +} + +type pathResult struct { + part string // current key part + gpart string // gjson get part + path string // remaining path + force bool // force a string key + more bool // there is more path to parse +} + +func isSimpleChar(ch byte) bool { + switch ch { + case '|', '#', '@', '*', '?': + return false + default: + return true + } +} + +func parsePath(path string) (res pathResult, simple bool) { + var r pathResult + if len(path) > 0 && path[0] == ':' { + r.force = true + path = path[1:] + } + for i := 0; i < len(path); i++ { + if path[i] == '.' { + r.part = path[:i] + r.gpart = path[:i] + r.path = path[i+1:] + r.more = true + return r, true + } + if !isSimpleChar(path[i]) { + return r, false + } + if path[i] == '\\' { + // go into escape mode. this is a slower path that + // strips off the escape character from the part. + epart := []byte(path[:i]) + gpart := []byte(path[:i+1]) + i++ + if i < len(path) { + epart = append(epart, path[i]) + gpart = append(gpart, path[i]) + i++ + for ; i < len(path); i++ { + if path[i] == '\\' { + gpart = append(gpart, '\\') + i++ + if i < len(path) { + epart = append(epart, path[i]) + gpart = append(gpart, path[i]) + } + continue + } else if path[i] == '.' { + r.part = string(epart) + r.gpart = string(gpart) + r.path = path[i+1:] + r.more = true + return r, true + } else if !isSimpleChar(path[i]) { + return r, false + } + epart = append(epart, path[i]) + gpart = append(gpart, path[i]) + } + } + // append the last part + r.part = string(epart) + r.gpart = string(gpart) + return r, true + } + } + r.part = path + r.gpart = path + return r, true +} + +func mustMarshalString(s string) bool { + for i := 0; i < len(s); i++ { + if s[i] < ' ' || s[i] > 0x7f || s[i] == '"' || s[i] == '\\' { + return true + } + } + return false +} + +// appendStringify makes a json string and appends to buf. +func appendStringify(buf []byte, s string) []byte { + if mustMarshalString(s) { + b, _ := jsongo.Marshal(s) + return append(buf, b...) + } + buf = append(buf, '"') + buf = append(buf, s...) + buf = append(buf, '"') + return buf +} + +// appendBuild builds a json block from a json path. +func appendBuild(buf []byte, array bool, paths []pathResult, raw string, + stringify bool) []byte { + if !array { + buf = appendStringify(buf, paths[0].part) + buf = append(buf, ':') + } + if len(paths) > 1 { + n, numeric := atoui(paths[1]) + if numeric || (!paths[1].force && paths[1].part == "-1") { + buf = append(buf, '[') + buf = appendRepeat(buf, "null,", n) + buf = appendBuild(buf, true, paths[1:], raw, stringify) + buf = append(buf, ']') + } else { + buf = append(buf, '{') + buf = appendBuild(buf, false, paths[1:], raw, stringify) + buf = append(buf, '}') + } + } else { + if stringify { + buf = appendStringify(buf, raw) + } else { + buf = append(buf, raw...) + } + } + return buf +} + +// atoui does a rip conversion of string -> unigned int. +func atoui(r pathResult) (n int, ok bool) { + if r.force { + return 0, false + } + for i := 0; i < len(r.part); i++ { + if r.part[i] < '0' || r.part[i] > '9' { + return 0, false + } + n = n*10 + int(r.part[i]-'0') + } + return n, true +} + +// appendRepeat repeats string "n" times and appends to buf. +func appendRepeat(buf []byte, s string, n int) []byte { + for i := 0; i < n; i++ { + buf = append(buf, s...) + } + return buf +} + +// trim does a rip trim +func trim(s string) string { + for len(s) > 0 { + if s[0] <= ' ' { + s = s[1:] + continue + } + break + } + for len(s) > 0 { + if s[len(s)-1] <= ' ' { + s = s[:len(s)-1] + continue + } + break + } + return s +} + +// deleteTailItem deletes the previous key or comma. +func deleteTailItem(buf []byte) ([]byte, bool) { +loop: + for i := len(buf) - 1; i >= 0; i-- { + // look for either a ',',':','[' + switch buf[i] { + case '[': + return buf, true + case ',': + return buf[:i], false + case ':': + // delete tail string + i-- + for ; i >= 0; i-- { + if buf[i] == '"' { + i-- + for ; i >= 0; i-- { + if buf[i] == '"' { + i-- + if i >= 0 && buf[i] == '\\' { + i-- + continue + } + for ; i >= 0; i-- { + // look for either a ',','{' + switch buf[i] { + case '{': + return buf[:i+1], true + case ',': + return buf[:i], false + } + } + } + } + break + } + } + break loop + } + } + return buf, false +} + +var errNoChange = &errorType{"no change"} + +func appendRawPaths(buf []byte, jstr string, paths []pathResult, raw string, + stringify, del bool) ([]byte, error) { + var err error + var res gjson.Result + var found bool + if del { + if paths[0].part == "-1" && !paths[0].force { + res = gjson.Get(jstr, "#") + if res.Int() > 0 { + res = gjson.Get(jstr, strconv.FormatInt(int64(res.Int()-1), 10)) + found = true + } + } + } + if !found { + res = gjson.Get(jstr, paths[0].gpart) + } + if res.Index > 0 { + if len(paths) > 1 { + buf = append(buf, jstr[:res.Index]...) + buf, err = appendRawPaths(buf, res.Raw, paths[1:], raw, + stringify, del) + if err != nil { + return nil, err + } + buf = append(buf, jstr[res.Index+len(res.Raw):]...) + return buf, nil + } + buf = append(buf, jstr[:res.Index]...) + var exidx int // additional forward stripping + if del { + var delNextComma bool + buf, delNextComma = deleteTailItem(buf) + if delNextComma { + i, j := res.Index+len(res.Raw), 0 + for ; i < len(jstr); i, j = i+1, j+1 { + if jstr[i] <= ' ' { + continue + } + if jstr[i] == ',' { + exidx = j + 1 + } + break + } + } + } else { + if stringify { + buf = appendStringify(buf, raw) + } else { + buf = append(buf, raw...) + } + } + buf = append(buf, jstr[res.Index+len(res.Raw)+exidx:]...) + return buf, nil + } + if del { + return nil, errNoChange + } + n, numeric := atoui(paths[0]) + isempty := true + for i := 0; i < len(jstr); i++ { + if jstr[i] > ' ' { + isempty = false + break + } + } + if isempty { + if numeric { + jstr = "[]" + } else { + jstr = "{}" + } + } + jsres := gjson.Parse(jstr) + if jsres.Type != gjson.JSON { + if numeric { + jstr = "[]" + } else { + jstr = "{}" + } + jsres = gjson.Parse(jstr) + } + var comma bool + for i := 1; i < len(jsres.Raw); i++ { + if jsres.Raw[i] <= ' ' { + continue + } + if jsres.Raw[i] == '}' || jsres.Raw[i] == ']' { + break + } + comma = true + break + } + switch jsres.Raw[0] { + default: + return nil, &errorType{"json must be an object or array"} + case '{': + end := len(jsres.Raw) - 1 + for ; end > 0; end-- { + if jsres.Raw[end] == '}' { + break + } + } + buf = append(buf, jsres.Raw[:end]...) + if comma { + buf = append(buf, ',') + } + buf = appendBuild(buf, false, paths, raw, stringify) + buf = append(buf, '}') + return buf, nil + case '[': + var appendit bool + if !numeric { + if paths[0].part == "-1" && !paths[0].force { + appendit = true + } else { + return nil, &errorType{ + "cannot set array element for non-numeric key '" + + paths[0].part + "'"} + } + } + if appendit { + njson := trim(jsres.Raw) + if njson[len(njson)-1] == ']' { + njson = njson[:len(njson)-1] + } + buf = append(buf, njson...) + if comma { + buf = append(buf, ',') + } + + buf = appendBuild(buf, true, paths, raw, stringify) + buf = append(buf, ']') + return buf, nil + } + buf = append(buf, '[') + ress := jsres.Array() + for i := 0; i < len(ress); i++ { + if i > 0 { + buf = append(buf, ',') + } + buf = append(buf, ress[i].Raw...) + } + if len(ress) == 0 { + buf = appendRepeat(buf, "null,", n-len(ress)) + } else { + buf = appendRepeat(buf, ",null", n-len(ress)) + if comma { + buf = append(buf, ',') + } + } + buf = appendBuild(buf, true, paths, raw, stringify) + buf = append(buf, ']') + return buf, nil + } +} + +func isOptimisticPath(path string) bool { + for i := 0; i < len(path); i++ { + if path[i] < '.' || path[i] > 'z' { + return false + } + if path[i] > '9' && path[i] < 'A' { + return false + } + if path[i] > 'z' { + return false + } + } + return true +} + +// Set sets a json value for the specified path. +// A path is in dot syntax, such as "name.last" or "age". +// This function expects that the json is well-formed, and does not validate. +// Invalid json will not panic, but it may return back unexpected results. +// An error is returned if the path is not valid. +// +// A path is a series of keys separated by a dot. +// +// { +// "name": {"first": "Tom", "last": "Anderson"}, +// "age":37, +// "children": ["Sara","Alex","Jack"], +// "friends": [ +// {"first": "James", "last": "Murphy"}, +// {"first": "Roger", "last": "Craig"} +// ] +// } +// "name.last" >> "Anderson" +// "age" >> 37 +// "children.1" >> "Alex" +// +func Set(json, path string, value interface{}) (string, error) { + return SetOptions(json, path, value, nil) +} + +// SetBytes sets a json value for the specified path. +// If working with bytes, this method preferred over +// Set(string(data), path, value) +func SetBytes(json []byte, path string, value interface{}) ([]byte, error) { + return SetBytesOptions(json, path, value, nil) +} + +// SetRaw sets a raw json value for the specified path. +// This function works the same as Set except that the value is set as a +// raw block of json. This allows for setting premarshalled json objects. +func SetRaw(json, path, value string) (string, error) { + return SetRawOptions(json, path, value, nil) +} + +// SetRawOptions sets a raw json value for the specified path with options. +// This furnction works the same as SetOptions except that the value is set +// as a raw block of json. This allows for setting premarshalled json objects. +func SetRawOptions(json, path, value string, opts *Options) (string, error) { + var optimistic bool + if opts != nil { + optimistic = opts.Optimistic + } + res, err := set(json, path, value, false, false, optimistic, false) + if err == errNoChange { + return json, nil + } + return string(res), err +} + +// SetRawBytes sets a raw json value for the specified path. +// If working with bytes, this method preferred over +// SetRaw(string(data), path, value) +func SetRawBytes(json []byte, path string, value []byte) ([]byte, error) { + return SetRawBytesOptions(json, path, value, nil) +} + +type dtype struct{} + +// Delete deletes a value from json for the specified path. +func Delete(json, path string) (string, error) { + return Set(json, path, dtype{}) +} + +// DeleteBytes deletes a value from json for the specified path. +func DeleteBytes(json []byte, path string) ([]byte, error) { + return SetBytes(json, path, dtype{}) +} + +type stringHeader struct { + data unsafe.Pointer + len int +} + +type sliceHeader struct { + data unsafe.Pointer + len int + cap int +} + +func set(jstr, path, raw string, + stringify, del, optimistic, inplace bool) ([]byte, error) { + if path == "" { + return []byte(jstr), &errorType{"path cannot be empty"} + } + if !del && optimistic && isOptimisticPath(path) { + res := gjson.Get(jstr, path) + if res.Exists() && res.Index > 0 { + sz := len(jstr) - len(res.Raw) + len(raw) + if stringify { + sz += 2 + } + if inplace && sz <= len(jstr) { + if !stringify || !mustMarshalString(raw) { + jsonh := *(*stringHeader)(unsafe.Pointer(&jstr)) + jsonbh := sliceHeader{ + data: jsonh.data, len: jsonh.len, cap: jsonh.len} + jbytes := *(*[]byte)(unsafe.Pointer(&jsonbh)) + if stringify { + jbytes[res.Index] = '"' + copy(jbytes[res.Index+1:], []byte(raw)) + jbytes[res.Index+1+len(raw)] = '"' + copy(jbytes[res.Index+1+len(raw)+1:], + jbytes[res.Index+len(res.Raw):]) + } else { + copy(jbytes[res.Index:], []byte(raw)) + copy(jbytes[res.Index+len(raw):], + jbytes[res.Index+len(res.Raw):]) + } + return jbytes[:sz], nil + } + return []byte(jstr), nil + } + buf := make([]byte, 0, sz) + buf = append(buf, jstr[:res.Index]...) + if stringify { + buf = appendStringify(buf, raw) + } else { + buf = append(buf, raw...) + } + buf = append(buf, jstr[res.Index+len(res.Raw):]...) + return buf, nil + } + } + var paths []pathResult + r, simple := parsePath(path) + if simple { + paths = append(paths, r) + for r.more { + r, simple = parsePath(r.path) + if !simple { + break + } + paths = append(paths, r) + } + } + if !simple { + if del { + return []byte(jstr), + &errorType{"cannot delete value from a complex path"} + } + return setComplexPath(jstr, path, raw, stringify) + } + njson, err := appendRawPaths(nil, jstr, paths, raw, stringify, del) + if err != nil { + return []byte(jstr), err + } + return njson, nil +} + +func setComplexPath(jstr, path, raw string, stringify bool) ([]byte, error) { + res := gjson.Get(jstr, path) + if !res.Exists() || !(res.Index != 0 || len(res.Indexes) != 0) { + return []byte(jstr), errNoChange + } + if res.Index != 0 { + njson := []byte(jstr[:res.Index]) + if stringify { + njson = appendStringify(njson, raw) + } else { + njson = append(njson, raw...) + } + njson = append(njson, jstr[res.Index+len(res.Raw):]...) + jstr = string(njson) + } + if len(res.Indexes) > 0 { + type val struct { + index int + res gjson.Result + } + vals := make([]val, 0, len(res.Indexes)) + res.ForEach(func(_, vres gjson.Result) bool { + vals = append(vals, val{res: vres}) + return true + }) + if len(res.Indexes) != len(vals) { + return []byte(jstr), errNoChange + } + for i := 0; i < len(res.Indexes); i++ { + vals[i].index = res.Indexes[i] + } + sort.SliceStable(vals, func(i, j int) bool { + return vals[i].index > vals[j].index + }) + for _, val := range vals { + vres := val.res + index := val.index + njson := []byte(jstr[:index]) + if stringify { + njson = appendStringify(njson, raw) + } else { + njson = append(njson, raw...) + } + njson = append(njson, jstr[index+len(vres.Raw):]...) + jstr = string(njson) + } + } + return []byte(jstr), nil +} + +// SetOptions sets a json value for the specified path with options. +// A path is in dot syntax, such as "name.last" or "age". +// This function expects that the json is well-formed, and does not validate. +// Invalid json will not panic, but it may return back unexpected results. +// An error is returned if the path is not valid. +func SetOptions(json, path string, value interface{}, + opts *Options) (string, error) { + if opts != nil { + if opts.ReplaceInPlace { + // it's not safe to replace bytes in-place for strings + // copy the Options and set options.ReplaceInPlace to false. + nopts := *opts + opts = &nopts + opts.ReplaceInPlace = false + } + } + jsonh := *(*stringHeader)(unsafe.Pointer(&json)) + jsonbh := sliceHeader{data: jsonh.data, len: jsonh.len, cap: jsonh.len} + jsonb := *(*[]byte)(unsafe.Pointer(&jsonbh)) + res, err := SetBytesOptions(jsonb, path, value, opts) + return string(res), err +} + +// SetBytesOptions sets a json value for the specified path with options. +// If working with bytes, this method preferred over +// SetOptions(string(data), path, value) +func SetBytesOptions(json []byte, path string, value interface{}, + opts *Options) ([]byte, error) { + var optimistic, inplace bool + if opts != nil { + optimistic = opts.Optimistic + inplace = opts.ReplaceInPlace + } + jstr := *(*string)(unsafe.Pointer(&json)) + var res []byte + var err error + switch v := value.(type) { + default: + b, merr := jsongo.Marshal(value) + if merr != nil { + return nil, merr + } + raw := *(*string)(unsafe.Pointer(&b)) + res, err = set(jstr, path, raw, false, false, optimistic, inplace) + case dtype: + res, err = set(jstr, path, "", false, true, optimistic, inplace) + case string: + res, err = set(jstr, path, v, true, false, optimistic, inplace) + case []byte: + raw := *(*string)(unsafe.Pointer(&v)) + res, err = set(jstr, path, raw, true, false, optimistic, inplace) + case bool: + if v { + res, err = set(jstr, path, "true", false, false, optimistic, inplace) + } else { + res, err = set(jstr, path, "false", false, false, optimistic, inplace) + } + case int8: + res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), + false, false, optimistic, inplace) + case int16: + res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), + false, false, optimistic, inplace) + case int32: + res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), + false, false, optimistic, inplace) + case int64: + res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), + false, false, optimistic, inplace) + case uint8: + res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), + false, false, optimistic, inplace) + case uint16: + res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), + false, false, optimistic, inplace) + case uint32: + res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), + false, false, optimistic, inplace) + case uint64: + res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), + false, false, optimistic, inplace) + case float32: + res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64), + false, false, optimistic, inplace) + case float64: + res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64), + false, false, optimistic, inplace) + } + if err == errNoChange { + return json, nil + } + return res, err +} + +// SetRawBytesOptions sets a raw json value for the specified path with options. +// If working with bytes, this method preferred over +// SetRawOptions(string(data), path, value, opts) +func SetRawBytesOptions(json []byte, path string, value []byte, + opts *Options) ([]byte, error) { + jstr := *(*string)(unsafe.Pointer(&json)) + vstr := *(*string)(unsafe.Pointer(&value)) + var optimistic, inplace bool + if opts != nil { + optimistic = opts.Optimistic + inplace = opts.ReplaceInPlace + } + res, err := set(jstr, path, vstr, false, false, optimistic, inplace) + if err == errNoChange { + return json, nil + } + return res, err +} diff --git a/vendor/modules.txt b/vendor/modules.txt index f4167c74dc..e71769596a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -366,7 +366,7 @@ github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1 github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1 github.com/cs3org/go-cs3apis/cs3/tx/v1beta1 github.com/cs3org/go-cs3apis/cs3/types/v1beta1 -# github.com/cs3org/reva/v2 v2.19.2-0.20240525160759-56879111e06a +# github.com/cs3org/reva/v2 v2.19.2-0.20240529081036-419196f2342e ## explicit; go 1.21 github.com/cs3org/reva/v2/cmd/revad/internal/grace github.com/cs3org/reva/v2/cmd/revad/runtime @@ -569,6 +569,7 @@ github.com/cs3org/reva/v2/pkg/ocm/share/repository/loader github.com/cs3org/reva/v2/pkg/ocm/share/repository/nextcloud github.com/cs3org/reva/v2/pkg/ocm/share/repository/registry github.com/cs3org/reva/v2/pkg/ocm/storage/received +github.com/cs3org/reva/v2/pkg/owncloud/ocs github.com/cs3org/reva/v2/pkg/password github.com/cs3org/reva/v2/pkg/permission github.com/cs3org/reva/v2/pkg/permission/manager/demo @@ -1790,6 +1791,9 @@ github.com/tidwall/match # github.com/tidwall/pretty v1.2.1 ## explicit; go 1.16 github.com/tidwall/pretty +# github.com/tidwall/sjson v1.2.5 +## explicit; go 1.14 +github.com/tidwall/sjson # github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 ## explicit github.com/toorop/go-dkim