Skip to content

Commit 1076773

Browse files
committed
fix(CorePlugin): Handle Depth header for COPY requests
1. According to the RFC[1] servers **must** support `Depth` 'infinity' and 0. 2. And COPY method on a collection without a Depth header MUST act as if a Depth header with value "infinity" was included. [1] rfc4918#section-9.8.3 Signed-off-by: Ferdinand Thiessen <[email protected]>
1 parent e180fcc commit 1076773

File tree

9 files changed

+108
-20
lines changed

9 files changed

+108
-20
lines changed

lib/DAV/CorePlugin.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,7 @@ public function httpCopy(RequestInterface $request, ResponseInterface $response)
650650
if (!$this->server->emit('beforeBind', [$copyInfo['destination']])) {
651651
return false;
652652
}
653-
if (!$this->server->emit('beforeCopy', [$path, $copyInfo['destination']])) {
653+
if (!$this->server->emit('beforeCopy', [$path, $copyInfo['destination'], $copyInfo['depth']])) {
654654
return false;
655655
}
656656

@@ -661,8 +661,8 @@ public function httpCopy(RequestInterface $request, ResponseInterface $response)
661661
$this->server->tree->delete($copyInfo['destination']);
662662
}
663663

664-
$this->server->tree->copy($path, $copyInfo['destination']);
665-
$this->server->emit('afterCopy', [$path, $copyInfo['destination']]);
664+
$this->server->tree->copy($path, $copyInfo['destination'], $copyInfo['depth']);
665+
$this->server->emit('afterCopy', [$path, $copyInfo['destination'], $copyInfo['depth']]);
666666
$this->server->emit('afterBind', [$copyInfo['destination']]);
667667

668668
// If a resource was overwritten we should send a 204, otherwise a 201

lib/DAV/ICopyTarget.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,11 @@ interface ICopyTarget extends ICollection
3131
* @param string $targetName new local file/collection name
3232
* @param string $sourcePath Full path to source node
3333
* @param INode $sourceNode Source node itself
34+
* @param string|int $depth How many level of children to copy.
35+
* The value can be 'infinity' or a positiv number including zero.
36+
* Zero means to only copy a shallow collection with props but without children.
3437
*
3538
* @return bool
3639
*/
37-
public function copyInto($targetName, $sourcePath, INode $sourceNode);
40+
public function copyInto($targetName, $sourcePath, INode $sourceNode, $depth);
3841
}

lib/DAV/Tree.php

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,11 @@ public function nodeExists($path)
119119
*
120120
* @param string $sourcePath The source location
121121
* @param string $destinationPath The full destination path
122+
* @param int|string $depth How much levle of children to copy.
123+
* The value can be 'infinity' or a positiv integer, including zero.
124+
* Zero means only copy the collection without children but with its properties.
122125
*/
123-
public function copy($sourcePath, $destinationPath)
126+
public function copy($sourcePath, $destinationPath, $depth = 'infinity')
124127
{
125128
$sourceNode = $this->getNodeForPath($sourcePath);
126129

@@ -129,8 +132,8 @@ public function copy($sourcePath, $destinationPath)
129132

130133
$destinationParent = $this->getNodeForPath($destinationDir);
131134
// Check if the target can handle the copy itself. If not, we do it ourselves.
132-
if (!$destinationParent instanceof ICopyTarget || !$destinationParent->copyInto($destinationName, $sourcePath, $sourceNode)) {
133-
$this->copyNode($sourceNode, $destinationParent, $destinationName);
135+
if (!$destinationParent instanceof ICopyTarget || !$destinationParent->copyInto($destinationName, $sourcePath, $sourceNode, $depth)) {
136+
$this->copyNode($sourceNode, $destinationParent, $destinationName, $depth);
134137
}
135138

136139
$this->markDirty($destinationDir);
@@ -160,7 +163,8 @@ public function move($sourcePath, $destinationPath)
160163
$moveSuccess = $newParentNode->moveInto($destinationName, $sourcePath, $sourceNode);
161164
}
162165
if (!$moveSuccess) {
163-
$this->copy($sourcePath, $destinationPath);
166+
// Move is a copy with depth = infinity and deleting the source afterwards
167+
$this->copy($sourcePath, $destinationPath, 'infinity');
164168
$this->getNodeForPath($sourcePath)->delete();
165169
}
166170
}
@@ -197,9 +201,13 @@ public function getChildren($path)
197201
$basePath .= '/';
198202
}
199203

