Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 56 additions & 25 deletions lib/DAV/CorePlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -303,28 +303,14 @@ public function httpDelete(RequestInterface $request, ResponseInterface $respons
*/
public function httpPropFind(RequestInterface $request, ResponseInterface $response)
{
$path = $request->getPath();

$requestBody = $request->getBodyAsString();
if (strlen($requestBody)) {
try {
$propFindXml = $this->server->xml->expect('{DAV:}propfind', $requestBody);
} catch (ParseException $e) {
throw new BadRequest($e->getMessage(), 0, $e);
}
} else {
$propFindXml = new Xml\Request\PropFind();
$propFindXml->allProp = true;
$propFindXml->properties = [];
}

$depth = $this->server->getHTTPDepth(1);
// The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled
if (!$this->server->enablePropfindDepthInfinity && 0 != $depth) {
$depth = 1;
}

$newProperties = $this->server->getPropertiesIteratorForPath($path, $propFindXml->properties, $depth);
$propFindXml = $this->parseRequestedProperties($request);
$propFindRequests = $this->server->generatePropFindsForPath(
$request->getPath(),
$propFindXml->properties,
$this->getDepth()
);
$this->server->emit('beforePropertyResolution', [&$propFindRequests]);
$fileProperties = $this->server->getNodePropertiesGenerator($propFindRequests);

// This is a multi-status response
$response->setStatus(207);
Expand All @@ -334,16 +320,17 @@ public function httpPropFind(RequestInterface $request, ResponseInterface $respo
// Normally this header is only needed for OPTIONS responses, however..
// iCal seems to also depend on these being set for PROPFIND. Since
// this is not harmful, we'll add it.
$features = ['1', '3', 'extended-mkcol'];
$features = [['1', '3', 'extended-mkcol']];
foreach ($this->server->getPlugins() as $plugin) {
$features = array_merge($features, $plugin->getFeatures());
$features[] = $plugin->getFeatures();
}
$features = array_merge(...$features);
$response->setHeader('DAV', implode(', ', $features));

$prefer = $this->server->getHTTPPrefer();
$minimal = 'minimal' === $prefer['return'];

$data = $this->server->generateMultiStatus($newProperties, $minimal);
$data = $this->server->generateMultiStatus($fileProperties, $minimal);
$response->setBody($data);

// Sending back false will interrupt the event chain and tell the server
Expand Down Expand Up @@ -904,4 +891,48 @@ public function getPluginInfo()
'link' => null,
];
}

/**
* Parses the PROPFIND request body and returns a PropFind XML object.
*
* If the request body contains XML, it is parsed using the server's XML service
* and must represent a {DAV:}propfind element. If the body is empty, a default
* PropFind(XML) object is created that requests all properties.
*
* @return array|Xml\Request\PropFind|string
*
* @throws BadRequest
*/
public function parseRequestedProperties(RequestInterface $request)
{
$requestBody = $request->getBodyAsString();
if (strlen($requestBody)) {
try {
$propFindXml =
$this->server->xml->expect('{DAV:}propfind', $requestBody);
} catch (ParseException $e) {
throw new BadRequest($e->getMessage(), 0, $e);
}
} else {
$propFindXml = new Xml\Request\PropFind();
$propFindXml->allProp = true;
$propFindXml->properties = [];
}

return $propFindXml;
}

/**
* Returns the appropriate value for depth from the HTTP request.
*/
protected function getDepth(): int
{
$depth = $this->server->getHTTPDepth(1);
// The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled
if (!$this->server->enablePropfindDepthInfinity && 0 != $depth) {
$depth = 1;
}

return $depth;
}
}
66 changes: 52 additions & 14 deletions lib/DAV/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@

namespace Sabre\DAV;

use Generator;
use Iterator;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Sabre\DAV\Exception\NotFound;
use Sabre\Event\EmitterInterface;
use Sabre\Event\WildcardEmitterTrait;
use Sabre\HTTP;
Expand Down Expand Up @@ -423,7 +426,7 @@ public function getPlugin($name)
/**
* Returns all plugins.
*
* @return array
* @return ServerPlugin[]
*/
public function getPlugins()
{
Expand Down Expand Up @@ -942,21 +945,20 @@ public function getPropertiesForPath($path, $propertyNames = [], $depth = 0)
}

