diff --git a/lib/DAV/Client.php b/lib/DAV/Client.php index a9de71cdbb..187b29ba37 100644 --- a/lib/DAV/Client.php +++ b/lib/DAV/Client.php @@ -174,27 +174,98 @@ public function __construct(array $settings) } /** - * Does a PROPFIND request. + * Does a PROPFIND request with filtered response returning only available properties. * * The list of requested properties must be specified as an array, in clark * notation. * - * The returned array will contain a list of filenames as keys, and - * properties as values. + * Depth should be either 0 or 1. A depth of 1 will cause a request to be + * made to the server to also return all child resources. + * + * For depth 0, just the array of properties for the resource is returned. + * + * For depth 1, the returned array will contain a list of resource names as keys, + * and an array of properties as values. * - * The properties array will contain the list of properties. Only properties - * that are actually returned from the server (without error) will be + * The array of properties will contain the properties as keys with their values as the value. + * Only properties that are actually returned from the server without error will be * returned, anything else is discarded. * + * @param 1|0 $depth + */ + public function propFind(string $url, array $properties, int $depth = 0): array + { + $result = $this->doPropFind($url, $properties, $depth); + + // If depth was 0, we only return the top item + if (0 === $depth) { + reset($result); + $result = current($result); + + return isset($result[200]) ? $result[200] : []; + } + + $newResult = []; + foreach ($result as $href => $statusList) { + $newResult[$href] = isset($statusList[200]) ? $statusList[200] : []; + } + + return $newResult; + } + + /** + * Does a PROPFIND request with unfiltered response. + * + * The list of requested properties must be specified as an array, in clark + * notation. + * * Depth should be either 0 or 1. A depth of 1 will cause a request to be * made to the server to also return all child resources. * - * @param string $url - * @param int $depth + * For depth 0, just the multi-level array of status and properties for the resource is returned. * - * @return array + * For depth 1, the returned array will contain a list of resources as keys and + * a multi-level array containing status and properties as value. + * + * The multi-level array of status and properties is formatted the same as what is + * documented for parseMultiStatus. + * + * All properties that are actually returned from the server are returned by this method. + * + * @param 1|0 $depth + */ + public function propFindUnfiltered(string $url, array $properties, int $depth = 0): array + { + $result = $this->doPropFind($url, $properties, $depth); + + // If depth was 0, we only return the top item + if (0 === $depth) { + reset($result); + + return current($result); + } else { + return $result; + } + } + + /** + * Does a PROPFIND request. + * + * The list of requested properties must be specified as an array, in clark + * notation. + * + * Depth should be either 0 or 1. A depth of 1 will cause a request to be + * made to the server to also return all child resources. + * + * The returned array will contain a list of resources as keys and + * a multi-level array containing status and properties as value. + * + * The multi-level array of status and properties is formatted the same as what is + * documented for parseMultiStatus. + * + * @param 1|0 $depth */ - public function propFind($url, array $properties, $depth = 0) + private function doPropFind(string $url, array $properties, int $depth = 0): array { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->formatOutput = true; @@ -232,22 +303,7 @@ public function propFind($url, array $properties, $depth = 0) throw new HTTP\ClientHttpException($response); } - $result = $this->parseMultiStatus($response->getBodyAsString()); - - // If depth was 0, we only return the top item - if (0 === $depth) { - reset($result); - $result = current($result); - - return isset($result[200]) ? $result[200] : []; - } - - $newResult = []; - foreach ($result as $href => $statusList) { - $newResult[$href] = isset($statusList[200]) ? $statusList[200] : []; - } - - return $newResult; + return $this->parseMultiStatus($response->getBodyAsString()); } /** diff --git a/tests/Sabre/DAV/ClientTest.php b/tests/Sabre/DAV/ClientTest.php index 33f960ace8..e19ec07b44 100644 --- a/tests/Sabre/DAV/ClientTest.php +++ b/tests/Sabre/DAV/ClientTest.php @@ -185,6 +185,288 @@ public function testPropFindDepth1() ], $request->getHeaders()); } + /** + * A PROPFIND on a folder containing resources will filter out the meta-data + * for resources that have a status that is not 200. + * For example, resources that are "403" (access is forbidden to the user) + * or "425" (too early), the resource may have been recently uploaded and + * still has some processing happening in the server before being made + * available for regular access. + */ + public function testPropFindMixedErrors() + { + $client = new ClientMock([ + 'baseUri' => '/', + ]); + + $responseBody = << + + + /folder1 + + + + Folder1 + + HTTP/1.1 200 OK + + + + /folder1/file1.txt + + + + File1 + + HTTP/1.1 200 OK + + + + /folder1/file2.txt + + + + File2 + + HTTP/1.1 403 Forbidden + + + + /folder1/file3.txt + + + + File3 + + HTTP/1.1 425 Too Early + + + +XML; + + $client->response = new Response(207, [], $responseBody); + $result = $client->propFind('folder1', ['{DAV:}resourcetype', '{DAV:}displayname', '{urn:zim}gir'], 1); + + self::assertEquals([ + '/folder1' => [ + '{DAV:}resourcetype' => new Xml\Property\ResourceType('{DAV:}collection'), + '{DAV:}displayname' => 'Folder1', + ], + '/folder1/file1.txt' => [ + '{DAV:}resourcetype' => null, + '{DAV:}displayname' => 'File1', + ], + '/folder1/file2.txt' => [], + '/folder1/file3.txt' => [], + ], $result); + + $request = $client->request; + self::assertEquals('PROPFIND', $request->getMethod()); + self::assertEquals('/folder1', $request->getUrl()); + self::assertEquals([ + 'Depth' => ['1'], + 'Content-Type' => ['application/xml'], + ], $request->getHeaders()); + } + + /** + * An "unfiltered" PROPFIND on a folder containing resources will include the + * meta-data for resources that have a status that is not 200. + * For example, resources that are "403" (access is forbidden to the user) + * or "425" (too early), the resource may have been recently uploaded and + * still has some processing happening in the server before being made + * available for regular access. + */ + public function testPropFindUnfilteredDepth0() + { + $client = new ClientMock([ + 'baseUri' => '/', + ]); + + $responseBody = << + + + /folder1 + + + + Folder1 + + HTTP/1.1 200 OK + + + + + + HTTP/1.1 404 Not Found + + + +XML; + + $client->response = new Response(207, [], $responseBody); + $result = $client->propFindUnfiltered('folder1', ['{DAV:}resourcetype', '{DAV:}displayname', '{DAV:}contentlength', '{urn:zim}gir']); + + self::assertEquals([ + 200 => [ + '{DAV:}resourcetype' => new Xml\Property\ResourceType('{DAV:}collection'), + '{DAV:}displayname' => 'Folder1', + ], + 404 => [ + '{DAV:}contentlength' => null, + ], + ], $result); + + $request = $client->request; + self::assertEquals('PROPFIND', $request->getMethod()); + self::assertEquals('/folder1', $request->getUrl()); + self::assertEquals([ + 'Depth' => ['0'], + 'Content-Type' => ['application/xml'], + ], $request->getHeaders()); + } + + /** + * An "unfiltered" PROPFIND on a folder containing resources will include the + * meta-data for resources that have a status that is not 200. + * For example, resources that are "403" (access is forbidden to the user) + * or "425" (too early), the resource may have been recently uploaded and + * still has some processing happening in the server before being made + * available for regular access. + */ + public function testPropFindUnfiltered() + { + $client = new ClientMock([ + 'baseUri' => '/', + ]); + + $responseBody = << + + + /folder1 + + + + Folder1 + + HTTP/1.1 200 OK + + + + + + HTTP/1.1 404 Not Found + + + + /folder1/file1.txt + + + + File1 + 12 + + HTTP/1.1 200 OK + + + + /folder1/file2.txt + + + + File2 + 27 + + HTTP/1.1 403 Forbidden + + + + /folder1/file3.txt + + + + File3 + 42 + + HTTP/1.1 425 Too Early + + + + /folder1/subfolder + + + + SubFolder + + HTTP/1.1 200 OK + + + + + + HTTP/1.1 404 Not Found + + + +XML; + + $client->response = new Response(207, [], $responseBody); + $result = $client->propFindUnfiltered('folder1', ['{DAV:}resourcetype', '{DAV:}displayname', '{DAV:}contentlength', '{urn:zim}gir'], 1); + + self::assertEquals([ + '/folder1' => [ + 200 => [ + '{DAV:}resourcetype' => new Xml\Property\ResourceType('{DAV:}collection'), + '{DAV:}displayname' => 'Folder1', + ], + 404 => [ + '{DAV:}contentlength' => null, + ], + ], + '/folder1/file1.txt' => [ + 200 => [ + '{DAV:}resourcetype' => null, + '{DAV:}displayname' => 'File1', + '{DAV:}contentlength' => 12, + ], + ], + '/folder1/file2.txt' => [ + 403 => [ + '{DAV:}resourcetype' => null, + '{DAV:}displayname' => 'File2', + '{DAV:}contentlength' => 27, + ], + ], + '/folder1/file3.txt' => [ + 425 => [ + '{DAV:}resourcetype' => null, + '{DAV:}displayname' => 'File3', + '{DAV:}contentlength' => 42, + ], + ], + '/folder1/subfolder' => [ + 200 => [ + '{DAV:}resourcetype' => new Xml\Property\ResourceType('{DAV:}collection'), + '{DAV:}displayname' => 'SubFolder', + ], + 404 => [ + '{DAV:}contentlength' => null, + ], + ], + ], $result); + + $request = $client->request; + self::assertEquals('PROPFIND', $request->getMethod()); + self::assertEquals('/folder1', $request->getUrl()); + self::assertEquals([ + 'Depth' => ['1'], + 'Content-Type' => ['application/xml'], + ], $request->getHeaders()); + } + public function testPropPatch() { $client = new ClientMock([