Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F1720920
Plugin.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Size
17 KB
Subscribers
None
Plugin.php
View Options
<?php
namespace
Sabre\DAV\Locks
;
use
Sabre\DAV
;
use
Sabre\HTTP\RequestInterface
;
use
Sabre\HTTP\ResponseInterface
;
/**
* Locking plugin
*
* This plugin provides locking support to a WebDAV server.
* The easiest way to get started, is by hooking it up as such:
*
* $lockBackend = new Sabre\DAV\Locks\Backend\File('./mylockdb');
* $lockPlugin = new Sabre\DAV\Locks\Plugin($lockBackend);
* $server->addPlugin($lockPlugin);
*
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class
Plugin
extends
DAV\ServerPlugin
{
/**
* locksBackend
*
* @var Backend\Backend\Interface
*/
protected
$locksBackend
;
/**
* server
*
* @var Sabre\DAV\Server
*/
protected
$server
;
/**
* __construct
*
* @param Backend\BackendInterface $locksBackend
*/
function
__construct
(
Backend\BackendInterface
$locksBackend
)
{
$this
->
locksBackend
=
$locksBackend
;
}
/**
* Initializes the plugin
*
* This method is automatically called by the Server class after addPlugin.
*
* @param DAV\Server $server
* @return void
*/
function
initialize
(
DAV\Server
$server
)
{
$this
->
server
=
$server
;
$this
->
server
->
xml
->
elementMap
[
'{DAV:}lockinfo'
]
=
'Sabre
\\
DAV
\\
Xml
\\
Request
\\
Lock'
;
$server
->
on
(
'method:LOCK'
,
[
$this
,
'httpLock'
]);
$server
->
on
(
'method:UNLOCK'
,
[
$this
,
'httpUnlock'
]);
$server
->
on
(
'validateTokens'
,
[
$this
,
'validateTokens'
]);
$server
->
on
(
'propFind'
,
[
$this
,
'propFind'
]);
$server
->
on
(
'afterUnbind'
,
[
$this
,
'afterUnbind'
]);
}
/**
* Returns a plugin name.
*
* Using this name other plugins will be able to access other plugins
* using Sabre\DAV\Server::getPlugin
*
* @return string
*/
function
getPluginName
()
{
return
'locks'
;
}
/**
* This method is called after most properties have been found
* it allows us to add in any Lock-related properties
*
* @param DAV\PropFind $propFind
* @param DAV\INode $node
* @return void
*/
function
propFind
(
DAV\PropFind
$propFind
,
DAV\INode
$node
)
{
$propFind
->
handle
(
'{DAV:}supportedlock'
,
function
()
{
return
new
DAV\Xml\Property\SupportedLock
();
});
$propFind
->
handle
(
'{DAV:}lockdiscovery'
,
function
()
use
(
$propFind
)
{
return
new
DAV\Xml\Property\LockDiscovery
(
$this
->
getLocks
(
$propFind
->
getPath
())
);
});
}
/**
* Use this method to tell the server this plugin defines additional
* HTTP methods.
*
* This method is passed a uri. It should only return HTTP methods that are
* available for the specified uri.
*
* @param string $uri
* @return array
*/
function
getHTTPMethods
(
$uri
)
{
return
[
'LOCK'
,
'UNLOCK'
];
}
/**
* Returns a list of features for the HTTP OPTIONS Dav: header.
*
* In this case this is only the number 2. The 2 in the Dav: header
* indicates the server supports locks.
*
* @return array
*/
function
getFeatures
()
{
return
[
2
];
}
/**
* Returns all lock information on a particular uri
*
* This function should return an array with Sabre\DAV\Locks\LockInfo objects. If there are no locks on a file, return an empty array.
*
* Additionally there is also the possibility of locks on parent nodes, so we'll need to traverse every part of the tree
* If the $returnChildLocks argument is set to true, we'll also traverse all the children of the object
* for any possible locks and return those as well.
*
* @param string $uri
* @param bool $returnChildLocks
* @return array
*/
function
getLocks
(
$uri
,
$returnChildLocks
=
false
)
{
return
$this
->
locksBackend
->
getLocks
(
$uri
,
$returnChildLocks
);
}
/**
* Locks an uri
*
* The WebDAV lock request can be operated to either create a new lock on a file, or to refresh an existing lock
* If a new lock is created, a full XML body should be supplied, containing information about the lock such as the type
* of lock (shared or exclusive) and the owner of the lock
*
* If a lock is to be refreshed, no body should be supplied and there should be a valid If header containing the lock
*
* Additionally, a lock can be requested for a non-existent file. In these case we're obligated to create an empty file as per RFC4918:S7.3
*
* @param RequestInterface $request
* @param ResponseInterface $response
* @return bool
*/
function
httpLock
(
RequestInterface
$request
,
ResponseInterface
$response
)
{
$uri
=
$request
->
getPath
();
$existingLocks
=
$this
->
getLocks
(
$uri
);
if
(
$body
=
$request
->
getBodyAsString
())
{
// This is a new lock request
$existingLock
=
null
;
// Checking if there's already non-shared locks on the uri.
foreach
(
$existingLocks
as
$existingLock
)
{
if
(
$existingLock
->
scope
===
LockInfo
::
EXCLUSIVE
)
{
throw
new
DAV\Exception\ConflictingLock
(
$existingLock
);
}
}
$lockInfo
=
$this
->
parseLockRequest
(
$body
);
$lockInfo
->
depth
=
$this
->
server
->
getHTTPDepth
();
$lockInfo
->
uri
=
$uri
;
if
(
$existingLock
&&
$lockInfo
->
scope
!=
LockInfo
::
SHARED
)
throw
new
DAV\Exception\ConflictingLock
(
$existingLock
);
}
else
{
// Gonna check if this was a lock refresh.
$existingLocks
=
$this
->
getLocks
(
$uri
);
$conditions
=
$this
->
server
->
getIfConditions
(
$request
);
$found
=
null
;
foreach
(
$existingLocks
as
$existingLock
)
{
foreach
(
$conditions
as
$condition
)
{
foreach
(
$condition
[
'tokens'
]
as
$token
)
{
if
(
$token
[
'token'
]
===
'opaquelocktoken:'
.
$existingLock
->
token
)
{
$found
=
$existingLock
;
break
3
;
}
}
}
}
// If none were found, this request is in error.
if
(
is_null
(
$found
))
{
if
(
$existingLocks
)
{
throw
new
DAV\Exception\Locked
(
reset
(
$existingLocks
));
}
else
{
throw
new
DAV\Exception\BadRequest
(
'An xml body is required for lock requests'
);
}
}
// This must have been a lock refresh
$lockInfo
=
$found
;
// The resource could have been locked through another uri.
if
(
$uri
!=
$lockInfo
->
uri
)
$uri
=
$lockInfo
->
uri
;
}
if
(
$timeout
=
$this
->
getTimeoutHeader
())
$lockInfo
->
timeout
=
$timeout
;
$newFile
=
false
;
// If we got this far.. we should go check if this node actually exists. If this is not the case, we need to create it first
try
{
$this
->
server
->
tree
->
getNodeForPath
(
$uri
);
// We need to call the beforeWriteContent event for RFC3744
// Edit: looks like this is not used, and causing problems now.
//
// See Issue 222
// $this->server->emit('beforeWriteContent',array($uri));
}
catch
(
DAV\Exception\NotFound
$e
)
{
// It didn't, lets create it
$this
->
server
->
createFile
(
$uri
,
fopen
(
'php://memory'
,
'r'
));
$newFile
=
true
;
}
$this
->
lockNode
(
$uri
,
$lockInfo
);
$response
->
setHeader
(
'Content-Type'
,
'application/xml; charset=utf-8'
);
$response
->
setHeader
(
'Lock-Token'
,
'<opaquelocktoken:'
.
$lockInfo
->
token
.
'>'
);
$response
->
setStatus
(
$newFile
?
201
:
200
);
$response
->
setBody
(
$this
->
generateLockResponse
(
$lockInfo
));
// Returning false will interupt the event chain and mark this method
// as 'handled'.
return
false
;
}
/**
* Unlocks a uri
*
* This WebDAV method allows you to remove a lock from a node. The client should provide a valid locktoken through the Lock-token http header
* The server should return 204 (No content) on success
*
* @param RequestInterface $request
* @param ResponseInterface $response
* @return void
*/
function
httpUnlock
(
RequestInterface
$request
,
ResponseInterface
$response
)
{
$lockToken
=
$request
->
getHeader
(
'Lock-Token'
);
// If the locktoken header is not supplied, we need to throw a bad request exception
if
(!
$lockToken
)
throw
new
DAV\Exception\BadRequest
(
'No lock token was supplied'
);
$path
=
$request
->
getPath
();
$locks
=
$this
->
getLocks
(
$path
);
// Windows sometimes forgets to include < and > in the Lock-Token
// header
if
(
$lockToken
[
0
]
!==
'<'
)
$lockToken
=
'<'
.
$lockToken
.
'>'
;
foreach
(
$locks
as
$lock
)
{
if
(
'<opaquelocktoken:'
.
$lock
->
token
.
'>'
==
$lockToken
)
{
$this
->
unlockNode
(
$path
,
$lock
);
$response
->
setHeader
(
'Content-Length'
,
'0'
);
$response
->
setStatus
(
204
);
// Returning false will break the method chain, and mark the
// method as 'handled'.
return
false
;
}
}
// If we got here, it means the locktoken was invalid
throw
new
DAV\Exception\LockTokenMatchesRequestUri
();
}
/**
* This method is called after a node is deleted.
*
* We use this event to clean up any locks that still exist on the node.
*
* @param string $path
* @return void
*/
function
afterUnbind
(
$path
)
{
$locks
=
$this
->
getLocks
(
$path
,
$includeChildren
=
true
);
foreach
(
$locks
as
$lock
)
{
$this
->
unlockNode
(
$path
,
$lock
);
}
}
/**
* Locks a uri
*
* All the locking information is supplied in the lockInfo object. The object has a suggested timeout, but this can be safely ignored
* It is important that if the existing timeout is ignored, the property is overwritten, as this needs to be sent back to the client
*
* @param string $uri
* @param LockInfo $lockInfo
* @return bool
*/
function
lockNode
(
$uri
,
LockInfo
$lockInfo
)
{
if
(!
$this
->
server
->
emit
(
'beforeLock'
,
[
$uri
,
$lockInfo
]))
return
;
return
$this
->
locksBackend
->
lock
(
$uri
,
$lockInfo
);
}
/**
* Unlocks a uri
*
* This method removes a lock from a uri. It is assumed all the supplied information is correct and verified
*
* @param string $uri
* @param LockInfo $lockInfo
* @return bool
*/
function
unlockNode
(
$uri
,
LockInfo
$lockInfo
)
{
if
(!
$this
->
server
->
emit
(
'beforeUnlock'
,
[
$uri
,
$lockInfo
]))
return
;
return
$this
->
locksBackend
->
unlock
(
$uri
,
$lockInfo
);
}
/**
* Returns the contents of the HTTP Timeout header.
*
* The method formats the header into an integer.
*
* @return int
*/
function
getTimeoutHeader
()
{
$header
=
$this
->
server
->
httpRequest
->
getHeader
(
'Timeout'
);
if
(
$header
)
{
if
(
stripos
(
$header
,
'second-'
)
===
0
)
$header
=
(
int
)(
substr
(
$header
,
7
));
elseif
(
stripos
(
$header
,
'infinite'
)
===
0
)
$header
=
LockInfo
::
TIMEOUT_INFINITE
;
else
throw
new
DAV\Exception\BadRequest
(
'Invalid HTTP timeout header'
);
}
else
{
$header
=
0
;
}
return
$header
;
}
/**
* Generates the response for successful LOCK requests
*
* @param LockInfo $lockInfo
* @return string
*/
protected
function
generateLockResponse
(
LockInfo
$lockInfo
)
{
return
$this
->
server
->
xml
->
write
(
'{DAV:}prop'
,
[
'{DAV:}lockdiscovery'
=>
new
DAV\Xml\Property\LockDiscovery
([
$lockInfo
])
]);
}
/**
* The validateTokens event is triggered before every request.
*
* It's a moment where this plugin can check all the supplied lock tokens
* in the If: header, and check if they are valid.
*
* In addition, it will also ensure that it checks any missing lokens that
* must be present in the request, and reject requests without the proper
* tokens.
*
* @param RequestInterface $request
* @param mixed $conditions
* @return void
*/
function
validateTokens
(
RequestInterface
$request
,
&
$conditions
)
{
// First we need to gather a list of locks that must be satisfied.
$mustLocks
=
[];
$method
=
$request
->
getMethod
();
// Methods not in that list are operations that doesn't alter any
// resources, and we don't need to check the lock-states for.
switch
(
$method
)
{
case
'DELETE'
:
$mustLocks
=
array_merge
(
$mustLocks
,
$this
->
getLocks
(
$request
->
getPath
(),
true
));
break
;
case
'MKCOL'
:
case
'MKCALENDAR'
:
case
'PROPPATCH'
:
case
'PUT'
:
case
'PATCH'
:
$mustLocks
=
array_merge
(
$mustLocks
,
$this
->
getLocks
(
$request
->
getPath
(),
false
));
break
;
case
'MOVE'
:
$mustLocks
=
array_merge
(
$mustLocks
,
$this
->
getLocks
(
$request
->
getPath
(),
true
));
$mustLocks
=
array_merge
(
$mustLocks
,
$this
->
getLocks
(
$this
->
server
->
calculateUri
(
$request
->
getHeader
(
'Destination'
)),
false
));
break
;
case
'COPY'
:
$mustLocks
=
array_merge
(
$mustLocks
,
$this
->
getLocks
(
$this
->
server
->
calculateUri
(
$request
->
getHeader
(
'Destination'
)),
false
));
break
;
case
'LOCK'
:
//Temporary measure.. figure out later why this is needed
// Here we basically ignore all incoming tokens...
foreach
(
$conditions
as
$ii
=>
$condition
)
{
foreach
(
$condition
[
'tokens'
]
as
$jj
=>
$token
)
{
$conditions
[
$ii
][
'tokens'
][
$jj
][
'validToken'
]
=
true
;
}
}
return
;
}
// It's possible that there's identical locks, because of shared
// parents. We're removing the duplicates here.
$tmp
=
[];
foreach
(
$mustLocks
as
$lock
)
$tmp
[
$lock
->
token
]
=
$lock
;
$mustLocks
=
array_values
(
$tmp
);
foreach
(
$conditions
as
$kk
=>
$condition
)
{
foreach
(
$condition
[
'tokens'
]
as
$ii
=>
$token
)
{
// Lock tokens always start with opaquelocktoken:
if
(
substr
(
$token
[
'token'
],
0
,
16
)
!==
'opaquelocktoken:'
)
{
continue
;
}
$checkToken
=
substr
(
$token
[
'token'
],
16
);
// Looping through our list with locks.
foreach
(
$mustLocks
as
$jj
=>
$mustLock
)
{
if
(
$mustLock
->
token
==
$checkToken
)
{
// We have a match!
// Removing this one from mustlocks
unset
(
$mustLocks
[
$jj
]);
// Marking the condition as valid.
$conditions
[
$kk
][
'tokens'
][
$ii
][
'validToken'
]
=
true
;
// Advancing to the next token
continue
2
;
}
}
// If we got here, it means that there was a
// lock-token, but it was not in 'mustLocks'.
//
// This is an edge-case, as it could mean that token
// was specified with a url that was not 'required' to
// check. So we're doing one extra lookup to make sure
// we really don't know this token.
//
// This also gets triggered when the user specified a
// lock-token that was expired.
$oddLocks
=
$this
->
getLocks
(
$condition
[
'uri'
]);
foreach
(
$oddLocks
as
$oddLock
)
{
if
(
$oddLock
->
token
===
$checkToken
)
{
// We have a hit!
$conditions
[
$kk
][
'tokens'
][
$ii
][
'validToken'
]
=
true
;
continue
2
;
}
}
// If we get all the way here, the lock-token was
// really unknown.
}
}
// If there's any locks left in the 'mustLocks' array, it means that
// the resource was locked and we must block it.
if
(
$mustLocks
)
{
throw
new
DAV\Exception\Locked
(
reset
(
$mustLocks
));
}
}
/**
* Parses a webdav lock xml body, and returns a new Sabre\DAV\Locks\LockInfo object
*
* @param string $body
* @return LockInfo
*/
protected
function
parseLockRequest
(
$body
)
{
$result
=
$this
->
server
->
xml
->
expect
(
'{DAV:}lockinfo'
,
$body
);
$lockInfo
=
new
LockInfo
();
$lockInfo
->
owner
=
$result
->
owner
;
$lockInfo
->
token
=
DAV\UUIDUtil
::
getUUID
();
$lockInfo
->
scope
=
$result
->
scope
;
return
$lockInfo
;
}
/**
* Returns a bunch of meta-data about the plugin.
*
* Providing this information is optional, and is mainly displayed by the
* Browser plugin.
*
* The description key in the returned array may contain html and will not
* be sanitized.
*
* @return array
*/
function
getPluginInfo
()
{
return
[
'name'
=>
$this
->
getPluginName
(),
'description'
=>
'The locks plugin turns this server into a class-2 WebDAV server and adds support for LOCK and UNLOCK'
,
'link'
=>
'http://sabre.io/dav/locks/'
,
];
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sat, Nov 23, 10:49 AM (2 h, 4 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
911091
Default Alt Text
Plugin.php (17 KB)
Attached To
rDAVCAL DokuWiki DAVCal PlugIn
Event Timeline
Log In to Comment