/**
* Returns a list of properties for a given path.
* Returns an iterable of tuples, each containing an initialized PropFind object
* and its corresponding node, for the given $path. The node tree is traversed
* starting from the node at $path, according to the specified $depth.
*
* The path that should be supplied should have the baseUrl stripped out
* The list of properties should be supplied in Clark notation. If the list is empty
* 'allprops' is assumed.
* Each PropFind represents a resource discovered during traversal but contains
* no property values. The objects are only initialized with the appropriate
* constructor parameters and are meant to be populated later by the property
* retrieval logic.
*
* If a depth of 1 is requested child elements will also be returned.
*
* @param string $path
* @param array $propertyNames
* @param int $depth
* @return iterable<array{PropFind, INode}> Iterable of [PropFind, INode] tuples
*
* @return \Iterator
* @throws NotFound when a node for $path cannot be found
*/
public function getPropertiesIteratorForPath($path, $propertyNames = [], $depth = 0)
public function generatePropFindsForPath($path, $propertyNames = [], $depth = 0): iterable
{
// The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled
if (!$this->enablePropfindDepthInfinity && 0 != $depth) {
Expand All @@ -979,8 +981,22 @@ public function getPropertiesIteratorForPath($path, $propertyNames = [], $depth
$propFindRequests = $this->generatePathNodes(clone $propFind, current($propFindRequests));
}

foreach ($propFindRequests as $propFindRequest) {
list($propFind, $node) = $propFindRequest;
return $propFindRequests;
}

/**
* Yields arrays representing multistatus results with all the discovered
* properties for the provided $propFindRequests.
*
* @see PropFind::getResultForMultiStatus()
*/
public function getNodePropertiesGenerator(iterable $propFindRequests): Generator
{
/**
* @var PropFind $propFind
* @var INode $node
*/
foreach ($propFindRequests as [$propFind, $node]) {
$r = $this->getPropertiesByNode($propFind, $node);
if ($r) {
$result = $propFind->getResultForMultiStatus();
Expand All @@ -999,6 +1015,28 @@ public function getPropertiesIteratorForPath($path, $propertyNames = [], $depth
}
}

/**
* Returns a list of properties for a given path.
*
* The path that should be supplied should have the baseUrl stripped out
* The list of properties should be supplied in Clark notation. If the list is empty
* 'allprops' is assumed.
*
* If a depth of 1 is requested child elements will also be returned.
*
* @param string $path
* @param array $propertyNames
* @param int $depth
*
* @return Iterator
*/
public function getPropertiesIteratorForPath($path, $propertyNames = [], $depth = 0)
{
$propFinds = $this->generatePropFindsForPath($path, $propertyNames, $depth);

return $this->getNodePropertiesGenerator($propFinds);
}

/**
* Returns a list of properties for a list of paths.
*
Expand Down
2 changes: 1 addition & 1 deletion tests/Sabre/CalDAV/CalendarObjectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ public function testGetLastModified()
$obj = $children[0];

$lastMod = $obj->getLastModified();
self::assertTrue(is_int($lastMod) || ctype_digit($lastMod) || is_null($lastMod));
self::assertTrue(is_null($lastMod) || is_int($lastMod) || ctype_digit($lastMod));
}

/**
Expand Down
2 changes: 1 addition & 1 deletion tests/Sabre/CalDAV/Schedule/SchedulingObjectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ public function testGetLastModified()
$obj = $children[0];

$lastMod = $obj->getLastModified();
self::assertTrue(is_int($lastMod) || ctype_digit($lastMod) || is_null($lastMod));
self::assertTrue(is_null($lastMod) || is_int($lastMod) || ctype_digit($lastMod));
}

/**
Expand Down
135 changes: 132 additions & 3 deletions tests/Sabre/DAV/CorePluginTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,140 @@

namespace Sabre\DAV;

class CorePluginTest extends \PHPUnit\Framework\TestCase
use Generator;
use PHPUnit\Framework\TestCase;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;

class CorePluginTest extends TestCase
{
private CorePlugin $plugin;

public function testGetInfo()
{
$corePlugin = new CorePlugin();
self::assertEquals('core', $corePlugin->getPluginInfo()['name']);
self::assertEquals('core', $this->plugin->getPluginInfo()['name']);
}

/**
* @dataProvider beforePropertyResolutionEventData
*/
public function testBeforePropertyResolutionEvent(string $basePath, array $originalPropFinds, array $modifiedPropFinds): void
{
$request = $this->createMock(RequestInterface::class);
$request->method('getPath')->willReturn($basePath);
$request->method('getBodyAsString')->willReturn('');

$response = $this->createMock(ResponseInterface::class);

$server = $this->getMockBuilder(Server::class)
->disableOriginalConstructor()
->onlyMethods([
'getHTTPDepth',
'getPlugins',
'getHTTPPrefer',
'generatePropFindsForPath',
'getNodePropertiesGenerator',
'generateMultiStatus',
'emit',
])
->getMock();

$server->method('getHTTPDepth')
->willReturn(1);
$server->method('getPlugins')
->willReturn([]);
$server->method('getHTTPPrefer')
->willReturn(['return' => null]);

$server->expects($this->once())
->method('generatePropFindsForPath')
->willReturn($originalPropFinds);

$server->method('generateMultiStatus')
->willReturn('');

$server->method('emit')
->willReturnCallback(
function (string $eventName, $eventArgs) use ($originalPropFinds, $modifiedPropFinds) {
$this->assertEquals('beforePropertyResolution', $eventName);
/** @var iterable $propFinds */
[$propFinds] = $eventArgs;
$this->assertIsIterable($propFinds);
$this->assertEquals($originalPropFinds, $propFinds);
$eventArgs[0] = $this->toGenerator($modifiedPropFinds);

return true;
}
);

$server->method('getNodePropertiesGenerator')
->willReturnCallback(function ($propFinds) use ($modifiedPropFinds) {
// check that the generator received the modified list of PropFinds
$this->assertIsIterable($propFinds);
$array = iterator_to_array($propFinds);
$this->assertEquals($modifiedPropFinds, $array);

return $this->toGenerator($modifiedPropFinds);
});

$server->method('generateMultiStatus')
->willReturnCallback(function ($fileProperties, $minimal) use ($modifiedPropFinds) {
// check that generateMultiStatus receives the modified list of PropFinds
$array = iterator_to_array($fileProperties);
$this->assertEquals($modifiedPropFinds, $array);
});

$this->plugin->initialize($server);
$this->plugin->httpPropFind($request, $response);
}

private function toGenerator(array $paths): Generator
{
yield from $paths;
}

/**
* @param string[] $paths
*
* @return array<array{PropFind, INode}>
*/
private function getPropFindNodeTuples(array $paths): array
{
$propFindNodeTuples = [];
foreach ($paths as $path) {
$propFindNodeTuples[] = [
new PropFind($path, [], 1, PropFind::ALLPROPS),
$this->createMock(INode::class),
];
}

return $propFindNodeTuples;
}

public function beforePropertyResolutionEventData(): array
{
$basePath = 'files/user';
$paths = [$basePath];
for ($i = 0; $i < 5; ++$i) {
$paths[] = "$basePath/$i";
}

$originalPropFinds = $this->getPropFindNodeTuples($paths);
$modifiedPropFinds = [$originalPropFinds[0]];

return [
'Test PROPFIND with all PropFind objects' => [
$basePath, $originalPropFinds, $originalPropFinds,
],
'Test PROPFIND with removed PropFind objects' => [
$basePath, $originalPropFinds, $modifiedPropFinds,
],
];
}

protected function setUp(): void
{
parent::setUp();
$this->plugin = new CorePlugin();
}
}