mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-05-07 20:15:31 -05:00
6814c61506
* 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>
206 lines
5.1 KiB
Go
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
|
|
}
|