github.com/cs3org/reva/v2@v2.27.7/internal/http/services/owncloud/ocdav/locks.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 ocdav
    20  
    21  import (
    22  	"context"
    23  	"encoding/xml"
    24  	"errors"
    25  	"fmt"
    26  	"io"
    27  	"net/http"
    28  	"path"
    29  	"strconv"
    30  	"strings"
    31  	"time"
    32  
    33  	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
    34  	userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
    35  	rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
    36  	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
    37  	types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
    38  	ocdavErrors "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/errors"
    39  	"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/net"
    40  	"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/prop"
    41  	"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/spacelookup"
    42  	"github.com/cs3org/reva/v2/pkg/appctx"
    43  	ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
    44  	"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
    45  	"github.com/cs3org/reva/v2/pkg/utils"
    46  	"github.com/google/uuid"
    47  	"go.opentelemetry.io/otel/attribute"
    48  )
    49  
    50  // Most of this is taken from https://github.com/golang/net/blob/master/webdav/lock.go
    51  
    52  // From RFC4918 http://www.webdav.org/specs/rfc4918.html#lock-tokens
    53  // This specification encourages servers to create Universally Unique Identifiers (UUIDs) for lock tokens,
    54  // and to use the URI form defined by "A Universally Unique Identifier (UUID) URN Namespace" ([RFC4122]).
    55  // However, servers are free to use any URI (e.g., from another scheme) so long as it meets the uniqueness
    56  // requirements. For example, a valid lock token might be constructed using the "opaquelocktoken" scheme
    57  // defined in Appendix C.
    58  //
    59  // Example: "urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6"
    60  //
    61  // we stick to the recommendation and use the URN Namespace
    62  const lockTokenPrefix = "urn:uuid:"
    63  
    64  // TODO(jfd) implement lock
    65  // see Web Distributed Authoring and Versioning (WebDAV) Locking Protocol:
    66  // https://www.greenbytes.de/tech/webdav/draft-reschke-webdav-locking-latest.html
    67  // Webdav supports a Depth: infinity lock, wopi only needs locks on files
    68  
    69  // https://www.greenbytes.de/tech/webdav/draft-reschke-webdav-locking-latest.html#write.locks.and.the.if.request.header
    70  // [...] a lock token MUST be submitted in the If header for all locked resources
    71  // that a method may interact with or the method MUST fail. [...]
    72  /*
    73  	COPY /~fielding/index.html HTTP/1.1
    74  	Host: example.com
    75  	Destination: http://example.com/users/f/fielding/index.html
    76  	If: <http://example.com/users/f/fielding/index.html>
    77  		(<opaquelocktoken:f81d4fae-7dec-11d0-a765-00a0c91e6bf6>)
    78  */
    79  
    80  // http://www.webdav.org/specs/rfc4918.html#ELEMENT_lockinfo
    81  type lockInfo struct {
    82  	XMLName   xml.Name  `xml:"lockinfo"`
    83  	Exclusive *struct{} `xml:"lockscope>exclusive"`
    84  	Shared    *struct{} `xml:"lockscope>shared"`
    85  	Write     *struct{} `xml:"locktype>write"`
    86  	Owner     owner     `xml:"owner"`
    87  	LockID    string    `xml:"locktoken>href"`
    88  }
    89  
    90  // http://www.webdav.org/specs/rfc4918.html#ELEMENT_owner
    91  type owner struct {
    92  	InnerXML string `xml:",innerxml"`
    93  }
    94  
    95  // Condition can match a WebDAV resource, based on a token or ETag.
    96  // Exactly one of Token and ETag should be non-empty.
    97  type Condition struct {
    98  	Not   bool
    99  	Token string
   100  	ETag  string
   101  }
   102  
   103  // LockSystem manages access to a collection of named resources. The elements
   104  // in a lock name are separated by slash ('/', U+002F) characters, regardless
   105  // of host operating system convention.
   106  type LockSystem interface {
   107  	// Confirm confirms that the caller can claim all of the locks specified by
   108  	// the given conditions, and that holding the union of all of those locks
   109  	// gives exclusive access to all of the named resources. Up to two resources
   110  	// can be named. Empty names are ignored.
   111  	//
   112  	// Exactly one of release and err will be non-nil. If release is non-nil,
   113  	// all of the requested locks are held until release is called. Calling
   114  	// release does not unlock the lock, in the WebDAV UNLOCK sense, but once
   115  	// Confirm has confirmed that a lock claim is valid, that lock cannot be
   116  	// Confirmed again until it has been released.
   117  	//
   118  	// If Confirm returns ErrConfirmationFailed then the Handler will continue
   119  	// to try any other set of locks presented (a WebDAV HTTP request can
   120  	// present more than one set of locks). If it returns any other non-nil
   121  	// error, the Handler will write a "500 Internal Server Error" HTTP status.
   122  	Confirm(ctx context.Context, now time.Time, name0, name1 string, conditions ...Condition) (release func(), err error)
   123  
   124  	// Create creates a lock with the given depth, duration, owner and root
   125  	// (name). The depth will either be negative (meaning infinite) or zero.
   126  	//
   127  	// If Create returns ErrLocked then the Handler will write a "423 Locked"
   128  	// HTTP status. If it returns any other non-nil error, the Handler will
   129  	// write a "500 Internal Server Error" HTTP status.
   130  	//
   131  	// See http://www.webdav.org/specs/rfc4918.html#rfc.section.9.10.6 for
   132  	// when to use each error.
   133  	//
   134  	// The token returned identifies the created lock. It should be an absolute
   135  	// URI as defined by RFC 3986, Section 4.3. In particular, it should not
   136  	// contain whitespace.
   137  	Create(ctx context.Context, now time.Time, details LockDetails) (token string, err error)
   138  
   139  	// Refresh refreshes the lock with the given token.
   140  	//
   141  	// If Refresh returns ErrLocked then the Handler will write a "423 Locked"
   142  	// HTTP Status. If Refresh returns ErrNoSuchLock then the Handler will write
   143  	// a "412 Precondition Failed" HTTP Status. If it returns any other non-nil
   144  	// error, the Handler will write a "500 Internal Server Error" HTTP status.
   145  	//
   146  	// See http://www.webdav.org/specs/rfc4918.html#rfc.section.9.10.6 for
   147  	// when to use each error.
   148  	Refresh(ctx context.Context, now time.Time, ref *provider.Reference, token string) error
   149  
   150  	// Unlock unlocks the lock with the given token.
   151  	//
   152  	// If Unlock returns ErrForbidden then the Handler will write a "403
   153  	// Forbidden" HTTP Status. If Unlock returns ErrLocked then the Handler
   154  	// will write a "423 Locked" HTTP status. If Unlock returns ErrNoSuchLock
   155  	// then the Handler will write a "409 Conflict" HTTP Status. If it returns
   156  	// any other non-nil error, the Handler will write a "500 Internal Server
   157  	// Error" HTTP status.
   158  	//
   159  	// See http://www.webdav.org/specs/rfc4918.html#rfc.section.9.11.1 for
   160  	// when to use each error.
   161  	Unlock(ctx context.Context, now time.Time, ref *provider.Reference, token string) error
   162  }
   163  
   164  // NewCS3LS returns a new CS3 based LockSystem.
   165  func NewCS3LS(s pool.Selectable[gateway.GatewayAPIClient]) LockSystem {
   166  	return &cs3LS{
   167  		selector: s,
   168  	}
   169  }
   170  
   171  type cs3LS struct {
   172  	selector pool.Selectable[gateway.GatewayAPIClient]
   173  }
   174  
   175  func (cls *cs3LS) Confirm(ctx context.Context, now time.Time, name0, name1 string, conditions ...Condition) (func(), error) {
   176  	return nil, ocdavErrors.ErrNotImplemented
   177  }
   178  
   179  func (cls *cs3LS) Create(ctx context.Context, now time.Time, details LockDetails) (string, error) {
   180  	// always assume depth infinity?
   181  	/*
   182  		if !details.ZeroDepth {
   183  		 The CS3 Lock api currently has no depth property, it only locks single resources
   184  			return "", ocdavErrors.ErrUnsupportedLockInfo
   185  		}
   186  	*/
   187  
   188  	u := ctxpkg.ContextMustGetUser(ctx)
   189  
   190  	// add metadata via opaque
   191  	// TODO: upate cs3api: https://github.com/cs3org/cs3apis/issues/213
   192  	o := utils.AppendPlainToOpaque(nil, "lockownername", u.GetDisplayName())
   193  	o = utils.AppendPlainToOpaque(o, "locktime", now.Format(time.RFC3339))
   194  
   195  	lockid := details.LockID
   196  	if lockid == "" {
   197  		// Having a lock token provides no special access rights. Anyone can find out anyone
   198  		// else's lock token by performing lock discovery. Locks must be enforced based upon
   199  		// whatever authentication mechanism is used by the server, not based on the secrecy
   200  		// of the token values.
   201  		// see: http://www.webdav.org/specs/rfc2518.html#n-lock-tokens
   202  		token := uuid.New()
   203  
   204  		lockid = lockTokenPrefix + token.String()
   205  	}
   206  	r := &provider.SetLockRequest{
   207  		Ref: details.Root,
   208  		Lock: &provider.Lock{
   209  			Opaque: o,
   210  			Type:   provider.LockType_LOCK_TYPE_EXCL,
   211  			User:   details.UserID, // no way to set an app lock? TODO maybe via the ownerxml
   212  			//AppName: , // TODO use a urn scheme?
   213  			LockId: lockid,
   214  		},
   215  	}
   216  	if details.Duration > 0 {
   217  		expiration := time.Now().UTC().Add(details.Duration)
   218  		r.Lock.Expiration = &types.Timestamp{
   219  			Seconds: uint64(expiration.Unix()),
   220  			Nanos:   uint32(expiration.Nanosecond()),
   221  		}
   222  	}
   223  
   224  	client, err := cls.selector.Next()
   225  	if err != nil {
   226  		return "", err
   227  	}
   228  
   229  	res, err := client.SetLock(ctx, r)
   230  	if err != nil {
   231  		return "", err
   232  	}
   233  	switch res.GetStatus().GetCode() {
   234  	case rpc.Code_CODE_OK:
   235  		return lockid, nil
   236  	default:
   237  		return "", ocdavErrors.NewErrFromStatus(res.GetStatus())
   238  	}
   239  
   240  }
   241  
   242  func (cls *cs3LS) Refresh(ctx context.Context, now time.Time, ref *provider.Reference, token string) error {
   243  	u := ctxpkg.ContextMustGetUser(ctx)
   244  
   245  	// add metadata via opaque
   246  	// TODO: upate cs3api: https://github.com/cs3org/cs3apis/issues/213
   247  	o := utils.AppendPlainToOpaque(nil, "lockownername", u.GetDisplayName())
   248  	o = utils.AppendPlainToOpaque(o, "locktime", now.Format(time.RFC3339))
   249  
   250  	if token == "" {
   251  		return errors.New("token is empty")
   252  	}
   253  
   254  	r := &provider.RefreshLockRequest{
   255  		Ref: ref,
   256  		Lock: &provider.Lock{
   257  			Opaque: o,
   258  			Type:   provider.LockType_LOCK_TYPE_EXCL,
   259  			//AppName: , // TODO use a urn scheme?
   260  			LockId: token,
   261  			User:   u.GetId(),
   262  		},
   263  	}
   264  
   265  	client, err := cls.selector.Next()
   266  	if err != nil {
   267  		return err
   268  	}
   269  
   270  	res, err := client.RefreshLock(ctx, r)
   271  	if err != nil {
   272  		return err
   273  	}
   274  	switch res.GetStatus().GetCode() {
   275  	case rpc.Code_CODE_OK:
   276  		return nil
   277  
   278  	default:
   279  		return ocdavErrors.NewErrFromStatus(res.GetStatus())
   280  	}
   281  }
   282  
   283  func (cls *cs3LS) Unlock(ctx context.Context, now time.Time, ref *provider.Reference, token string) error {
   284  	u := ctxpkg.ContextMustGetUser(ctx)
   285  
   286  	r := &provider.UnlockRequest{
   287  		Ref: ref,
   288  		Lock: &provider.Lock{
   289  			LockId: token, // can be a token or a Coded-URL
   290  			User:   u.Id,
   291  		},
   292  	}
   293  
   294  	client, err := cls.selector.Next()
   295  	if err != nil {
   296  		return err
   297  	}
   298  
   299  	res, err := client.Unlock(ctx, r)
   300  	if err != nil {
   301  		return err
   302  	}
   303  
   304  	newErr := ocdavErrors.NewErrFromStatus(res.GetStatus())
   305  	if newErr != nil {
   306  		appctx.GetLogger(ctx).Error().Str("token", token).Interface("unlock", ref).Msg("could not unlock " + res.GetStatus().GetMessage())
   307  	}
   308  	return newErr
   309  }
   310  
   311  // LockDetails are a lock's metadata.
   312  type LockDetails struct {
   313  	// Root is the root resource name being locked. For a zero-depth lock, the
   314  	// root is the only resource being locked.
   315  	Root *provider.Reference
   316  	// Duration is the lock timeout. A negative duration means infinite.
   317  	Duration time.Duration
   318  	// OwnerXML is the verbatim <owner> XML given in a LOCK HTTP request.
   319  	//
   320  	// TODO: does the "verbatim" nature play well with XML namespaces?
   321  	// Does the OwnerXML field need to have more structure? See
   322  	// https://codereview.appspot.com/175140043/#msg2
   323  	OwnerXML string
   324  	UserID   *userpb.UserId
   325  	// ZeroDepth is whether the lock has zero depth. If it does not have zero
   326  	// depth, it has infinite depth.
   327  	ZeroDepth bool
   328  	// OwnerName is the name of the lock owner
   329  	OwnerName string
   330  	// Locktime is the time the lock was created
   331  	Locktime time.Time
   332  	// LockID is the lock token
   333  	LockID string
   334  }
   335  
   336  func readLockInfo(r io.Reader) (li lockInfo, status int, err error) {
   337  	c := &countingReader{r: r}
   338  	if err = xml.NewDecoder(c).Decode(&li); err != nil {
   339  		if err == io.EOF {
   340  			if c.n == 0 {
   341  				// An empty body means to refresh the lock.
   342  				// http://www.webdav.org/specs/rfc4918.html#refreshing-locks
   343  				return lockInfo{}, 0, nil
   344  			}
   345  			err = ocdavErrors.ErrInvalidLockInfo
   346  		}
   347  		return lockInfo{}, http.StatusBadRequest, err
   348  	}
   349  	// We only support exclusive (non-shared) write locks. In practice, these are
   350  	// the only types of locks that seem to matter.
   351  	// We are ignoring the any properties in the lock details, and assume an exclusive write lock is requested.
   352  	// https://datatracker.ietf.org/doc/html/rfc4918#section-7 only describes write locks
   353  	//
   354  	// if li.Exclusive == nil || li.Shared != nil {
   355  	//   return lockInfo{}, http.StatusNotImplemented, errors.ErrUnsupportedLockInfo
   356  	// }
   357  	// what should we return if the user requests a shared lock? or leaves out the locktype? the testsuite will only send the property lockscope, not locktype
   358  	// the oc tests cover both shared and exclusive locks. What is the WOPI lock? a shared or an exclusive lock?
   359  	// since it is issued by a service it seems to be an exclusive lock.
   360  	// the owner could be a link to the collaborative app ... to join the session
   361  	return li, 0, nil
   362  }
   363  
   364  type countingReader struct {
   365  	n int
   366  	r io.Reader
   367  }
   368  
   369  func (c *countingReader) Read(p []byte) (int, error) {
   370  	n, err := c.r.Read(p)
   371  	c.n += n
   372  	return n, err
   373  }
   374  
   375  const infiniteTimeout = -1
   376  
   377  // parseTimeout parses the Timeout HTTP header, as per section 10.7. If s is
   378  // empty, an infiniteTimeout is returned.
   379  func parseTimeout(s string) (time.Duration, error) {
   380  	if s == "" {
   381  		return infiniteTimeout, nil
   382  	}
   383  	if i := strings.IndexByte(s, ','); i >= 0 {
   384  		s = s[:i]
   385  	}
   386  	s = strings.TrimSpace(s)
   387  	if s == "Infinite" {
   388  		return infiniteTimeout, nil
   389  	}
   390  	const pre = "Second-"
   391  	if !strings.HasPrefix(s, pre) {
   392  		return 0, ocdavErrors.ErrInvalidTimeout
   393  	}
   394  	s = s[len(pre):]
   395  	if s == "" || s[0] < '0' || '9' < s[0] {
   396  		return 0, ocdavErrors.ErrInvalidTimeout
   397  	}
   398  	n, err := strconv.ParseInt(s, 10, 64)
   399  	if err != nil || 1<<32-1 < n {
   400  		return 0, ocdavErrors.ErrInvalidTimeout
   401  	}
   402  	return time.Duration(n) * time.Second, nil
   403  }
   404  
   405  const (
   406  	infiniteDepth = -1
   407  	invalidDepth  = -2
   408  )
   409  
   410  // parseDepth maps the strings "0", "1" and "infinity" to 0, 1 and
   411  // infiniteDepth. Parsing any other string returns invalidDepth.
   412  //
   413  // Different WebDAV methods have further constraints on valid depths:
   414  //   - PROPFIND has no further restrictions, as per section 9.1.
   415  //   - COPY accepts only "0" or "infinity", as per section 9.8.3.
   416  //   - MOVE accepts only "infinity", as per section 9.9.2.
   417  //   - LOCK accepts only "0" or "infinity", as per section 9.10.3.
   418  //
   419  // These constraints are enforced by the handleXxx methods.
   420  func parseDepth(s string) int {
   421  	switch s {
   422  	case "0":
   423  		return 0
   424  	case "1":
   425  		return 1
   426  	case "infinity":
   427  		return infiniteDepth
   428  	}
   429  	return invalidDepth
   430  }
   431  
   432  /*
   433  the oc 10 wopi app code locks like this:
   434  
   435  	$storage->lockNodePersistent($file->getInternalPath(), [
   436  		'token' => $wopiLock,
   437  		'owner' => "{$user->getDisplayName()} via Office Online"
   438  	]);
   439  
   440  if owner is empty it defaults to '{displayname} ({email})', which is not a url ... but ... shrug
   441  
   442  The LockManager also defaults to exclusive locks:
   443  
   444  	$scope = ILock::LOCK_SCOPE_EXCLUSIVE;
   445  	if (isset($lockInfo['scope'])) {
   446  		$scope = $lockInfo['scope'];
   447  	}
   448  */
   449  func (s *svc) handleLock(w http.ResponseWriter, r *http.Request, ns string) (retStatus int, retErr error) {
   450  	ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), fmt.Sprintf("%s %v", r.Method, r.URL.Path))
   451  	defer span.End()
   452  
   453  	span.SetAttributes(attribute.String("component", "ocdav"))
   454  
   455  	fn := path.Join(ns, r.URL.Path) // TODO do we still need to jail if we query the registry about the spaces?
   456  
   457  	// TODO instead of using a string namespace ns pass in the space with the request?
   458  	ref, cs3Status, err := spacelookup.LookupReferenceForPath(ctx, s.gatewaySelector, fn)
   459  	if err != nil {
   460  		return http.StatusInternalServerError, err
   461  	}
   462  	if cs3Status.Code != rpc.Code_CODE_OK {
   463  		return http.StatusInternalServerError, ocdavErrors.NewErrFromStatus(cs3Status)
   464  	}
   465  
   466  	return s.lockReference(ctx, w, r, ref)
   467  }
   468  
   469  func (s *svc) handleSpacesLock(w http.ResponseWriter, r *http.Request, spaceID string) (retStatus int, retErr error) {
   470  	ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), fmt.Sprintf("%s %v", r.Method, r.URL.Path))
   471  	defer span.End()
   472  
   473  	span.SetAttributes(attribute.String("component", "ocdav"))
   474  
   475  	ref, err := spacelookup.MakeStorageSpaceReference(spaceID, r.URL.Path)
   476  	if err != nil {
   477  		return http.StatusBadRequest, fmt.Errorf("invalid space id")
   478  	}
   479  
   480  	return s.lockReference(ctx, w, r, &ref)
   481  }
   482  
   483  func (s *svc) lockReference(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference) (retStatus int, retErr error) {
   484  	sublog := appctx.GetLogger(ctx).With().Interface("ref", ref).Logger()
   485  	duration, err := parseTimeout(r.Header.Get(net.HeaderTimeout))
   486  	if err != nil {
   487  		return http.StatusBadRequest, ocdavErrors.ErrInvalidTimeout
   488  	}
   489  
   490  	li, status, err := readLockInfo(r.Body)
   491  	if err != nil {
   492  		return status, ocdavErrors.ErrInvalidLockInfo
   493  	}
   494  
   495  	u := ctxpkg.ContextMustGetUser(ctx)
   496  	token, now, created := "", time.Now(), false
   497  	ld := LockDetails{UserID: u.Id, Root: ref, Duration: duration, OwnerName: u.GetDisplayName(), Locktime: now, LockID: li.LockID}
   498  	if li == (lockInfo{}) {
   499  		// An empty lockInfo means to refresh the lock.
   500  		ih, ok := parseIfHeader(r.Header.Get(net.HeaderIf))
   501  		if !ok {
   502  			return http.StatusBadRequest, ocdavErrors.ErrInvalidIfHeader
   503  		}
   504  		if len(ih.lists) == 1 && len(ih.lists[0].conditions) == 1 {
   505  			token = ih.lists[0].conditions[0].Token
   506  		}
   507  		if token == "" {
   508  			return http.StatusBadRequest, ocdavErrors.ErrInvalidLockToken
   509  		}
   510  		err = s.LockSystem.Refresh(ctx, now, ref, token)
   511  		if err != nil {
   512  			if err == ocdavErrors.ErrNoSuchLock {
   513  				return http.StatusPreconditionFailed, err
   514  			}
   515  			return http.StatusInternalServerError, err
   516  		}
   517  
   518  		ld.LockID = token
   519  
   520  	} else {
   521  		// Section 9.10.3 says that "If no Depth header is submitted on a LOCK request,
   522  		// then the request MUST act as if a "Depth:infinity" had been submitted."
   523  		depth := infiniteDepth
   524  		if hdr := r.Header.Get(net.HeaderDepth); hdr != "" {
   525  			depth = parseDepth(hdr)
   526  			if depth != 0 && depth != infiniteDepth {
   527  				// Section 9.10.3 says that "Values other than 0 or infinity must not be
   528  				// used with the Depth header on a LOCK method".
   529  				return http.StatusBadRequest, ocdavErrors.ErrInvalidDepth
   530  			}
   531  		}
   532  		/* our url path has been shifted, so we don't need to do this?
   533  		reqPath, status, err := h.stripPrefix(r.URL.Path)
   534  		if err != nil {
   535  			return status, err
   536  		}
   537  		*/
   538  		// TODO look up username and email
   539  		//  if li.Owner.InnerXML == "" {
   540  		//    // PHP version: 'owner' => "{$user->getDisplayName()} via Office Online"
   541  		//    ld.OwnerXML = ld.UserID.OpaqueId
   542  		//  }
   543  		ld.OwnerXML = li.Owner.InnerXML // TODO optional, should be a URL
   544  		ld.ZeroDepth = depth == 0
   545  
   546  		//TODO: @jfd the code tries to create a lock for a file that may not even exist,
   547  		//      should we do that in the decomposedfs as well? the node does not exist
   548  		//      this actually is a name based lock ... ugh
   549  		token, err = s.LockSystem.Create(ctx, now, ld)
   550  
   551  		//
   552  		if err != nil {
   553  			switch {
   554  			case errors.Is(err, ocdavErrors.ErrLocked):
   555  				return http.StatusLocked, err
   556  			case errors.Is(err, ocdavErrors.ErrForbidden):
   557  				return http.StatusForbidden, err
   558  			default:
   559  				return http.StatusInternalServerError, err
   560  			}
   561  		}
   562  
   563  		defer func() {
   564  			if retErr != nil {
   565  				if err := s.LockSystem.Unlock(ctx, now, ref, token); err != nil {
   566  					appctx.GetLogger(ctx).Error().Err(err).Interface("lock", ld).Msg("could not unlock after failed lock")
   567  				}
   568  			}
   569  		}()
   570  
   571  		// Create the resource if it didn't previously exist.
   572  		// TODO use sdk to stat?
   573  		/*
   574  			if _, err := s.FileSystem.Stat(ctx, reqPath); err != nil {
   575  				f, err := s.FileSystem.OpenFile(ctx, reqPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
   576  				if err != nil {
   577  					// TODO: detect missing intermediate dirs and return http.StatusConflict?
   578  					return http.StatusInternalServerError, err
   579  				}
   580  				f.Close()
   581  				created = true
   582  			}
   583  		*/
   584  		// http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the
   585  		// Lock-Token value is a Coded-URL. We add angle brackets.
   586  		w.Header().Set("Lock-Token", "<"+token+">")
   587  	}
   588  
   589  	w.Header().Set("Content-Type", "application/xml; charset=utf-8")
   590  	if created {
   591  		// This is "w.WriteHeader(http.StatusCreated)" and not "return
   592  		// http.StatusCreated, nil" because we write our own (XML) response to w
   593  		// and Handler.ServeHTTP would otherwise write "Created".
   594  		w.WriteHeader(http.StatusCreated)
   595  	}
   596  	n, err := writeLockInfo(w, token, ld)
   597  	if err != nil {
   598  		sublog.Err(err).Int("bytes_written", n).Msg("error writing response")
   599  	}
   600  	return 0, nil
   601  }
   602  
   603  func writeLockInfo(w io.Writer, token string, ld LockDetails) (int, error) {
   604  	depth := "infinity"
   605  	if ld.ZeroDepth {
   606  		depth = "0"
   607  	}
   608  	href := ld.Root.Path // FIXME add base url and space?
   609  
   610  	lockdiscovery := strings.Builder{}
   611  	lockdiscovery.WriteString(xml.Header)
   612  	lockdiscovery.WriteString("<d:prop xmlns:d=\"DAV:\" xmlns:oc=\"http://owncloud.org/ns\"><d:lockdiscovery><d:activelock>\n")
   613  	lockdiscovery.WriteString("  <d:locktype><d:write/></d:locktype>\n")
   614  	lockdiscovery.WriteString("  <d:lockscope><d:exclusive/></d:lockscope>\n")
   615  	lockdiscovery.WriteString(fmt.Sprintf("  <d:depth>%s</d:depth>\n", depth))
   616  	if ld.OwnerXML != "" {
   617  		lockdiscovery.WriteString(fmt.Sprintf("  <d:owner>%s</d:owner>\n", ld.OwnerXML))
   618  	}
   619  	if ld.Duration > 0 {
   620  		timeout := ld.Duration / time.Second
   621  		lockdiscovery.WriteString(fmt.Sprintf("  <d:timeout>Second-%d</d:timeout>\n", timeout))
   622  	} else {
   623  		lockdiscovery.WriteString("  <d:timeout>Infinite</d:timeout>\n")
   624  	}
   625  	if token != "" {
   626  		lockdiscovery.WriteString(fmt.Sprintf("  <d:locktoken><d:href>%s</d:href></d:locktoken>\n", prop.Escape(token)))
   627  	}
   628  	if href != "" {
   629  		lockdiscovery.WriteString(fmt.Sprintf("  <d:lockroot><d:href>%s</d:href></d:lockroot>\n", prop.Escape(href)))
   630  	}
   631  	if ld.OwnerName != "" {
   632  		lockdiscovery.WriteString(fmt.Sprintf("  <oc:ownername>%s</oc:ownername>\n", prop.Escape(ld.OwnerName)))
   633  	}
   634  	if !ld.Locktime.IsZero() {
   635  		lockdiscovery.WriteString(fmt.Sprintf("  <oc:locktime>%s</oc:locktime>\n", prop.Escape(ld.Locktime.Format(time.RFC3339))))
   636  	}
   637  
   638  	lockdiscovery.WriteString("</d:activelock></d:lockdiscovery></d:prop>")
   639  
   640  	return fmt.Fprint(w, lockdiscovery.String())
   641  }
   642  
   643  func (s *svc) handleUnlock(w http.ResponseWriter, r *http.Request, ns string) (status int, err error) {
   644  	ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), fmt.Sprintf("%s %v", r.Method, r.URL.Path))
   645  	defer span.End()
   646  
   647  	span.SetAttributes(attribute.String("component", "ocdav"))
   648  
   649  	fn := path.Join(ns, r.URL.Path) // TODO do we still need to jail if we query the registry about the spaces?
   650  
   651  	// TODO instead of using a string namespace ns pass in the space with the request?
   652  	ref, cs3Status, err := spacelookup.LookupReferenceForPath(ctx, s.gatewaySelector, fn)
   653  	if err != nil {
   654  		return http.StatusInternalServerError, err
   655  	}
   656  	if cs3Status.Code != rpc.Code_CODE_OK {
   657  		return http.StatusInternalServerError, ocdavErrors.NewErrFromStatus(cs3Status)
   658  	}
   659  
   660  	return s.unlockReference(ctx, w, r, ref)
   661  }
   662  
   663  func (s *svc) handleSpaceUnlock(w http.ResponseWriter, r *http.Request, spaceID string) (status int, err error) {
   664  	ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), fmt.Sprintf("%s %v", r.Method, r.URL.Path))
   665  	defer span.End()
   666  
   667  	span.SetAttributes(attribute.String("component", "ocdav"))
   668  
   669  	ref, err := spacelookup.MakeStorageSpaceReference(spaceID, r.URL.Path)
   670  	if err != nil {
   671  		return http.StatusBadRequest, fmt.Errorf("invalid space id")
   672  	}
   673  
   674  	return s.unlockReference(ctx, w, r, &ref)
   675  }
   676  
   677  func (s *svc) unlockReference(ctx context.Context, _ http.ResponseWriter, r *http.Request, ref *provider.Reference) (retStatus int, retErr error) {
   678  	// http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the
   679  	// Lock-Token value should be a Coded-URL OR a token. We strip its angle brackets.
   680  	t := r.Header.Get(net.HeaderLockToken)
   681  	if len(t) > 2 && t[0] == '<' && t[len(t)-1] == '>' {
   682  		t = t[1 : len(t)-1]
   683  	}
   684  
   685  	err := s.LockSystem.Unlock(ctx, time.Now(), ref, t)
   686  	switch {
   687  	case err == nil:
   688  		return http.StatusNoContent, nil
   689  	case errors.Is(err, ocdavErrors.ErrLocked):
   690  		return http.StatusLocked, err
   691  	case errors.Is(err, ocdavErrors.ErrForbidden):
   692  		return http.StatusForbidden, err
   693  	}
   694  	return http.StatusInternalServerError, err
   695  }
   696  
   697  func requestLockToken(r *http.Request) string {
   698  	return strings.TrimSuffix(strings.TrimPrefix(r.Header.Get(net.HeaderLockToken), "<"), ">")
   699  }