1
0
mirror of https://github.com/owncloud/ocis.git synced 2026-10-25 11:39:24 +03:00
Files
ocis/services/web/pkg/theme/theme.go
Lukas Hirt 9568b738b5 fix(web): [OCISDEV-1243] admin-uploaded logo overrides theme-specific logo (#12847)
A theme can define its own logo per UI variant under
clients.web.themes[].logo, including the built-in "owncloud" theme, most
of whose variants (dark, high contrast, ...) already do this. The admin
logo upload endpoint only ever patched clients.web.defaults.logo, so the
active theme's own per-variant logo kept winning over a logo an admin
explicitly uploaded through the admin settings web app.

Force the branding (admin-uploaded) logo onto every theme variant's own
logo entries after the theme merge, so it always takes precedence.

Signed-off-by: Lukas Hirt <info@hirt.cz>
2026-08-26 06:39:29 +00:00

174 lines
5.6 KiB
Go

package theme
import (
"maps"
"path"
"github.com/owncloud/ocis/v2/ocis-pkg/capabilities"
"github.com/owncloud/ocis/v2/services/graph/pkg/unifiedrole"
)
var (
_brandingRoot = "_branding"
_themeFileName = "theme.json"
// _brandingLogoKeys are the clients.web.defaults.logo sub-keys the admin
// logo upload endpoint controls. LogoUpload, LogoReset, and
// applyLogoOverride all derive their keys from this single list so they
// can't drift apart — anything added here automatically also overrides a
// theme's own per-variant logo.
_brandingLogoKeys = []string{"topbar", "login"}
)
// 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{
"label": unifiedrole.GetUnifiedRoleLabel(unifiedrole.UnifiedRoleViewerID),
"iconName": "eye",
},
unifiedrole.UnifiedRoleViewerListGrantsID: KV{
"label": unifiedrole.GetUnifiedRoleLabel(unifiedrole.UnifiedRoleViewerListGrantsID),
"iconName": "eye",
},
unifiedrole.UnifiedRoleSpaceViewerID: KV{
"label": unifiedrole.GetUnifiedRoleLabel(unifiedrole.UnifiedRoleSpaceViewerID),
"iconName": "eye",
},
unifiedrole.UnifiedRoleFileEditorID: KV{
"label": unifiedrole.GetUnifiedRoleLabel(unifiedrole.UnifiedRoleFileEditorID),
"iconName": "pencil",
},
unifiedrole.UnifiedRoleFileEditorListGrantsID: KV{
"label": unifiedrole.GetUnifiedRoleLabel(unifiedrole.UnifiedRoleFileEditorListGrantsID),
"iconName": "pencil",
},
unifiedrole.UnifiedRoleFileEditorListGrantsWithVersionsID: KV{
"label": unifiedrole.GetUnifiedRoleLabel(unifiedrole.UnifiedRoleFileEditorListGrantsWithVersionsID),
"iconName": "pencil",
},
unifiedrole.UnifiedRoleEditorID: KV{
"label": unifiedrole.GetUnifiedRoleLabel(unifiedrole.UnifiedRoleEditorID),
"iconName": "pencil",
},
unifiedrole.UnifiedRoleEditorListGrantsID: KV{
"label": unifiedrole.GetUnifiedRoleLabel(unifiedrole.UnifiedRoleEditorListGrantsID),
"iconName": "pencil",
},
unifiedrole.UnifiedRoleEditorListGrantsWithVersionsID: KV{
"label": unifiedrole.GetUnifiedRoleLabel(unifiedrole.UnifiedRoleEditorListGrantsWithVersionsID),
"iconName": "pencil",
},
unifiedrole.UnifiedRoleSpaceEditorID: KV{
"label": unifiedrole.GetUnifiedRoleLabel(unifiedrole.UnifiedRoleSpaceEditorID),
"iconName": "pencil",
},
unifiedrole.UnifiedRoleSpaceEditorWithoutVersionsID: KV{
"label": unifiedrole.GetUnifiedRoleLabel(unifiedrole.UnifiedRoleSpaceEditorWithoutVersionsID),
"iconName": "pencil",
},
unifiedrole.UnifiedRoleSpaceEditorWithoutTrashbinID: KV{
"label": unifiedrole.GetUnifiedRoleLabel(unifiedrole.UnifiedRoleSpaceEditorWithoutTrashbinID),
"iconName": "pencil",
},
unifiedrole.UnifiedRoleManagerID: KV{
"label": unifiedrole.GetUnifiedRoleLabel(unifiedrole.UnifiedRoleManagerID),
"iconName": "user-star",
},
unifiedrole.UnifiedRoleEditorLiteID: KV{
"label": unifiedrole.GetUnifiedRoleLabel(unifiedrole.UnifiedRoleEditorLiteID),
"iconName": "upload",
},
unifiedrole.UnifiedRoleSecureViewerID: KV{
"label": unifiedrole.GetUnifiedRoleLabel(unifiedrole.UnifiedRoleSecureViewerID),
"iconName": "shield",
},
unifiedrole.UnifiedRoleDeniedID: KV{
"label": unifiedrole.GetUnifiedRoleLabel(unifiedrole.UnifiedRoleDeniedID),
"iconName": "stop-circle",
},
},
},
}
// 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
}
// asMap returns v as a map[string]any, regardless of whether its concrete
// type is KV or map[string]any (both occur depending on whether a KV tree
// was built in code or decoded from JSON).
func asMap(v any) (map[string]any, bool) {
switch t := v.(type) {
case map[string]any:
return t, true
case KV:
return map[string]any(t), true
default:
return nil, false
}
}
// getPath looks up a dotted path of keys in a nested KV/map tree.
func getPath(v any, path ...string) (any, bool) {
cur := v
for _, p := range path {
m, ok := asMap(cur)
if !ok {
return nil, false
}
cur, ok = m[p]
if !ok {
return nil, false
}
}
return cur, true
}
// applyLogoOverride forces any admin-uploaded logo (branding) onto every
// theme variant's own logo entries. Without this, a theme that defines
// clients.web.themes[].logo itself would keep showing its own logo instead
// of the one the admin explicitly uploaded, since that per-variant value
// lives outside the clients.web.defaults.logo keys the upload patches.
//
// This mutates merged in place, which is only safe because clients.* is
// per-request data coming from baseTheme/brandingTheme, never from the
// shared, package-level themeDefaults tree that MergeKV also aliases in.
func applyLogoOverride(merged KV, branding KV) {
overrides := map[string]any{}
for _, key := range _brandingLogoKeys {
if val, ok := getPath(branding, "clients", "web", "defaults", "logo", key); ok {
overrides[key] = val
}
}
if len(overrides) == 0 {
return
}
themes, ok := getPath(merged, "clients", "web", "themes")
if !ok {
return
}
variants, ok := themes.([]any)
if !ok {
return
}
for _, item := range variants {
variant, ok := asMap(item)
if !ok {
continue
}
logo, ok := asMap(variant["logo"])
if !ok {
continue
}
maps.Copy(logo, overrides)
}
}