Files
opencloud/services/web/pkg/service/v0/branding.go
T
Florian Schade 6814c61506 [full-ci] enhancement: allow ocis to provide custom web applications (#8523)
* enhancement: allow ocis to provide custom web applications

* enhancement: add an option to disable web apps

* test: add default logger tests

* test: add app loading tests

* test: add asset server tests

* enhancement: make use of dedicated app conf file and app asset paths

* enhancement: adjust asset locations and deprecate WEB_ASSET_PATH

* enhancement: get rid of default logger and use the service level logger instead

* Apply suggestions from code review

Co-authored-by: Benedikt Kulmann <benedikt@kulmann.biz>
Co-authored-by: kobergj <juliankoberg@googlemail.com>

* enhancement: use basename as app id

* Apply suggestions from code review

Co-authored-by: Martin <github@diemattels.at>

* enhancement: use afero as fs abstraction

* enhancement: simplify logo upload

* enhancement: make use of introductionVersion field annotations

---------

Co-authored-by: Benedikt Kulmann <benedikt@kulmann.biz>
Co-authored-by: kobergj <juliankoberg@googlemail.com>
Co-authored-by: Martin <github@diemattels.at>
2024-03-05 14:11:18 +01:00

206 lines
5.1 KiB
Go

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
}