200-
foreach ($node->getChildren() as $child) {
201-
$this->cache[$basePath.$child->getName()] = $child;
202-
yield $child;
204+
if ($node instanceof ICollection) {
205+
foreach ($node->getChildren() as $child) {
206+
$this->cache[$basePath.$child->getName()] = $child;
207+
yield $child;
208+
}
209+
} else {
210+
yield from [];
203211
}
204212
}
205213

@@ -285,8 +293,9 @@ public function getMultipleNodes($paths)
285293
* copyNode.
286294
*
287295
* @param string $destinationName
296+
* @param int|string $depth How many children of the node to copy
288297
*/
289-
protected function copyNode(INode $source, ICollection $destinationParent, $destinationName = null)
298+
protected function copyNode(INode $source, ICollection $destinationParent, ?string $destinationName = null, $depth = 'infinity')
290299
{
291300
if ('' === (string) $destinationName) {
292301
$destinationName = $source->getName();
@@ -308,10 +317,16 @@ protected function copyNode(INode $source, ICollection $destinationParent, $dest
308317
$destination = $destinationParent->getChild($destinationName);
309318
} elseif ($source instanceof ICollection) {
310319
$destinationParent->createDirectory($destinationName);
311-
312320
$destination = $destinationParent->getChild($destinationName);
313-
foreach ($source->getChildren() as $child) {
314-
$this->copyNode($child, $destination);
321+
322+
// Copy children if depth is not zero
323+
if ($depth !== 0) {
324+
// Adjust next depth for children (keep 'infinity' or decrease)
325+
$depth = $depth === 'infinity' ? 'infinity' : $depth - 1;
326+
$destination = $destinationParent->getChild($destinationName);
327+
foreach ($source->getChildren() as $child) {
328+
$this->copyNode($child, $destination, null, $depth);
329+
}
315330
}
316331
}
317332
if ($source instanceof IProperties && $destination instanceof IProperties) {

tests/Sabre/DAV/FSExt/ServerTest.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,10 @@ public function testCopy()
234234
{
235235
mkdir($this->tempDir.'/testcol');
236236

237-
$request = new HTTP\Request('COPY', '/test.txt', ['Destination' => '/testcol/test2.txt']);
237+
$request = new HTTP\Request('COPY', '/test.txt', [
238+
'Destination' => '/testcol/test2.txt',
239+
'Depth' => 'infinity',
240+
]);
238241
$this->server->httpRequest = ($request);
239242
$this->server->exec();
240243

tests/Sabre/DAV/HttpCopyTest.php

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,29 @@ class HttpCopyTest extends DAVServerTest
2121
*/
2222
public function setUpTree()
2323
{
24-
$this->tree = new Mock\Collection('root', [
24+
$propsCollection = new Mock\PropertiesCollection('propscoll', [
25+
'file3' => 'content3',
26+
'file4' => 'content4',
27+
], [
28+
'my-prop' => 'my-value',
29+
]);
30+
$propsCollection->failMode = 'updatepropstrue';
31+
$this->tree = new Mock\PropertiesCollection('root', [
2532
'file1' => 'content1',
2633
'file2' => 'content2',
27-
'coll1' => [
34+
'coll1' => new Mock\Collection('coll1', [
2835
'file3' => 'content3',
2936
'file4' => 'content4',
30-
],
37+
]),
38+
'propscoll' => $propsCollection,
3139
]);
3240
}
3341

3442
public function testCopyFile()
3543
{
3644
$request = new HTTP\Request('COPY', '/file1', [
3745
'Destination' => '/file5',
46+
'Depth' => 'infinity',
3847
]);
3948
$response = $this->request($request);
4049
self::assertEquals(201, $response->getStatus());
@@ -54,6 +63,7 @@ public function testCopyFileToExisting()
5463
{
5564
$request = new HTTP\Request('COPY', '/file1', [
5665
'Destination' => '/file2',
66+
'Depth' => 'infinity',
5767
]);
5868
$response = $this->request($request);
5969
self::assertEquals(204, $response->getStatus());
@@ -64,6 +74,7 @@ public function testCopyFileToExistingOverwriteT()
6474
{
6575
$request = new HTTP\Request('COPY', '/file1', [
6676
'Destination' => '/file2',
77+
'Depth' => 'infinity',
6778
'Overwrite' => 'T',
6879
]);
6980
$response = $this->request($request);
@@ -75,6 +86,7 @@ public function testCopyFileToExistingOverwriteBadValue()
7586
{
7687
$request = new HTTP\Request('COPY', '/file1', [
7788
'Destination' => '/file2',
89+
'Depth' => 'infinity',
7890
'Overwrite' => 'B',
7991
]);
8092
$response = $this->request($request);
@@ -85,6 +97,7 @@ public function testCopyFileNonExistantParent()
8597
{
8698
$request = new HTTP\Request('COPY', '/file1', [
8799
'Destination' => '/notfound/file2',
100+
'Depth' => 'infinity',
88101
]);
89102
$response = $this->request($request);
90103
self::assertEquals(409, $response->getStatus());
@@ -94,6 +107,7 @@ public function testCopyFileToExistingOverwriteF()
94107
{
95108
$request = new HTTP\Request('COPY', '/file1', [
96109
'Destination' => '/file2',
110+
'Depth' => 'infinity',
97111
'Overwrite' => 'F',
98112
]);
99113
$response = $this->request($request);
@@ -110,6 +124,7 @@ public function testCopyFileToExistinBlockedCreateDestination()
110124
});
111125
$request = new HTTP\Request('COPY', '/file1', [
112126
'Destination' => '/file2',
127+
'Depth' => 'infinity',
113128
'Overwrite' => 'T',
114129
]);
115130
$response = $this->request($request);
@@ -122,16 +137,39 @@ public function testCopyColl()
122137
{
123138
$request = new HTTP\Request('COPY', '/coll1', [
124139
'Destination' => '/coll2',
140+
'Depth' => 'infinity',
125141
]);
126142
$response = $this->request($request);
127143
self::assertEquals(201, $response->getStatus());
128144
self::assertEquals('content3', $this->tree->getChild('coll2')->getChild('file3')->get());
129145
}
130146

147+
public function testShallowCopyColl()
148+
{
149+
// Ensure proppatches are applied
150+
$this->tree->failMode = 'updatepropstrue';
151+
$request = new HTTP\Request('COPY', '/propscoll', [
152+
'Destination' => '/shallow-coll',
153+
'Depth' => '0',
154+
]);
155+
$response = $this->request($request);
156+
// reset
157+
$this->tree->failMode = false;
158+
159+
self::assertEquals(201, $response->getStatus());
160+
// The copied collection exists
161+
self::assertEquals(true, $this->tree->childExists('shallow-coll'));
162+
// But it does not contain children
163+
self::assertEquals([], $this->tree->getChild('shallow-coll')->getChildren());
164+
// But the properties are preserved
165+
self::assertEquals(['my-prop' => 'my-value'], $this->tree->getChild('shallow-coll')->getProperties([]));
166+
}
167+
131168
public function testCopyCollToSelf()
132169
{
133170
$request = new HTTP\Request('COPY', '/coll1', [
134171
'Destination' => '/coll1',
172+
'Depth' => 'infinity',
135173
]);
136174
$response = $this->request($request);
137175
self::assertEquals(403, $response->getStatus());
@@ -141,6 +179,7 @@ public function testCopyCollToExisting()
141179
{
142180
$request = new HTTP\Request('COPY', '/coll1', [
143181
'Destination' => '/file2',
182+
'Depth' => 'infinity',
144183
]);
145184
$response = $this->request($request);
146185
self::assertEquals(204, $response->getStatus());
@@ -151,6 +190,7 @@ public function testCopyCollToExistingOverwriteT()
151190
{
152191
$request = new HTTP\Request('COPY', '/coll1', [
153192
'Destination' => '/file2',
193+
'Depth' => 'infinity',
154194
'Overwrite' => 'T',
155195
]);
156196
$response = $this->request($request);
@@ -162,6 +202,7 @@ public function testCopyCollToExistingOverwriteF()
162202
{
163203
$request = new HTTP\Request('COPY', '/coll1', [
164204
'Destination' => '/file2',
205+
'Depth' => 'infinity',
165206
'Overwrite' => 'F',
166207
]);
167208
$response = $this->request($request);
@@ -173,6 +214,7 @@ public function testCopyCollIntoSubtree()
173214
{
174215
$request = new HTTP\Request('COPY', '/coll1', [
175216
'Destination' => '/coll1/subcol',
217+
'Depth' => 'infinity',
176218
]);
177219
$response = $this->request($request);
178220
self::assertEquals(409, $response->getStatus());

tests/Sabre/DAV/Locks/PluginTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,7 @@ public function testLockCopyLockSource()
585585

586586
$request = new HTTP\Request('COPY', '/dir/child.txt', [
587587
'Destination' => '/dir/child2.txt',
588+
'Depth' => 'infinity',
588589
]);
589590

590591
$this->server->httpRequest = $request;
@@ -619,6 +620,7 @@ public function testLockCopyLockDestination()
619620

620621
$request = new HTTP\Request('COPY', '/dir/child.txt', [
621622
'Destination' => '/dir/child2.txt',
623+
'Depth' => 'infinity',
622624
]);
623625
$this->server->httpRequest = $request;
624626
$this->server->exec();

tests/Sabre/DAV/Mock/Collection.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ public function createDirectory($name)
118118
*/
119119
public function getChildren()
120120
{
121-
return $this->children;
121+
return $this->children ?? [];
122122
}
123123

124124
/**

tests/Sabre/DAV/Mock/PropertiesCollection.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ public function propPatch(PropPatch $proppatch)
4545
$proppatch->handleRemaining(function ($updateProperties) {
4646
switch ($this->failMode) {
4747
case 'updatepropsfalse': return false;
48+
case 'updatepropstrue':
49+
foreach ($updateProperties as $k => $v) {
50+
$this->properties[$k] = $v;
51+
}
52+
return true;
4853
case 'updatepropsarray':
4954
$r = [];
5055
foreach ($updateProperties as $k => $v) {
@@ -76,6 +81,10 @@ public function propPatch(PropPatch $proppatch)
7681
*/
7782
public function getProperties($requestedProperties)
7883
{
84+
if (count($requestedProperties) === 0) {
85+
return $this->properties;
86+
}
87+
7988
$returnedProperties = [];
8089
foreach ($requestedProperties as $requestedProperty) {
8190
if (isset($this->properties[$requestedProperty])) {
@@ -86,4 +95,17 @@ public function getProperties($requestedProperties)
8695

8796
return $returnedProperties;
8897
}
98+
99+
/**
100+
* Creates a new subdirectory. (Override to ensure props are preserved)
101+
*
102+
* @param string $name
103+
*/
104+
public function createDirectory($name)
105+
{
106+
$child = new self($name, []);
107+
// keep same setting
108+
$child->failMode = $this->failMode;
109+
$this->children[] = $child;
110+
}
89111
}

tests/Sabre/DAV/ServerEventsTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public function testAfterCopy()
6767
$this->server->createFile($oldPath, 'body');
6868
$request = new HTTP\Request('COPY', $oldPath, [
6969
'Destination' => $newPath,
70+
'Depth' => 'infinity',
7071
]);
7172
$this->server->httpRequest = $request;
7273

0 commit comments

Comments
 (0)