Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
@@ -0,0 +1,114 @@
package response
import (
"encoding/xml"
"net/http"
"reflect"
"github.com/go-chi/render"
"github.com/qsfera/server/services/ocs/pkg/service/v0/data"
)
// Response is the top level response structure
type Response struct {
OCS *Payload `json:"ocs" xml:"ocs"`
}
var (
elementStartElement = xml.StartElement{Name: xml.Name{Local: "element"}}
metaStartElement = xml.StartElement{Name: xml.Name{Local: "meta"}}
ocsName = xml.Name{Local: "ocs"}
dataName = xml.Name{Local: "data"}
)
// Payload combines response metadata and data
type Payload struct {
Meta data.Meta `json:"meta" xml:"meta"`
Data any `json:"data,omitempty" xml:"data,omitempty"`
}
// MarshalXML handles ocs specific wrapping of array members in 'element' tags for the data
func (rsp Response) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
// first the easy part
// use ocs as the surrounding tag
start.Name = ocsName
if err = e.EncodeToken(start); err != nil {
return
}
// encode the meta tag
if err = e.EncodeElement(rsp.OCS.Meta, metaStartElement); err != nil {
return
}
// we need to use reflection to determine if p.Data is an array or a slice
rt := reflect.TypeOf(rsp.OCS.Data)
if rt != nil && (rt.Kind() == reflect.Array || rt.Kind() == reflect.Slice) {
// this is how to wrap the data elements in their own <element> tag
v := reflect.ValueOf(rsp.OCS.Data)
if err = e.EncodeToken(xml.StartElement{Name: dataName}); err != nil {
return
}
for i := 0; i < v.Len(); i++ {
if err = e.EncodeElement(v.Index(i).Interface(), elementStartElement); err != nil {
return
}
}
if err = e.EncodeToken(xml.EndElement{Name: dataName}); err != nil {
return
}
} else if err = e.EncodeElement(rsp.OCS.Data, xml.StartElement{Name: dataName}); err != nil {
return
}
// write the closing <ocs> tag
if err = e.EncodeToken(xml.EndElement{Name: start.Name}); err != nil {
return
}
return
}
// Render sets the status code of the http response, taking the ocs version into account
func (rsp *Response) Render(w http.ResponseWriter, r *http.Request) error {
version := APIVersion(r.Context())
m := statusCodeMapper(version)
statusCode := m(rsp.OCS.Meta)
render.Status(r, statusCode)
if version == ocsVersion2 && statusCode == http.StatusOK {
rsp.OCS.Meta.StatusCode = statusCode
}
return nil
}
// DataRender creates an OK Payload for the given data
func DataRender(d any) render.Renderer {
return &Response{
&Payload{
Meta: data.MetaOK,
Data: d,
},
}
}
// ErrRender creates an Error Payload with the given OCS error code and message
// The httpcode will be determined using the API version stored in the context
func ErrRender(c int, m string) render.Renderer {
return &Response{
&Payload{
Meta: data.Meta{Status: "error", StatusCode: c, Message: m},
},
}
}
func statusCodeMapper(version string) func(data.Meta) int {
var mapper func(data.Meta) int
switch version {
case ocsVersion1:
mapper = OcsV1StatusCodes
case ocsVersion2:
mapper = OcsV2StatusCodes
default:
mapper = defaultStatusCodeMapper
}
return mapper
}
@@ -0,0 +1,92 @@
package response
import (
"context"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/qsfera/server/services/ocs/pkg/service/v0/data"
)
type key int
const (
apiVersionKey key = iota
ocsVersion1 = "1"
ocsVersion2 = "2"
)
var (
defaultStatusCodeMapper = OcsV2StatusCodes
)
// APIVersion retrieves the api version from the context.
func APIVersion(ctx context.Context) string {
value := ctx.Value(apiVersionKey)
if value != nil {
return value.(string)
}
return ""
}
// OcsV1StatusCodes returns the http status codes for the OCS API v1.
func OcsV1StatusCodes(meta data.Meta) int {
if meta.StatusCode == data.MetaUnauthorized.StatusCode {
return http.StatusUnauthorized
}
return http.StatusOK
}
// OcsV2StatusCodes maps the OCS codes to http status codes for the ocs API v2.
// see https://github.com/owncloud/core/blob/c08baf580927ecb8ec179028dda255fdd85b4568/lib/private/legacy/api.php#L528
// also HTTP status codes for apps are the same as OCS codes
// see https://github.com/owncloud/core/blob/b9ff4c93e051c94adfb301545098ae627e52ef76/lib/public/AppFramework/OCSController.php#L142-L150
// I think this is a bug in the ocs v2 api, but since we are going to mimic bugs in КуСфера ... here goes
func OcsV2StatusCodes(meta data.Meta) int {
sc := meta.StatusCode
switch sc {
case data.MetaNotFound.StatusCode:
return http.StatusNotFound
case data.MetaUnknownError.StatusCode:
fallthrough
case data.MetaServerError.StatusCode:
return http.StatusInternalServerError
case data.MetaUnauthorized.StatusCode:
return http.StatusUnauthorized
case data.MetaOK.StatusCode:
// TODO mustn't data.Meta be a pointer so this assignment has an effect
meta.StatusCode = http.StatusOK
return http.StatusOK
}
// any 2xx, 4xx and 5xx will be used as is
if sc >= 200 && sc < 600 {
return sc
}
// any error codes > 100 are treated as client errors
if sc > 100 && sc < 200 {
return http.StatusBadRequest
}
// TODO change this status code? yes, align with oc10 core mapStatusCodes
return http.StatusOK
}
// VersionCtx middleware is used to determine the response mapper from
// the URL parameters passed through as the request. In case
// the Version is unknown, we stop here and return a 404.
func VersionCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
version := chi.URLParam(r, "version")
if version == "" {
_ = render.Render(w, r, ErrRender(data.MetaBadRequest.StatusCode, "unknown ocs api version"))
return
}
w.Header().Set("Ocs-Api-Version", version)
// store version in context so handlers can access it
ctx := context.WithValue(r.Context(), apiVersionKey, version)
next.ServeHTTP(w, r.WithContext(ctx))
})
}