github.com/cs3org/reva/v2@v2.27.7/internal/http/services/owncloud/ocs/response/response.go (about)

     1  // Copyright 2018-2021 CERN
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  //
    15  // In applying this license, CERN does not waive the privileges and immunities
    16  // granted to it by virtue of its status as an Intergovernmental Organization
    17  // or submit itself to any jurisdiction.
    18  
    19  package response
    20  
    21  import (
    22  	"bytes"
    23  	"context"
    24  	"encoding/json"
    25  	"encoding/xml"
    26  	"net/http"
    27  	"reflect"
    28  
    29  	"github.com/cs3org/reva/v2/pkg/appctx"
    30  	"github.com/go-chi/chi/v5"
    31  )
    32  
    33  type key int
    34  
    35  const (
    36  	apiVersionKey key = 1
    37  )
    38  
    39  var (
    40  	defaultStatusCodeMapper = OcsV2StatusCodes
    41  )
    42  
    43  // Response is the top level response structure
    44  type Response struct {
    45  	OCS *Payload `json:"ocs"`
    46  }
    47  
    48  // Payload combines response metadata and data
    49  type Payload struct {
    50  	XMLName struct{}    `json:"-" xml:"ocs"`
    51  	Meta    Meta        `json:"meta" xml:"meta"`
    52  	Data    interface{} `json:"data,omitempty" xml:"data,omitempty"`
    53  }
    54  
    55  var (
    56  	elementStartElement = xml.StartElement{Name: xml.Name{Local: "element"}}
    57  	metaStartElement    = xml.StartElement{Name: xml.Name{Local: "meta"}}
    58  	ocsName             = xml.Name{Local: "ocs"}
    59  	dataName            = xml.Name{Local: "data"}
    60  )
    61  
    62  // MarshalXML handles ocs specific wrapping of array members in 'element' tags for the data
    63  func (p Payload) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
    64  	// first the easy part
    65  	// use ocs as the surrounding tag
    66  	start.Name = ocsName
    67  	if err = e.EncodeToken(start); err != nil {
    68  		return
    69  	}
    70  
    71  	// encode the meta tag
    72  	if err = e.EncodeElement(p.Meta, metaStartElement); err != nil {
    73  		return
    74  	}
    75  
    76  	// we need to use reflection to determine if p.Data is an array or a slice
    77  	rt := reflect.TypeOf(p.Data)
    78  	if rt != nil && (rt.Kind() == reflect.Array || rt.Kind() == reflect.Slice) {
    79  		// this is how to wrap the data elements in their own <element> tag
    80  		v := reflect.ValueOf(p.Data)
    81  		if err = e.EncodeToken(xml.StartElement{Name: dataName}); err != nil {
    82  			return
    83  		}
    84  		for i := 0; i < v.Len(); i++ {
    85  			if err = e.EncodeElement(v.Index(i).Interface(), elementStartElement); err != nil {
    86  				return
    87  			}
    88  		}
    89  		if err = e.EncodeToken(xml.EndElement{Name: dataName}); err != nil {
    90  			return
    91  		}
    92  	} else if err = e.EncodeElement(p.Data, xml.StartElement{Name: dataName}); err != nil {
    93  		return
    94  	}
    95  
    96  	// write the closing <ocs> tag
    97  	if err = e.EncodeToken(xml.EndElement{Name: start.Name}); err != nil {
    98  		return
    99  	}
   100  	return
   101  }
   102  
   103  // Meta holds response metadata
   104  type Meta struct {
   105  	Status       string `json:"status" xml:"status"`
   106  	StatusCode   int    `json:"statuscode" xml:"statuscode"`
   107  	Message      string `json:"message" xml:"message"`
   108  	TotalItems   string `json:"totalitems,omitempty" xml:"totalitems,omitempty"`
   109  	ItemsPerPage string `json:"itemsperpage,omitempty" xml:"itemsperpage,omitempty"`
   110  }
   111  
   112  // MetaOK is the default ok response
   113  var MetaOK = Meta{Status: "ok", StatusCode: 100, Message: "OK"}
   114  
   115  // MetaFailure is a failure response with code 101
   116  var MetaFailure = Meta{Status: "", StatusCode: 101, Message: "Failure"}
   117  
   118  // MetaInvalidInput is an error response with code 102
   119  var MetaInvalidInput = Meta{Status: "", StatusCode: 102, Message: "Invalid Input"}
   120  
   121  // MetaForbidden is an error response with code 104
   122  var MetaForbidden = Meta{Status: "", StatusCode: 104, Message: "Forbidden"}
   123  
   124  // MetaBadRequest is used for unknown errors
   125  var MetaBadRequest = Meta{Status: "error", StatusCode: 400, Message: "Bad Request"}
   126  
   127  // MetaPathNotFound is returned when trying to share not existing resources
   128  var MetaPathNotFound = Meta{Status: "failure", StatusCode: 404, Message: MessagePathNotFound}
   129  
   130  // MetaLocked is returned when trying to share not existing resources
   131  var MetaLocked = Meta{Status: "failure", StatusCode: 423, Message: "The file is locked"}
   132  
   133  // MetaServerError is returned on server errors
   134  var MetaServerError = Meta{Status: "error", StatusCode: 996, Message: "Server Error"}
   135  
   136  // MetaUnauthorized is returned on unauthorized requests
   137  var MetaUnauthorized = Meta{Status: "error", StatusCode: 997, Message: "Unauthorised"}
   138  
   139  // MetaNotFound is returned when trying to access not existing resources
   140  var MetaNotFound = Meta{Status: "error", StatusCode: 998, Message: "Not Found"}
   141  
   142  // MetaUnknownError is used for unknown errors
   143  var MetaUnknownError = Meta{Status: "error", StatusCode: 999, Message: "Unknown Error"}
   144  
   145  // MessageUserNotFound is  used when a user can not be found
   146  var MessageUserNotFound = "The requested user could not be found"
   147  
   148  // MessageGroupNotFound is used when a group can not be found
   149  var MessageGroupNotFound = "The requested group could not be found"
   150  
   151  // MessagePathNotFound is used when a file or folder can not be found
   152  var MessagePathNotFound = "Wrong path, file/folder doesn't exist"
   153  
   154  // MessageShareExists is used when a user tries to create a new share for the same user
   155  var MessageShareExists = "A share for the recipient already exists"
   156  
   157  // MessageLockedForSharing is used when a user tries to create a new share until the file is in use by at least one user
   158  var MessageLockedForSharing = "The file is locked until the file is in use by at least one user"
   159  
   160  // WriteOCSSuccess handles writing successful ocs response data
   161  func WriteOCSSuccess(w http.ResponseWriter, r *http.Request, d interface{}) {
   162  	WriteOCSData(w, r, MetaOK, d, nil)
   163  }
   164  
   165  // WriteOCSError handles writing error ocs responses
   166  func WriteOCSError(w http.ResponseWriter, r *http.Request, c int, m string, err error) {
   167  	WriteOCSData(w, r, Meta{Status: "error", StatusCode: c, Message: m}, nil, err)
   168  }
   169  
   170  // WriteOCSData handles writing ocs data in json and xml
   171  func WriteOCSData(w http.ResponseWriter, r *http.Request, m Meta, d interface{}, err error) {
   172  	WriteOCSResponse(w, r, Response{
   173  		OCS: &Payload{
   174  			Meta: m,
   175  			Data: d,
   176  		},
   177  	}, err)
   178  }
   179  
   180  // WriteOCSResponse handles writing ocs responses in json and xml
   181  func WriteOCSResponse(w http.ResponseWriter, r *http.Request, res Response, err error) {
   182  	if err != nil {
   183  		appctx.GetLogger(r.Context()).
   184  			Debug().
   185  			Err(err).
   186  			Str("ocs_msg", res.OCS.Meta.Message).
   187  			Msg("writing ocs error response")
   188  	}
   189  
   190  	version := APIVersion(r.Context())
   191  	m := statusCodeMapper(version)
   192  	statusCode := m(res.OCS.Meta)
   193  	if version == "2" && statusCode == http.StatusOK {
   194  		res.OCS.Meta.StatusCode = statusCode
   195  	}
   196  
   197  	var encoder func(Response) ([]byte, error)
   198  	if r.URL.Query().Get("format") == "json" {
   199  		w.Header().Set("Content-Type", "application/json; charset=utf-8")
   200  		encoder = encodeJSON
   201  	} else {
   202  		w.Header().Set("Content-Type", "text/xml; charset=utf-8")
   203  		encoder = encodeXML
   204  	}
   205  	w.WriteHeader(statusCode)
   206  	encoded, err := encoder(res)
   207  	if err != nil {
   208  		appctx.GetLogger(r.Context()).Error().Err(err).Msg("error encoding ocs response")
   209  		w.WriteHeader(http.StatusInternalServerError)
   210  		return
   211  	}
   212  
   213  	_, err = w.Write(encoded)
   214  	if err != nil {
   215  		appctx.GetLogger(r.Context()).Error().Err(err).Msg("error writing ocs response")
   216  		w.WriteHeader(http.StatusInternalServerError)
   217  	}
   218  }
   219  
   220  func encodeXML(res Response) ([]byte, error) {
   221  	marshalled, err := xml.Marshal(res.OCS)
   222  	if err != nil {
   223  		return nil, err
   224  	}
   225  	b := new(bytes.Buffer)
   226  	b.WriteString(xml.Header)
   227  	b.Write(marshalled)
   228  	return b.Bytes(), nil
   229  }
   230  
   231  func encodeJSON(res Response) ([]byte, error) {
   232  	return json.Marshal(res)
   233  }
   234  
   235  // OcsV1StatusCodes returns the http status codes for the OCS API v1.
   236  func OcsV1StatusCodes(meta Meta) int {
   237  	return http.StatusOK
   238  }
   239  
   240  // OcsV2StatusCodes maps the OCS codes to http status codes for the ocs API v2.
   241  func OcsV2StatusCodes(meta Meta) int {
   242  	sc := meta.StatusCode
   243  	switch sc {
   244  	case MetaNotFound.StatusCode:
   245  		return http.StatusNotFound
   246  	case MetaUnknownError.StatusCode:
   247  		fallthrough
   248  	case MetaServerError.StatusCode:
   249  		return http.StatusInternalServerError
   250  	case MetaUnauthorized.StatusCode:
   251  		return http.StatusUnauthorized
   252  	case MetaOK.StatusCode:
   253  		meta.StatusCode = http.StatusOK
   254  		return http.StatusOK
   255  	case MetaForbidden.StatusCode:
   256  		return http.StatusForbidden
   257  	}
   258  	// any 2xx, 4xx and 5xx will be used as is
   259  	if sc >= 200 && sc < 600 {
   260  		return sc
   261  	}
   262  
   263  	// any error codes > 100 are treated as client errors
   264  	if sc > 100 && sc < 200 {
   265  		return http.StatusBadRequest
   266  	}
   267  
   268  	// TODO change this status code?
   269  	return http.StatusOK
   270  }
   271  
   272  // WithAPIVersion puts the api version in the context.
   273  func VersionCtx(next http.Handler) http.Handler {
   274  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   275  		version := chi.URLParam(r, "version")
   276  		if version == "" {
   277  			WriteOCSError(w, r, MetaBadRequest.StatusCode, "unknown ocs api version", nil)
   278  			return
   279  		}
   280  		w.Header().Set("Ocs-Api-Version", version)
   281  
   282  		// store version in context so handlers can access it
   283  		ctx := context.WithValue(r.Context(), apiVersionKey, version)
   284  		next.ServeHTTP(w, r.WithContext(ctx))
   285  	})
   286  }
   287  
   288  // APIVersion retrieves the api version from the context.
   289  func APIVersion(ctx context.Context) string {
   290  	value := ctx.Value(apiVersionKey)
   291  	if value != nil {
   292  		return value.(string)
   293  	}
   294  	return ""
   295  }
   296  
   297  func statusCodeMapper(version string) func(Meta) int {
   298  	var mapper func(Meta) int
   299  	switch version {
   300  	case "1":
   301  		mapper = OcsV1StatusCodes
   302  	case "2":
   303  		mapper = OcsV2StatusCodes
   304  	default:
   305  		mapper = defaultStatusCodeMapper
   306  	}
   307  	return mapper
   308  }