* * @copyright Copyright (c) 2018, ownCloud GmbH * @license AGPL-3.0 * * This code is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License, version 3, * along with this program. If not, see * */ use Behat\Gherkin\Node\PyStringNode; use Behat\Gherkin\Node\TableNode; use GuzzleHttp\Exception\BadResponseException; use GuzzleHttp\Exception\GuzzleException; use PHPUnit\Framework\Assert; use Psr\Http\Message\ResponseInterface; use GuzzleHttp\Stream\StreamInterface; use TestHelpers\OcisHelper; use TestHelpers\UploadHelper; use TestHelpers\WebDavHelper; use TestHelpers\HttpRequestHelper; use TestHelpers\Asserts\WebDav as WebDavAssert; use TestHelpers\GraphHelper; /** * WebDav functions */ trait WebDav { private string $davPath = "remote.php/webdav"; private bool $usingOldDavPath = true; private bool $usingSpacesDavPath = false; /** * @var ResponseInterface[] */ private array $uploadResponses; /** * @var int|string|null */ private $storedFileID = null; private ?int $lastUploadDeleteTime = null; /** * a variable that contains the DAV path without "remote.php/(web)dav" * when setting $this->davPath directly by usingDavPath() */ private ?string $customDavPath = null; /** * response content parsed from XML to an array */ private array $responseXml = []; /** * add resource created by admin in an array * This array is used while cleaning up the resource created by admin during test run * As of now it tracks only for (files|folder) creation * This can be expanded and modified to track other actions like (upload, deleted.) */ private array $adminResources = []; /** * response content parsed into a SimpleXMLElement */ private ?SimpleXMLElement $responseXmlObject = null; private int $httpRequestTimeout = 0; private ?int $chunkingToUse = null; /** * The ability to do requests with depth infinity is disabled by default. * This remembers when the setting dav.propfind.depth_infinity has been * enabled, so that test code can make use of it as appropriate. */ private bool $davPropfindDepthInfinityEnabled = false; /** * @return void */ public function davPropfindDepthInfinityEnabled():void { $this->davPropfindDepthInfinityEnabled = true; } /** * @return void */ public function davPropfindDepthInfinityDisabled():void { $this->davPropfindDepthInfinityEnabled = false; } /** * @return bool */ public function davPropfindDepthInfinityIsEnabled():bool { return $this->davPropfindDepthInfinityEnabled; } /** * @param int $lastUploadDeleteTime * * @return void */ public function setLastUploadDeleteTime(int $lastUploadDeleteTime):void { $this->lastUploadDeleteTime = $lastUploadDeleteTime; } /** * @return number */ public function getLastUploadDeleteTime():int { return $this->lastUploadDeleteTime; } /** * @return SimpleXMLElement|null */ public function getResponseXmlObject():?SimpleXMLElement { return $this->responseXmlObject; } /** * @param SimpleXMLElement $responseXmlObject * * @return void */ public function setResponseXmlObject(SimpleXMLElement $responseXmlObject):void { $this->responseXmlObject = $responseXmlObject; } /** * @return void */ public function clearResponseXmlObject():void { $this->responseXmlObject = null; } /** * @param string $fileID * * @return void */ public function setStoredFileID(string $fileID):void { $this->storedFileID = $fileID; } /** * @return string */ public function getStoredFileID():string { return $this->storedFileID; } /** * @param SimpleXMLElement|null $xmlObject * * @return string the etag or an empty string if the getetag property does not exist */ public function getEtagFromResponseXmlObject(?SimpleXMLElement $xmlObject = null): string { $xmlObject = $xmlObject ?? $this->getResponseXml(); $xmlPart = $xmlObject->xpath("//d:prop/d:getetag"); if (!\is_array($xmlPart) || (\count($xmlPart) === 0)) { return ''; } return $xmlPart[0]->__toString(); } /** * * @param string $eTag * * @return boolean */ public function isEtagValid(string $eTag): bool { if (\preg_match("/^\"[a-f0-9:.]{1,32}\"$/", $eTag) ) { return true; } else { return false; } } /** * @param array $responseXml * * @return void */ public function setResponseXml(array $responseXml):void { $this->responseXml = $responseXml; } /** * @return string */ public function getOldDavPath():string { return "remote.php/webdav"; } /** * @return string */ public function getNewDavPath():string { return "remote.php/dav"; } /** * @return string */ public function getSpacesDavPath():string { return "dav/spaces"; } /** * @Given /^using (old|new|spaces) (?:dav|DAV) path$/ * * @param string $davChoice * * @return void */ public function usingOldOrNewDavPath(string $davChoice):void { if ($davChoice === 'old') { $this->usingOldDavPath(); } elseif ($davChoice === 'new') { $this->usingNewDavPath(); } else { $this->usingSpacesDavPath(); } } /** * Select the old DAV path as the default for later scenario steps * * @return void */ public function usingOldDavPath():void { $this->davPath = $this->getOldDavPath(); $this->usingOldDavPath = true; $this->customDavPath = null; $this->usingSpacesDavPath = false; } /** * Select the new DAV path as the default for later scenario steps * * @return void */ public function usingNewDavPath():void { $this->davPath = $this->getNewDavPath(); $this->usingOldDavPath = false; $this->customDavPath = null; $this->usingSpacesDavPath = false; } /** * Select the spaces dav path as the default for later scenario steps * * @return void */ public function usingSpacesDavPath():void { $this->davPath = $this->getSpacesDavPath(); $this->usingOldDavPath = false; $this->customDavPath = null; $this->usingSpacesDavPath = true; } /** * gives the DAV path of a file including the subfolder of the webserver * e.g. when the server runs in `http://localhost/owncloud/` * this function will return `owncloud/remote.php/webdav/prueba.txt` * * @param string $user * * @return string * @throws GuzzleException */ public function getFullDavFilesPath(string $user):string { $spaceId = null; if ($this->getDavPathVersion() === WebDavHelper::DAV_VERSION_SPACES) { $spaceId = (WebDavHelper::$SPACE_ID_FROM_OCIS) ?: WebDavHelper::getPersonalSpaceIdForUser( $this->getBaseUrl(), $user, $this->getPasswordForUser($user), $this->getStepLineRef() ); } $path = $this->getBasePath() . "/" . WebDavHelper::getDavPath($user, $this->getDavPathVersion(), "files", $spaceId); $path = WebDavHelper::sanitizeUrl($path); return \ltrim($path, "/"); } /** * @param string $token * @param string $type * * @return string */ public function getPublicLinkDavPath(string $token, string $type):string { $path = $this->getBasePath() . "/" . WebDavHelper::getDavPath($token, $this->getDavPathVersion(), $type); $path = WebDavHelper::sanitizeUrl($path); return \ltrim($path, "/"); } /** * Select a suitable DAV path version number. * Some endpoints have only existed since a certain point in time, so for * those make sure to return a DAV path version that works for that endpoint. * Otherwise return the currently selected DAV path version. * * @param string|null $for the category of endpoint that the DAV path will be used for * * @return int DAV path version (1, 2 or 3) selected, or appropriate for the endpoint */ public function getDavPathVersion(?string $for = null):?int { if ($this->usingSpacesDavPath) { return WebDavHelper::DAV_VERSION_SPACES; } if ($for === 'systemtags') { // systemtags only exists since DAV v2 return WebDavHelper::DAV_VERSION_NEW; } if ($for === 'file_versions') { // file_versions only exists since DAV v2 return WebDavHelper::DAV_VERSION_NEW; } if ($this->usingOldDavPath === true) { return WebDavHelper::DAV_VERSION_OLD; } else { return WebDavHelper::DAV_VERSION_NEW; } } /** * Select a suitable DAV path. * Some endpoints have only existed since a certain point in time, so for * those make sure to return a DAV path that works for that endpoint. * Otherwise return the currently selected DAV path. * * @param string|null $for the category of endpoint that the DAV path will be used for * * @return string DAV path selected, or appropriate for the endpoint */ public function getDavPath(?string $for = null):string { $davPathVersion = $this->getDavPathVersion($for); if ($davPathVersion === WebDavHelper::DAV_VERSION_OLD) { return $this->getOldDavPath(); } if ($davPathVersion === WebDavHelper::DAV_VERSION_NEW) { return $this->getNewDavPath(); } return $this->getSpacesDavPath(); } /** * @param string|null $user * @param string|null $method * @param string|null $path * @param array|null $headers * @param StreamInterface|null $body * @param string|null $type * @param string|null $davPathVersion * @param bool $stream Set to true to stream a response rather * than download it all up-front. * @param string|null $password * @param array|null $urlParameter * @param string|null $doDavRequestAsUser * @param bool|null $isGivenStep * * @return ResponseInterface * * @throws GuzzleException * @throws JsonException */ public function makeDavRequest( ?string $user, ?string $method, ?string $path, ?array $headers, $body = null, ?string $type = "files", ?string $davPathVersion = null, bool $stream = false, ?string $password = null, ?array $urlParameter = [], ?string $doDavRequestAsUser = null, ?bool $isGivenStep = false ):ResponseInterface { $user = $this->getActualUsername($user); if ($this->customDavPath !== null) { $path = $this->customDavPath . $path; } if ($davPathVersion === null) { $davPathVersion = $this->getDavPathVersion(); } else { $davPathVersion = (int) $davPathVersion; } if ($password === null) { $password = $this->getPasswordForUser($user); } return WebDavHelper::makeDavRequest( $this->getBaseUrl(), $user, $password, $method, $path, $headers, $this->getStepLineRef(), $body, $davPathVersion, $type, null, "basic", $stream, $this->httpRequestTimeout, null, $urlParameter, $doDavRequestAsUser, $isGivenStep ); } /** * * @param string $user * @param string $folder * @param bool|null $isGivenStep * @param string|null $password * * @return ResponseInterface * @throws JsonException | GuzzleException * @throws GuzzleException | JsonException */ public function createFolder(string $user, string $folder, ?bool $isGivenStep = false, ?string $password = null): ResponseInterface { $folder = '/' . \ltrim($folder, '/'); return $this->makeDavRequest( $user, "MKCOL", $folder, [], null, "files", null, false, $password, [], null, $isGivenStep ); } /** * @param string $user * @param string|null $path * @param string|null $doDavRequestAsUser * @param string|null $width * @param string|null $height * * @return ResponseInterface * @throws GuzzleException */ public function downloadPreviews(string $user, ?string $path, ?string $doDavRequestAsUser, ?string $width, ?string $height):ResponseInterface { $user = $this->getActualUsername($user); $doDavRequestAsUser = $this->getActualUsername($doDavRequestAsUser); $urlParameter = [ 'x' => $width, 'y' => $height, 'forceIcon' => '0', 'preview' => '1' ]; return $this->makeDavRequest( $user, "GET", $path, [], null, "files", null, false, null, $urlParameter, $doDavRequestAsUser ); } /** * @Then the number of versions should be :arg1 * * @param int $number * * @return void * @throws Exception */ public function theNumberOfVersionsShouldBe(int $number):void { $resXml = $this->getResponseXmlObject(); if ($resXml === null) { $resXml = HttpRequestHelper::getResponseXml( $this->getResponse(), __METHOD__ ); $this->setResponseXmlObject($resXml); } $xmlPart = $resXml->xpath("//d:getlastmodified"); $actualNumber = \count($xmlPart); Assert::assertEquals( $number, $actualNumber, "Expected number of versions was '$number', but got '$actualNumber'" ); } /** * @Then the number of etag elements in the response should be :number * * @param int $number * * @return void * @throws Exception */ public function theNumberOfEtagElementInTheResponseShouldBe(int $number):void { $resXml = $this->getResponseXmlObject(); if ($resXml === null) { $resXml = HttpRequestHelper::getResponseXml( $this->getResponse(), __METHOD__ ); } $xmlPart = $resXml->xpath("//d:getetag"); $actualNumber = \count($xmlPart); Assert::assertEquals( $number, $actualNumber, "Expected number of etag elements was '$number', but got '$actualNumber'" ); } /** * @param string $user * @param string $fileDestination * * @return string * @throws GuzzleException */ public function destinationHeaderValue(string $user, string $fileDestination):string { $fileDestination = \ltrim($fileDestination, "/"); $spaceId = $this->getPersonalSpaceIdForUser($user); // If the destination is a share, we need to get the space ID for the Shares space if (\str_starts_with($fileDestination, "Shares/")) { $spaceId = $this->spacesContext->getSpaceIdByName($user, "Shares"); if ($this->getDavPathVersion() === WebDavHelper::DAV_VERSION_SPACES) { $fileDestination = \preg_replace("/^Shares\//", "", $fileDestination); } } $fullUrl = $this->getBaseUrl() . '/' . WebDavHelper::getDavPath($user, $this->getDavPathVersion(), "files", $spaceId); return \rtrim($fullUrl, '/') . '/' . $fileDestination; } /** * @Given /^user "([^"]*)" has moved (?:file|folder|entry) "([^"]*)" to "([^"]*)"$/ * * @param string|null $user * @param string|null $fileSource * @param string|null $fileDestination * * @return void */ public function userHasMovedFile( ?string $user, ?string $fileSource, ?string $fileDestination ):void { $response = $this->moveResource($user, $fileSource, $fileDestination); $actualStatusCode = $response->getStatusCode(); $this->theHTTPStatusCodeShouldBe( 201, " Failed moving resource '$fileSource' to '$fileDestination'." . " Expected status code was 201 but got '$actualStatusCode' ", $response ); } /** * @param string $user * @param string $source * @param string $destination * * @return ResponseInterface */ public function moveResource(string $user, string $source, string $destination) { $user = $this->getActualUsername($user); $headers['Destination'] = $this->destinationHeaderValue( $user, $destination ); return $this->makeDavRequest( $user, "MOVE", $source, $headers ); } /** * @When user :user moves file :source to :destination using the WebDAV API * @When user :user moves folder :source to :destination using the WebDAV API * * @param string $user * @param string $source * @param string $destination * * @return void * @throws JsonException * @throws GuzzleException */ public function userMovesFileOrFolderUsingTheWebDavAPI( string $user, string $source, string $destination ):void { $response = $this->moveResource($user, $source, $destination); $this->setResponse($response); $this->pushToLastHttpStatusCodesArray(); } /** * @When user :user moves the following file using the WebDAV API * * @param string $user * @param TableNode $table * * @return void * @throws Exception */ public function userMovesTheFollowingFileUsingTheWebdavApi(string $user, TableNode $table):void { $this->verifyTableNodeColumns($table, ["source", "destination"]); $rows = $table->getHash(); foreach ($rows as $row) { $response = $this->moveResource($user, $row["source"], $row["destination"]); $this->setResponse($response); $this->pushToLastHttpStatusCodesArray( (string) $response->getStatusCode() ); } } /** * @When /^user "([^"]*)" moves the following (?:files|folders|entries)\s?(asynchronously|) using the WebDAV API$/ * * @param string $user * @param string $type "asynchronously" or empty * @param TableNode $table * * @return void * @throws Exception */ public function userMovesFollowingFileUsingTheAPI( string $user, string $type, TableNode $table ):void { $this->verifyTableNodeColumns($table, ["from", "to"]); $paths = $table->getHash(); foreach ($paths as $file) { $response = $this->moveResource($user, $file['from'], $file['to']); $this->pushToLastHttpStatusCodesArray( (string) $response->getStatusCode() ); } } /** * @Then /^user "([^"]*)" should be able to rename (file|folder|entry) "([^"]*)" to "([^"]*)"$/ * * @param string $user * @param string $entry * @param string $source * @param string $destination * * @return void * @throws Exception */ public function theUserShouldBeAbleToRenameEntryTo(string $user, string $entry, string $source, string $destination):void { $user = $this->getActualUsername($user); $this->checkFileOrFolderExistsForUser($user, $entry, $source); $response = $this->moveResource($user, $source, $destination); $this->theHTTPStatusCodeShouldBeBetween(201, 204, $response); $this->checkFileOrFolderDoesNotExistsForUser($user, $entry, $source); $this->checkFileOrFolderExistsForUser($user, $entry, $destination); } /** * @Then /^user "([^"]*)" should not be able to rename (file|folder|entry) "([^"]*)" to "([^"]*)"$/ * * @param string $user * @param string $entry * @param string $source * @param string $destination * * @return void * @throws Exception */ public function theUserShouldNotBeAbleToRenameEntryTo(string $user, string $entry, string $source, string $destination):void { $this->checkFileOrFolderExistsForUser($user, $entry, $source); $response = $this->moveResource($user, $source, $destination); $this->theHTTPStatusCodeShouldBeBetween(400, 499, $response); $this->checkFileOrFolderExistsForUser($user, $entry, $source); $this->checkFileOrFolderDoesNotExistsForUser($user, $entry, $destination); } /** * @param string $user * @param string $fileSource * @param string $fileDestination * * @return ResponseInterface */ public function copyFile( string $user, string $fileSource, string $fileDestination ):ResponseInterface { $user = $this->getActualUsername($user); $headers['Destination'] = $this->destinationHeaderValue( $user, $fileDestination ); return $this->makeDavRequest( $user, "COPY", $fileSource, $headers ); } /** * @When /^user "([^"]*)" copies (?:file|folder) "([^"]*)" to "([^"]*)" using the WebDAV API$/ * * @param string $user * @param string $fileSource * @param string $fileDestination * * @return void */ public function userCopiesFileUsingTheAPI( string $user, string $fileSource, string $fileDestination ):void { $response = $this->copyFile($user, $fileSource, $fileDestination); $this->setResponse($response); $this->pushToLastHttpStatusCodesArray(); } /** * @Given /^user "([^"]*)" has copied file "([^"]*)" to "([^"]*)"$/ * * @param string $user * @param string $fileSource * @param string $fileDestination * * @return void */ public function userHasCopiedFileUsingTheAPI( string $user, string $fileSource, string $fileDestination ):void { $response = $this->copyFile($user, $fileSource, $fileDestination); $this->theHTTPStatusCodeShouldBe( ["201", "204"], "HTTP status code was not 201 or 204 while trying to copy file '$fileSource' to '$fileDestination' for user '$user'", $response ); } /** * @param string $user * @param string $fileSource * @param string $range * * @return ResponseInterface */ public function downloadFileWithRange(string $user, string $fileSource, string $range):ResponseInterface { $user = $this->getActualUsername($user); $headers['Range'] = $range; return $this->makeDavRequest( $user, "GET", $fileSource, $headers ); } /** * @When /^user "([^"]*)" downloads file "([^"]*)" with range "([^"]*)" using the WebDAV API$/ * * @param string $user * @param string $fileSource * @param string $range * * @return void */ public function userDownloadsFileWithRangeUsingWebDavApi(string $user, string $fileSource, string $range):void { $this->setResponse($this->downloadFileWithRange($user, $fileSource, $range)); } /** * @Then /^user "([^"]*)" using password "([^"]*)" should not be able to download file "([^"]*)"$/ * * @param string $user * @param string $password * @param string $fileName * * @return void */ public function userUsingPasswordShouldNotBeAbleToDownloadFile( string $user, string $password, string $fileName ):void { $user = $this->getActualUsername($user); $password = $this->getActualPassword($password); $response = $this->downloadFileAsUserUsingPassword($user, $fileName, $password); Assert::assertGreaterThanOrEqual( 400, $response->getStatusCode(), __METHOD__ . ' download must fail' ); Assert::assertLessThanOrEqual( 499, $response->getStatusCode(), __METHOD__ . ' 4xx error expected but got status code "' . $response->getStatusCode() . '"' ); } /** * @Then /^user "([^"]*)" should not be able to download file "([^"]*)"$/ * * @param string $user * @param string $fileName * * @return void * @throws JsonException */ public function userShouldNotBeAbleToDownloadFile( string $user, string $fileName ):void { $user = $this->getActualUsername($user); $password = $this->getPasswordForUser($user); $response = $this->downloadFileAsUserUsingPassword($user, $fileName, $password); Assert::assertGreaterThanOrEqual( 400, $response->getStatusCode(), __METHOD__ . ' download must fail' ); Assert::assertLessThanOrEqual( 499, $response->getStatusCode(), __METHOD__ . ' 4xx error expected but got status code "' . $response->getStatusCode() . '"' ); } /** * @Then /^user "([^"]*)" should be able to access a skeleton file$/ * * @param string $user * * @return void */ public function userShouldBeAbleToAccessASkeletonFile(string $user):void { $user = $this->getActualUsername($user); $response = $this->downloadFileAsUserUsingPassword($user, "textfile0.txt"); $actualStatus = $response->getStatusCode(); Assert::assertEquals( 200, $actualStatus, "Expected status code to be '200', but got '$actualStatus'" ); $this->checkDownloadedContentMatches("ownCloud test text file 0\n", '', $response); } /** * @Then the size of the downloaded file should be :size bytes * * @param string $size * * @return void */ public function sizeOfDownloadedFileShouldBe(string $size):void { $actualSize = \strlen((string) $this->response->getBody()); Assert::assertEquals( $size, $actualSize, "Expected size of the downloaded file was '$size' but got '$actualSize'" ); } /** * @Then /^the downloaded content should end with "([^"]*)"$/ * * @param string $content * * @return void */ public function downloadedContentShouldEndWith(string $content):void { $actualContent = \substr((string) $this->response->getBody(), -\strlen($content)); Assert::assertEquals( $content, $actualContent, "The downloaded content was expected to end with '$content', but actually ended with '$actualContent'." ); } /** * @Then /^the downloaded content should be "([^"]*)"$/ * * @param string $content * * @return void */ public function downloadedContentShouldBe(string $content):void { $this->checkDownloadedContentMatches($content, '', $this->getResponse()); } /** * @param string $expectedContent * @param string $extraErrorText * @param ResponseInterface|null $response * * @return void */ public function checkDownloadedContentMatches( string $expectedContent, string $extraErrorText = "", ?ResponseInterface $response = null ):void { $response = $response ?? $this->response; $actualContent = (string) $response->getBody(); // For this test we really care about the content. // A separate "Then" step can specifically check the HTTP status. // But if the content is wrong (e.g. empty) then it is useful to // report the HTTP status to give some clue what might be the problem. $actualStatus = $response->getStatusCode(); if ($extraErrorText !== "") { $extraErrorText .= "\n"; } Assert::assertEquals( $expectedContent, $actualContent, $extraErrorText . "The content was expected to be '$expectedContent', but actually is '$actualContent'. HTTP status was $actualStatus" ); } /** * @Then the content in the response should match the following content: * * @param PyStringNode $content * * @return void */ public function theContentInTheResponseShouldMatchTheFollowingContent(PyStringNode $content): void { $this->checkDownloadedContentMatches($content->getRaw(), '', $this->getResponse()); } /** * @Then the content in the response should include the following content: * * @param PyStringNode $content * * @return void */ public function theContentInTheResponseShouldIncludeTheFollowingContent(PyStringNode $content): void { Assert::assertStringContainsString( $content->getRaw(), (string) $this->response->getBody() ); } /** * @Then /^if the HTTP status code was "([^"]*)" then the downloaded content for multipart byterange should be:$/ * * @param int $statusCode * @param PyStringNode $content * * @return void * */ public function theDownloadedContentForMultipartByteRangeShouldBe(int $statusCode, PyStringNode $content):void { $actualStatusCode = $this->response->getStatusCode(); if ($actualStatusCode === $statusCode) { $actualContent = (string) $this->response->getBody(); $pattern = ["/--\w*/", "/\s*/m"]; $actualContent = \preg_replace($pattern, "", $actualContent); $content = \preg_replace("/\s*/m", '', $content->getRaw()); Assert::assertEquals( $content, $actualContent, "The downloaded content was expected to be '$content', but actually is '$actualContent'. HTTP status was $actualStatusCode" ); } } /** * @Then /^if the HTTP status code was "([^"]*)" then the downloaded content should be "([^"]*)"$/ * * @param int $statusCode * @param string $content * * @return void */ public function checkStatusCodeForDownloadedContentShouldBe(int $statusCode, string $content):void { $actualStatusCode = $this->response->getStatusCode(); if ($actualStatusCode === $statusCode) { $this->checkDownloadedContentMatches($content, '', $this->getResponse()); } } /** * @Then the content of file :fileName for user :user should be :content * * @param string $fileName * @param string $user * @param string $content * * @return void */ public function contentOfFileForUserShouldBe(string $fileName, string $user, string $content):void { $user = $this->getActualUsername($user); $response = $this->downloadFileAsUserUsingPassword($user, $fileName); $actualStatus = $response->getStatusCode(); if ($actualStatus !== 200) { throw new Exception( "Expected status code to be '200', but got '$actualStatus'" ); } $this->checkDownloadedContentMatches($content, '', $response); } /** * @Then /^the content of the following files for user "([^"]*)" should be "([^"]*)"$/ * * @param string $user * @param string $content * @param TableNode $table * * @return void * @throws Exception */ public function contentOfFollowingFilesShouldBe(string $user, string $content, TableNode $table):void { $this->verifyTableNodeColumns($table, ["path"]); $paths = $table->getHash(); $user = $this->getActualUsername($user); foreach ($paths as $file) { $response = $this->downloadFileAsUserUsingPassword($user, $file["path"]); $actualStatus = $response->getStatusCode(); Assert::assertEquals( 200, $actualStatus, "Expected status code to be '200', but got '$actualStatus'" ); $this->checkDownloadedContentMatches($content, '', $response); } } /** * @Then /^the content of file "([^"]*)" for user "([^"]*)" using password "([^"]*)" should be "([^"]*)"$/ * * @param string $fileName * @param string $user * @param string|null $password * @param string $content * * @return void */ public function contentOfFileForUserUsingPasswordShouldBe( string $fileName, string $user, ?string $password, string $content ):void { $user = $this->getActualUsername($user); $password = $this->getActualPassword($password); $response = $this->downloadFileAsUserUsingPassword($user, $fileName, $password); $this->checkDownloadedContentMatches($content, '', $response); } /** * @Then /^the content of file "([^"]*)" for user "([^"]*)" should be:$/ * * @param string $fileName * @param string $user * @param PyStringNode $content * * @return void */ public function contentOfFileForUserShouldBePyString( string $fileName, string $user, PyStringNode $content ):void { $user = $this->getActualUsername($user); $response = $this->downloadFileAsUserUsingPassword($user, $fileName); $actualStatus = $response->getStatusCode(); Assert::assertEquals( 200, $actualStatus, "Expected status code to be '200', but got '$actualStatus'" ); $this->checkDownloadedContentMatches($content->getRaw(), '', $response); } /** * @When user :user downloads file :fileName using the WebDAV API * @When user :user tries to download file :fileName using the WebDAV API * * @param string $user * @param string $fileName * * @return void */ public function userDownloadsFileUsingTheAPI( string $user, string $fileName ):void { $this->setResponse($this->downloadFileAsUserUsingPassword($user, $fileName)); } /** * @param string $user * @param string $fileName * @param string|null $password * @param array|null $headers * * @return ResponseInterface */ public function downloadFileAsUserUsingPassword( string $user, string $fileName, ?string $password = null, ?array $headers = [] ):ResponseInterface { $user = $this->getActualUsername($user); $password = $this->getActualPassword($password); return $this->makeDavRequest( $user, 'GET', $fileName, $headers, null, "files", null, false, $password ); } /** * @When the public gets the size of the last shared public link using the WebDAV API * * @return void * @throws Exception */ public function publicGetsSizeOfLastSharedPublicLinkUsingTheWebdavApi():void { $token = ($this->isUsingSharingNG()) ? $this->shareNgGetLastCreatedLinkShareToken() : $this->getLastCreatedPublicShareToken(); $url = $this->getBaseUrl() . "/remote.php/dav/public-files/$token"; $this->response = HttpRequestHelper::sendRequest( $url, $this->getStepLineRef(), "PROPFIND" ); } /** * @When user :user gets the size of file :resource using the WebDAV API * * @param string $user * @param string $resource * * @return void * @throws Exception */ public function userGetsSizeOfFileUsingTheWebdavApi(string $user, string $resource):void { $user = $this->getActualUsername($user); $password = $this->getPasswordForUser($user); $this->response = WebDavHelper::propfind( $this->getBaseUrl(), $user, $password, $resource, [], $this->getStepLineRef(), "0", "files", $this->getDavPathVersion() ); } /** * @Then the size of the file should be :size * * @param string $size * * @return void * @throws Exception */ public function theSizeOfTheFileShouldBe(string $size):void { $responseXml = HttpRequestHelper::getResponseXml( $this->response, __METHOD__ ); $xmlPart = $responseXml->xpath("//d:prop/d:getcontentlength"); $actualSize = (string) $xmlPart[0]; Assert::assertEquals( $size, $actualSize, __METHOD__ . " Expected size of the file was '$size', but got '$actualSize' instead." ); } /** * @Then /^the content of file "([^"]*)" for user "([^"]*)" should be "([^"]*)" plus end-of-line$/ * * @param string $fileName * @param string $user * @param string $content * * @return void */ public function contentOfFileForUserShouldBePlusEndOfLine(string $fileName, string $user, string $content):void { $user = $this->getActualUsername($user); $response = $this->downloadFileAsUserUsingPassword($user, $fileName); $actualStatus = $response->getStatusCode(); Assert::assertEquals( 200, $actualStatus, "Expected status code to be '200', but got '$actualStatus'" ); $this->checkDownloadedContentMatches("$content\n", '', $response); } /** * @Then the following headers should be set * * @param TableNode $table * * @return void * @throws Exception */ public function theFollowingHeadersShouldBeSet(TableNode $table):void { $this->verifyTableNodeColumns( $table, ['header', 'value'] ); foreach ($table->getColumnsHash() as $header) { $headerName = $header['header']; $expectedHeaderValue = $header['value']; $returnedHeader = $this->response->getHeader($headerName); $expectedHeaderValue = $this->substituteInLineCodes($expectedHeaderValue); if (empty($returnedHeader)) { throw new Exception( \sprintf( "Missing expected header '%s'", $headerName ) ); } $headerValue = $returnedHeader[0]; Assert::assertEquals( $expectedHeaderValue, $headerValue, __METHOD__ . " Expected value for header '$headerName' was '$expectedHeaderValue', but got '$headerValue' instead." ); } } /** * @Then as :user :entry :path should not exist * * @param string $user * @param string $entry * @param string $path * * @return ResponseInterface * @throws Exception */ public function asFileOrFolderShouldNotExist( string $user, string $entry, string $path ):void { $this->checkFileOrFolderDoesNotExistsForUser($user, $entry, $path); } /** * @param string $user * @param string $entry * @param string|null $path * @param string $type * * @return void */ public function checkFileOrFolderDoesNotExistsForUser( string $user, string $entry = "file", ?string $path = null, string $type = "files" ):void { $user = $this->getActualUsername($user); $path = $this->substituteInLineCodes($path); $response = $this->listFolder( $user, $path, '0', null, $type ); $statusCode = $response->getStatusCode(); if ($statusCode < 400 || $statusCode > 499) { try { $responseXml = HttpRequestHelper::getResponseXml( $response, __METHOD__ ); } catch (Exception $e) { Assert::fail( "$entry '$path' should not exist. But API returned $statusCode without XML in the body" ); } Assert::assertTrue( $this->isEtagValid($this->getEtagFromResponseXmlObject($responseXml)), "$entry '$path' should not exist. But API returned $statusCode without an etag in the body" ); $isCollection = $responseXml->xpath("//d:prop/d:resourcetype/d:collection"); if (\count($isCollection) === 0) { $actualResourceType = "file"; } else { $actualResourceType = "folder"; } if ($entry === $actualResourceType) { Assert::fail( "$entry '$path' should not exist. But it does." ); } } } /** * @Then /^as "([^"]*)" the following (files|folders) should not exist$/ * * @param string $user * @param string $entry * @param TableNode $table * * @return void * @throws Exception */ public function followingFilesShouldNotExist( string $user, string $entry, TableNode $table ):void { $this->verifyTableNodeColumns($table, ["path"]); $paths = $table->getHash(); $entry = \rtrim($entry, "s"); foreach ($paths as $file) { $this->checkFileOrFolderDoesNotExistsForUser($user, $entry, $file["path"]); } } /** * @Then as :user :entry :path should exist * * @param string $user * @param string $entry * @param string $path * * @return void * @throws Exception */ public function asFileOrFolderShouldExist( string $user, string $entry, string $path ):void { $this->checkFileOrFolderExistsForUser($user, $entry, $path); } /** * @param string $user * @param string $entry * @param string $path * @param string|null $type * * @return void */ public function checkFileOrFolderExistsForUser( string $user, string $entry, string $path, ?string $type = "files" ):void { $user = $this->getActualUsername($user); $path = $this->substituteInLineCodes($path); $responseXml = $this->listFolderAndReturnResponseXml( $user, $path, '0', null, $type ); Assert::assertTrue( $this->isEtagValid($this->getEtagFromResponseXmlObject($responseXml)), "$entry '$path' expected to exist for user $user but not found" ); $isCollection = $responseXml->xpath("//d:prop/d:resourcetype/d:collection"); if ($entry === "folder") { Assert::assertEquals(\count($isCollection), 1, "Unexpectedly, `$path` is not a folder"); } elseif ($entry === "file") { Assert::assertEquals(\count($isCollection), 0, "Unexpectedly, `$path` is not a file"); } } /** * @Then /^as "([^"]*)" the following (files|folders) should exist$/ * * @param string $user * @param string $entry * @param TableNode $table * * @return void * @throws Exception */ public function followingFilesOrFoldersShouldExist( string $user, string $entry, TableNode $table ):void { $this->verifyTableNodeColumns($table, ["path"]); $paths = $table->getHash(); $entry = \rtrim($entry, "s"); foreach ($paths as $file) { $this->checkFileOrFolderExistsForUser($user, $entry, $file["path"]); } } /** * * @param string $user * @param string $entry * @param string $path * @param string $type * * @return bool */ public function fileOrFolderExists( string $user, string $entry, string $path, string $type = "files" ):bool { try { $this->checkFileOrFolderExistsForUser($user, $entry, $path, $type); return true; } catch (Exception $e) { return false; } } /** * * @param string $user * @param string $path * @param string $folderDepth requires 1 to see elements without children * @param array|null $properties * @param string $type * * @return ResponseInterface * @throws Exception */ public function listFolder( string $user, string $path, string $folderDepth, ?array $properties = null, string $type = "files" ):ResponseInterface { if ($this->customDavPath !== null) { $path = $this->customDavPath . $path; } return WebDavHelper::listFolder( $this->getBaseUrl(), $this->getActualUsername($user), $this->getPasswordForUser($user), $path, $folderDepth, $this->getStepLineRef(), $properties, $type, $this->getDavPathVersion() ); } /** * * @param string $user * @param string $path * @param string $folderDepth requires 1 to see elements without children * @param array|null $properties * @param string $type * * @return SimpleXMLElement * @throws Exception */ public function listFolderAndReturnResponseXml( string $user, string $path, string $folderDepth, ?array $properties = null, string $type = "files" ):SimpleXMLElement { return HttpRequestHelper::getResponseXml( $this->listFolder( $user, $path, $folderDepth, $properties, $type ), __METHOD__ ); } /** * @Then /^user "([^"]*)" should (not|)\s?see the following elements$/ * * @param string $user * @param string $shouldOrNot * @param TableNode $elements * * @return void * @throws InvalidArgumentException|Exception * */ public function userShouldSeeTheElements(string $user, string $shouldOrNot, TableNode $elements):void { $should = ($shouldOrNot !== "not"); $this->checkElementList($user, $elements, $should); } /** * asserts that the user can or cannot see a list of files/folders by propfind * * @param string $user * @param TableNode $elements * @param boolean $expectedToBeListed * * @return void * @throws InvalidArgumentException * @throws Exception * */ public function checkElementList( string $user, TableNode $elements, bool $expectedToBeListed = true ):void { $user = $this->getActualUsername($user); $this->verifyTableNodeColumnsCount($elements, 1); $elementRows = $elements->getRows(); $elementsSimplified = $this->simplifyArray($elementRows); if ($this->davPropfindDepthInfinityIsEnabled()) { // get a full "infinite" list of the user's root folder in one request // and process that to check the elements (resources) $responseXmlObject = $this->listFolderAndReturnResponseXml( $user, "/", "infinity" ); foreach ($elementsSimplified as $expectedElement) { // Allow the table of expected elements to have entries that do // not have to specify the "implied" leading slash, or have multiple // leading slashes, to make scenario outlines more flexible $expectedElement = $this->encodePath($expectedElement); $expectedElement = "/" . \ltrim($expectedElement, "/"); $webdavPath = "/" . $this->getFullDavFilesPath($user) . $expectedElement; $element = $responseXmlObject->xpath( "//d:response/d:href[text() = \"$webdavPath\"]" ); if ($expectedToBeListed && (!isset($element[0]) || urldecode($element[0]->__toString()) !== urldecode($webdavPath)) ) { Assert::fail( "$webdavPath is not in propfind answer but should be" ); } elseif (!$expectedToBeListed && isset($element[0]) ) { Assert::fail( "$webdavPath is in propfind answer but should not be" ); } } } else { // do a PROPFIND for each element foreach ($elementsSimplified as $elementToRequest) { // Allow the table of expected elements to have entries that do // not have to specify the "implied" leading slash, or have multiple // leading slashes, to make scenario outlines more flexible $elementToRequest = "/" . \ltrim($elementToRequest, "/"); // Note: in the request we ask to do a PROPFIND on a resource like: // /some-folder with spaces/sub-folder // but the response has encoded values for the special characters like: // /some-folder%20with%20spaces/sub-folder // So we need both $elementToRequest and $expectedElement $expectedElement = $this->encodePath($elementToRequest); $responseXmlObject = $this->listFolderAndReturnResponseXml( $user, $elementToRequest, "1" ); // TODO: make it work for folder entries // Doesn't work for folder entries // as the folder entry has trailing '/' in d:href $webdavPath = "/" . $this->getFullDavFilesPath($user) . $expectedElement; $element = $responseXmlObject->xpath( "//d:response/d:href[text() = \"$webdavPath\"]" ); if ($expectedToBeListed && (!isset($element[0]) || urldecode($element[0]->__toString()) !== urldecode($webdavPath)) ) { Assert::fail( "$webdavPath is not in propfind answer but should be" ); } elseif (!$expectedToBeListed && isset($element[0]) ) { Assert::fail( "$webdavPath is in propfind answer but should not be" ); } } } } /** * @param string $user * @param string $source * @param string $destination * @param bool|null $isGivenStep * * @return ResponseInterface */ public function uploadFile( string $user, string $source, string $destination, ?bool $isGivenStep = false ):ResponseInterface { $user = $this->getActualUsername($user); $file = \fopen($this->acceptanceTestsDirLocation() . $source, 'r'); $this->pauseUploadDelete(); $response = $this->makeDavRequest( $user, "PUT", $destination, [], $file, "files", null, false, null, [], null, $isGivenStep ); $this->lastUploadDeleteTime = \time(); return $response; } /** * @When user :user uploads file :source to :destination using the WebDAV API * * @param string $user * @param string $source * @param string $destination * * @return void */ public function userUploadsAFileToUsingWebDavApi( string $user, string $source, string $destination ):void { $response = $this->uploadFile($user, $source, $destination); $this->setResponse($response); $this->setResponseXml( HttpRequestHelper::parseResponseAsXml($response) ); $this->pushToLastHttpStatusCodesArray( (string) $this->getResponse()->getStatusCode() ); } /** * @Given user :user has uploaded file :source to :destination * * @param string $user * @param string $source * @param string $destination * * @return array */ public function userHasUploadedAFileTo(string $user, string $source, string $destination):array { $response = $this->uploadFile($user, $source, $destination, true); $this->theHTTPStatusCodeShouldBe( ["201", "204"], "HTTP status code was not 201 or 204 while trying to upload file '$source' to '$destination' for user '$user'", $response ); return $response->getHeader('oc-fileid'); } /** * Upload file as a user with different headers * * @param string $user * @param string $source * @param string $destination * @param array|null $headers * @param int|null $noOfChunks Only use for chunked upload when $this->chunkingToUse is not null * * @return void * @throws Exception */ public function uploadFileWithHeaders( string $user, string $source, string $destination, ?array $headers = [], ?int $noOfChunks = 0 ):void { $chunkingVersion = $this->chunkingToUse; if ($noOfChunks <= 0) { $chunkingVersion = null; } try { $this->responseXml = []; $this->pauseUploadDelete(); $this->response = UploadHelper::upload( $this->getBaseUrl(), $this->getActualUsername($user), $this->getUserPassword($user), $source, $destination, $this->getStepLineRef(), $headers, $this->getDavPathVersion(), $chunkingVersion, $noOfChunks ); $this->lastUploadDeleteTime = \time(); } catch (BadResponseException $e) { // 4xx and 5xx responses cause an exception $this->response = $e->getResponse(); } } /** * @param string $user * @param string $source * @param string $destination * @param integer $noOfChunks * @param string|null $chunkingVersion * @param boolean $async * @param array|null $headers * * @return void */ public function userUploadsAFileInChunk( string $user, string $source, string $destination, int $noOfChunks = 2, ?string $chunkingVersion = null, bool $async = false, ?array $headers = [] ):void { $user = $this->getActualUsername($user); Assert::assertGreaterThan( 0, $noOfChunks, "What does it mean to have $noOfChunks chunks?" ); //use the chunking version that works with the set DAV version if ($chunkingVersion === null) { if ($this->usingOldDavPath || $this->usingSpacesDavPath) { $chunkingVersion = "v1"; } else { $chunkingVersion = "v2"; } } $this->useSpecificChunking($chunkingVersion); Assert::assertTrue( WebDavHelper::isValidDavChunkingCombination( $this->getDavPathVersion(), $this->chunkingToUse ), "invalid chunking/webdav version combination" ); if ($async === true) { $headers['OC-LazyOps'] = 'true'; } $this->uploadFileWithHeaders( $user, $this->acceptanceTestsDirLocation() . $source, $destination, $headers, $noOfChunks ); $this->pushToLastStatusCodesArrays(); } /** * sets the chunking version from human-readable format * * @param string $version (no|v1|v2|new|old) * * @return void */ public function useSpecificChunking(string $version):void { if ($version === "v1" || $version === "old") { $this->chunkingToUse = 1; } elseif ($version === "v2" || $version === "new") { $this->chunkingToUse = 2; } elseif ($version === "no") { $this->chunkingToUse = null; } else { throw new InvalidArgumentException( "cannot set chunking version to $version" ); } } /** * Uploading with old/new DAV and chunked/non-chunked. * Except do not do the new-DAV-new-chunking combination. That is not being * supported on all implementations. * * @When user :user uploads file :source to filenames based on :destination with all mechanisms except new chunking using the WebDAV API * * @param string $user * @param string $source * @param string $destination * * @return void * @throws Exception */ public function userUploadsAFileToWithAllMechanismsExceptNewChunking( string $user, string $source, string $destination ):void { $user = $this->getActualUsername($user); $this->uploadResponses = UploadHelper::uploadWithAllMechanisms( $this->getBaseUrl(), $this->getActualUsername($user), $this->getUserPassword($user), $this->acceptanceTestsDirLocation() . $source, $destination, $this->getStepLineRef(), false, 'new' ); } /** * @When /^user "([^"]*)" uploads file "([^"]*)" to "([^"]*)" in (\d+) chunks (?:with (new|old|v1|v2) chunking and)?\s?using the WebDAV API$/ * * @param string $user * @param string $source * @param string $destination * @param int $noOfChunks * @param string|null $chunkingVersion old|v1|new|v2 null for autodetect * * @return void * @throws Exception */ public function userUploadsAFileToWithChunks( string $user, string $source, string $destination, int $noOfChunks = 2, ?string $chunkingVersion = null ):void { $this->userUploadsAFileInChunk($user, $source, $destination, $noOfChunks, $chunkingVersion); } /** * @Then /^the HTTP status code of all upload responses should be "([^"]*)"$/ * * @param int $statusCode * * @return void */ public function theHTTPStatusCodeOfAllUploadResponsesShouldBe(int $statusCode):void { foreach ($this->uploadResponses as $response) { Assert::assertEquals( $statusCode, $response->getStatusCode(), 'Response did not return expected status code' ); } } /** * @Then the HTTP status code of responses on all endpoints should be :statusCode * * @param int $statusCode * * @return void * @throws Exception */ public function theHTTPStatusCodeOfResponsesOnAllEndpointsShouldBe(int $statusCode):void { $duplicateRemovedStatusCodes = \array_unique($this->lastHttpStatusCodesArray); if (\count($duplicateRemovedStatusCodes) === 1) { Assert::assertSame( $statusCode, \intval($duplicateRemovedStatusCodes[0]), 'Responses did not return expected http status code' ); $this->emptyLastHTTPStatusCodesArray(); } else { throw new Exception( 'Expected same but found different http status codes of last requested responses.' . 'Found status codes: ' . \implode(',', $this->lastHttpStatusCodesArray) ); } } /** * @param string $statusCodes a comma-separated string of expected HTTP status codes * * @return void * @throws Exception */ public function checkTheHTTPStatusCodeOfResponsesOnEachEndpoint(string $statusCodes):void { $expectedStatusCodes = \explode(',', $statusCodes); $actualStatusCodes = $this->lastHttpStatusCodesArray; $count = \count($expectedStatusCodes); $statusCodesAreAllOk = true; if ($count === \count($actualStatusCodes)) { for ($i = 0; $i < $count; $i++) { $expectedCode = (int)\trim($expectedStatusCodes[$i]); $actualCode = (int)$actualStatusCodes[$i]; if ($expectedCode !== $actualCode) { $statusCodesAreAllOk = false; } } } else { $statusCodesAreAllOk = false; } $this->emptyLastHTTPStatusCodesArray(); Assert::assertTrue( $statusCodesAreAllOk, 'Expected HTTP status codes: "' . $statusCodes . '". Found HTTP status codes: "' . \implode(',', $actualStatusCodes) . '"' ); } /** * @Then the HTTP status code of responses on each endpoint should be :statusCodes respectively * * @param string $statusCodes a comma-separated string of expected HTTP status codes * * @return void * @throws Exception */ public function theHTTPStatusCodeOfResponsesOnEachEndpointShouldBe(string $statusCodes):void { $this->checkTheHTTPStatusCodeOfResponsesOnEachEndpoint($statusCodes); } /** * @Then the HTTP status code of responses on each endpoint should be :ocisStatusCodes on oCIS or :revaStatusCodes on reva * * @param string $ocisStatusCodes a comma-separated string of expected HTTP status codes when running on oCIS * @param string $revaStatusCodes a comma-separated string of expected HTTP status codes when running on reva * * @return void * @throws Exception */ public function theHTTPStatusCodeOfResponsesOnEachEndpointShouldBeOcisReva(string $ocisStatusCodes, string $revaStatusCodes):void { if (OcisHelper::isTestingOnReva()) { $expectedStatusCodes = $revaStatusCodes; } else { $expectedStatusCodes = $ocisStatusCodes; } $this->checkTheHTTPStatusCodeOfResponsesOnEachEndpoint($expectedStatusCodes); } /** * @Then the OCS status code of responses on each endpoint should be :statusCode respectively * * @param string $statusCodes * * @return void * @throws Exception */ public function theOCStatusCodeOfResponsesOnEachEndpointShouldBe(string $statusCodes):void { $statusCodes = \explode(',', $statusCodes); $count = \count($statusCodes); if ($count === \count($this->lastOCSStatusCodesArray)) { for ($i = 0; $i < $count; $i++) { Assert::assertSame( (int)\trim($statusCodes[$i]), (int)$this->lastOCSStatusCodesArray[$i], 'Responses did not return expected OCS status code' ); } } else { throw new Exception( 'Expected OCS status codes: "' . \implode(',', $statusCodes) . '". Found OCS status codes: "' . \implode(',', $this->lastOCSStatusCodesArray) . '"' ); } } /** * @Then the OCS status code of responses on all endpoints should be :statusCode * * @param string $statusCode * * @return void * @throws Exception */ public function theOCSStatusCodeOfResponsesOnAllEndpointsShouldBe(string $statusCode):void { $duplicateRemovedStatusCodes = \array_unique($this->lastOCSStatusCodesArray); if (\count($duplicateRemovedStatusCodes) === 1) { Assert::assertSame( \intval($statusCode), \intval($duplicateRemovedStatusCodes[0]), 'Responses did not return expected ocs status code' ); $this->emptyLastOCSStatusCodesArray(); } else { throw new Exception( 'Expected same but found different ocs status codes of last requested responses.' . 'Found status codes: ' . \implode(',', $this->lastOCSStatusCodesArray) ); } } /** * @Then user :user should be able to upload file :source to :destination * * @param string $user * @param string $source * @param string $destination * * @return void * @throws Exception */ public function userShouldBeAbleToUploadFileTo(string $user, string $source, string $destination):void { $user = $this->getActualUsername($user); $response = $this->uploadFile($user, $source, $destination); $this->theHTTPStatusCodeShouldBe( ["201", "204"], "HTTP status code was not 201 or 204 while trying to upload file '$destination'", $response ); $this->checkFileOrFolderExistsForUser($user, "file", $destination); } /** * @Then user :user should not be able to upload file :source to :destination * * @param string $user * @param string $source * @param string $destination * * @return void * @throws Exception */ public function theUserShouldNotBeAbleToUploadFileTo(string $user, string $source, string $destination):void { $fileAlreadyExists = $this->fileOrFolderExists($user, "file", $destination); if ($fileAlreadyExists) { $response = $this->downloadFileAsUserUsingPassword($user, $destination); $initialContent = (string) $response->getBody(); } $response = $this->uploadFile($user, $source, $destination); $this->theHTTPStatusCodeShouldBe(["403", "423"], "", $response); if ($fileAlreadyExists) { $response = $this->downloadFileAsUserUsingPassword($user, $destination); $currentContent = (string) $response->getBody(); Assert::assertSame( $initialContent, $currentContent, __METHOD__ . " user $user was unexpectedly able to upload $source to $destination - the content has changed:" ); } else { $this->checkFileOrFolderDoesNotExistsForUser($user, "file", $destination); } } /** * @Then /^the HTTP status code of all upload responses should be between "(\d+)" and "(\d+)"$/ * * @param int $minStatusCode * @param int $maxStatusCode * * @return void */ public function theHTTPStatusCodeOfAllUploadResponsesShouldBeBetween( int $minStatusCode, int $maxStatusCode ):void { foreach ($this->uploadResponses as $response) { Assert::assertGreaterThanOrEqual( $minStatusCode, $response->getStatusCode(), 'Response did not return expected status code' ); Assert::assertLessThanOrEqual( $maxStatusCode, $response->getStatusCode(), 'Response did not return expected status code' ); } } /** * @param string $user * @param string $destination * @param string $shouldOrNot * @param string|null $exceptChunkingType * * @return void */ public function checkIfFilesExist( string $user, string $destination, string $shouldOrNot, ?string $exceptChunkingType = '' ):void { switch ($exceptChunkingType) { case 'old': $exceptChunkingSuffix = 'olddav-oldchunking'; break; case 'new': $exceptChunkingSuffix = 'newdav-newchunking'; break; default: $exceptChunkingSuffix = ''; break; } if ($shouldOrNot !== "not") { foreach (['old', 'new'] as $davVersion) { foreach (["{$davVersion}dav-regular", "{$davVersion}dav-{$davVersion}chunking"] as $suffix) { if ($suffix !== $exceptChunkingSuffix) { $this->checkFileOrFolderExistsForUser( $user, 'file', "$destination-$suffix" ); } } } } else { foreach (['old', 'new'] as $davVersion) { foreach (["{$davVersion}dav-regular", "{$davVersion}dav-{$davVersion}chunking"] as $suffix) { if ($suffix !== $exceptChunkingSuffix) { $this->checkFileOrFolderDoesNotExistsForUser( $user, 'file', "$destination-$suffix" ); } } } } } /** * @Given user :user has uploaded file :destination of size :bytes bytes * * @param string $user * @param string $destination * @param string $bytes * * @return void * @throws Exception */ public function userHasUploadedFileToOfSizeBytes(string $user, string $destination, string $bytes):void { $user = $this->getActualUsername($user); $filename = "filespecificSize.txt"; $this->createLocalFileOfSpecificSize($filename, $bytes, 'a'); Assert::assertFileExists($this->workStorageDirLocation() . $filename); $response = $this->uploadFile($user, $this->temporaryStorageSubfolderName() . "/$filename", $destination); $expectedElements = new TableNode([["$destination"]]); $this->checkElementList($user, $expectedElements); $this->theHTTPStatusCodeShouldBe([201, 204], '', $response); } /** * @Given user :user has uploaded file :destination ending with :text of size :bytes bytes * * @param string $user * @param string $destination * @param string $text * @param string $bytes * * @return void * @throws Exception */ public function userHasUploadedFileToEndingWithOfSizeBytes(string $user, string $destination, string $text, string $bytes):void { $filename = "filespecificSize.txt"; $this->createLocalFileOfSpecificSize($filename, $bytes, $text); Assert::assertFileExists($this->workStorageDirLocation() . $filename); $response = $this->uploadFile($user, $this->temporaryStorageSubfolderName() . "/$filename", $destination); $this->theHTTPStatusCodeShouldBeBetween(200, 299, $response); $this->removeFile($this->workStorageDirLocation(), $filename); $expectedElements = new TableNode([["$destination"]]); $this->checkElementList($user, $expectedElements); } /** * @param string $user * @param string|null $content * @param string $destination * @param bool|null $isGivenStep * * @return ResponseInterface * @throws JsonException * @throws GuzzleException */ public function uploadFileWithContent( string $user, ?string $content, string $destination, ?bool $isGivenStep = false ): ResponseInterface { $user = $this->getActualUsername($user); $this->pauseUploadDelete(); $response = $this->makeDavRequest( $user, "PUT", $destination, [], $content, "files", null, false, null, [], null, $isGivenStep ); $this->lastUploadDeleteTime = \time(); return $response; } /** * @When user :user uploads file with content :content to :destination using the WebDAV API * * @param string $user * @param string|null $content * @param string $destination * * @return void * @throws GuzzleException * @throws JsonException */ public function userUploadsAFileWithContentTo( string $user, ?string $content, string $destination ):void { $response = $this->uploadFileWithContent($user, $content, $destination); $this->setResponse($response); $this->pushToLastHttpStatusCodesArray(); } /** * @When /^user "([^"]*)" uploads the following files with content "([^"]*)"$/ * * @param string $user * @param string|null $content * @param TableNode $table * * @return void * @throws Exception|GuzzleException */ public function userUploadsFollowingFilesWithContentTo( string $user, ?string $content, TableNode $table ):void { $this->verifyTableNodeColumns($table, ["path"]); $paths = $table->getHash(); foreach ($paths as $destination) { $response = $this->uploadFileWithContent($user, $content, $destination["path"]); $this->setResponse($response); $this->pushToLastStatusCodesArrays(); } } /** * @When user :user uploads file :source to :destination with mtime :mtime using the WebDAV API * * @param string $user * @param string $source * @param string $destination * @param string $mtime Time in human-readable format is taken as input which is converted into milliseconds that is used by API * @param bool|null $isGivenStep * * @return void * @throws Exception */ public function userUploadsFileToWithMtimeUsingTheWebdavApi( string $user, string $source, string $destination, string $mtime, ?bool $isGivenStep = false ):void { $mtime = new DateTime($mtime); $mtime = $mtime->format('U'); $user = $this->getActualUsername($user); $this->response = UploadHelper::upload( $this->getBaseUrl(), $user, $this->getPasswordForUser($user), $this->acceptanceTestsDirLocation() . $source, $destination, $this->getStepLineRef(), ["X-OC-Mtime" => $mtime], $this->getDavPathVersion(), null, 1, $isGivenStep ); } /** * @Given user :user has uploaded file :source to :destination with mtime :mtime using the WebDAV API * * @param string $user * @param string $source * @param string $destination * @param string $mtime Time in human-readable format is taken as input which is converted into milliseconds that is used by API * * @return void * @throws Exception */ public function userHasUploadedFileToWithMtimeUsingTheWebdavApi( string $user, string $source, string $destination, string $mtime ):void { $mtime = new DateTime($mtime); $mtime = $mtime->format('U'); $user = $this->getActualUsername($user); $response = UploadHelper::upload( $this->getBaseUrl(), $user, $this->getPasswordForUser($user), $this->acceptanceTestsDirLocation() . $source, $destination, $this->getStepLineRef(), ["X-OC-Mtime" => $mtime], $this->getDavPathVersion(), null, 1, true ); $this->theHTTPStatusCodeShouldBe( ["201", "204"], "", $response ); } /** * @Then as :user the mtime of the file :resource should be :mtime * * @param string $user * @param string $resource * @param string $mtime * * @return void * @throws Exception */ public function theMtimeOfTheFileShouldBe( string $user, string $resource, string $mtime ):void { $user = $this->getActualUsername($user); $password = $this->getPasswordForUser($user); $baseUrl = $this->getBaseUrl(); $mtime = new DateTime($mtime); $mtime = $mtime->format('U'); Assert::assertEquals( $mtime, WebDavHelper::getMtimeOfResource( $user, $password, $baseUrl, $resource, $this->getStepLineRef(), $this->getDavPathVersion() ) ); } /** * @Given user :user has uploaded file with content :content to :destination * * @param string $user * @param string|null $content * @param string $destination * * @return void * @throws GuzzleException * @throws JsonException */ public function userHasUploadedAFileWithContentTo( string $user, ?string $content, string $destination ):array { $user = $this->getActualUsername($user); $response = $this->uploadFileWithContent($user, $content, $destination, true); $this->theHTTPStatusCodeShouldBe( ["201", "204"], "HTTP status code was not 201 or 204 while trying to upload file '$destination' for user '$user'", $response ); return $response->getHeader('oc-fileid'); } /** * @Given /^user "([^"]*)" has uploaded the following files with content "([^"]*)"$/ * * @param string $user * @param string|null $content * @param TableNode $table * * @return void * @throws Exception|GuzzleException */ public function userHasUploadedFollowingFilesWithContent( string $user, ?string $content, TableNode $table ):void { $this->verifyTableNodeColumns($table, ["path"]); $files = $table->getHash(); foreach ($files as $destination) { $destination = $destination['path']; $user = $this->getActualUsername($user); $response = $this->uploadFileWithContent($user, $content, $destination, true); $this->theHTTPStatusCodeShouldBe( ["201", "204"], "HTTP status code was not 201 or 204 while trying to upload file '$destination' for user '$user'", $response ); } } /** * @When /^user "([^"]*)" downloads the following files using the WebDAV API$/ * * @param string $user * @param TableNode $table * * @return void * @throws Exception */ public function userDownloadsFollowingFiles( string $user, TableNode $table ):void { $this->verifyTableNodeColumns($table, ["path"]); $files = $table->getHash(); $this->emptyLastHTTPStatusCodesArray(); foreach ($files as $fileName) { $response = $this->downloadFileAsUserUsingPassword($user, $fileName["path"]); $this->setResponse($response); $this->pushToLastStatusCodesArrays(); } } /** * @When user :user uploads a file with content :content and mtime :mtime to :destination using the WebDAV API * * @param string $user * @param string|null $content * @param string $mtime * @param string $destination * * @return void * @throws Exception */ public function userUploadsAFileWithContentAndMtimeTo( string $user, ?string $content, string $mtime, string $destination ):void { $user = $this->getActualUsername($user); $mtime = new DateTime($mtime); $mtime = $mtime->format('U'); $response = $this->makeDavRequest( $user, "PUT", $destination, ["X-OC-Mtime" => $mtime], $content ); $this->setResponse($response); } /** * @param string $user * @param string $checksum * @param string|null $content * @param string $destination * @param bool|null $isGivenStep * * @return ResponseInterface */ public function uploadFileWithChecksumAndContent( string $user, string $checksum, ?string $content, string $destination, ?bool $isGivenStep = false ):ResponseInterface { $this->pauseUploadDelete(); $response = $this->makeDavRequest( $user, "PUT", $destination, ['OC-Checksum' => $checksum], $content, "files", null, false, null, [], null, $isGivenStep ); $this->lastUploadDeleteTime = \time(); return $response; } /** * @When user :user uploads file with checksum :checksum and content :content to :destination using the WebDAV API * * @param string $user * @param string $checksum * @param string $content * @param string $destination * * @return void */ public function userUploadsAFileWithChecksumAndContentTo( string $user, string $checksum, string $content, string $destination ):void { $response = $this->uploadFileWithChecksumAndContent($user, $checksum, $content, $destination); $this->setResponse($response); } /** * @Given user :user has uploaded file with checksum :checksum and content :content to :destination * * @param string $user * @param string $checksum * @param string|null $content * @param string $destination * * @return void */ public function userHasUploadedAFileWithChecksumAndContentTo( string $user, string $checksum, ?string $content, string $destination ):void { $response = $this->uploadFileWithChecksumAndContent( $user, $checksum, $content, $destination, true ); $this->theHTTPStatusCodeShouldBe( ["201", "204"], "HTTP status code was not 201 or 204 while trying to upload file with checksum '$checksum' to '$destination' for user '$user'", $response ); } /** * @Then /^user "([^"]*)" should be able to delete (file|folder|entry) "([^"]*)"$/ * * @param string $user * @param string $entry * @param string $source * * @return void * @throws Exception */ public function userShouldBeAbleToDeleteEntry(string $user, string $entry, string $source):void { $user = $this->getActualUsername($user); $this->checkFileOrFolderExistsForUser($user, $entry, $source); $this->deleteFile($user, $source); $this->checkFileOrFolderDoesNotExistsForUser($user, $entry, $source); } /** * @Then /^user "([^"]*)" should not be able to delete (file|folder|entry) "([^"]*)"$/ * * @param string $user * @param string $entry * @param string $source * * @return void * @throws Exception */ public function theUserShouldNotBeAbleToDeleteEntry(string $user, string $entry, string $source):void { $this->checkFileOrFolderExistsForUser($user, $entry, $source); $this->deleteFile($user, $source); $this->checkFileOrFolderExistsForUser($user, $entry, $source); } /** * @param string $user * @param string $resource * * @return void */ public function deleteFile(string $user, string $resource):ResponseInterface { $user = $this->getActualUsername($user); $this->pauseUploadDelete(); $response = $this->makeDavRequest($user, 'DELETE', $resource, []); $this->lastUploadDeleteTime = \time(); return $response; } /** * @When user :user deletes file/folder :resource using the WebDAV API * * @param string $user * @param string $resource * * @return void */ public function userDeletesFile(string $user, string $resource):void { $response = $this->deleteFile($user, $resource); $this->setResponse($response); $this->pushToLastStatusCodesArrays(); } /** * @When user :user deletes file :filename from space :space using file-id path :davPath * * @param string $user * @param string $filename * @param string $space * @param string $davPath * * @return void */ public function userDeletesFileFromSpaceUsingFileIdPath(string $user, string $filename, string $space, string $davPath):void { $requestUrl = $this->getBaseUrl() . $davPath; $user = $this->getActualUsername($user); $password = $this->getPasswordForUser($user); $response = HttpRequestHelper::sendRequest( $requestUrl, null, 'DELETE', $user, $password ); $this->setResponse($response); $this->pushToLastStatusCodesArrays(); } /** * @Given /^user "([^"]*)" has deleted (?:file|folder|entity) "([^"]*)"$/ * * @param string $user * @param string $resource * * @return void * @throws Exception */ public function userHasDeletedResource(string $user, string $resource):void { $user = $this->getActualUsername($user); $response = $this->deleteFile($user, $resource); // If the file or folder was there and got deleted then we get a 204 // That is good and the expected status // If the file or folder was already not there then we get a 404 // That is not expected. Scenarios that use "Given user has deleted..." // should only be using such steps when it is a file that exists and needs // to be deleted. $this->theHTTPStatusCodeShouldBe( ["204"], "HTTP status code was not 204 while trying to delete resource '$resource' for user '$user'", $response ); } /** * @Given /^user "([^"]*)" has deleted the following (?:files|folders|resources)$/ * * @param string $user * @param TableNode $table * * @return void * @throws Exception */ public function userHasDeletedFollowingFiles(string $user, TableNode $table):void { $this->verifyTableNodeColumns($table, ["path"]); $paths = $table->getHash(); foreach ($paths as $file) { $file = $file["path"]; $user = $this->getActualUsername($user); $response = $this->deleteFile($user, $file); $this->theHTTPStatusCodeShouldBe( ["204"], "HTTP status code was not 204 while trying to delete resource '$file' for user '$user'", $response ); } } /** * @When /^user "([^"]*)" deletes the following (?:files|folders)$/ * * @param string $user * @param TableNode $table * * @return void * @throws Exception */ public function userDeletesFollowingFiles(string $user, TableNode $table):void { $user = $this->getActualUsername($user); $this->verifyTableNodeColumns($table, ["path"]); $paths = $table->getHash(); foreach ($paths as $file) { $response = $this->deleteFile($user, $file["path"]); $this->setResponse($response); $this->pushToLastStatusCodesArrays(); $this->pushToLastStatusCodesArrays(); } } /** * @When /^user "([^"]*)" deletes these (?:files|folders|entries) without delays using the WebDAV API$/ * * @param string $user * @param TableNode $table of files or folders to delete * * @return void * @throws Exception */ public function userDeletesFilesFoldersWithoutDelays(string $user, TableNode $table):void { $user = $this->getActualUsername($user); $this->verifyTableNodeColumnsCount($table, 1); foreach ($table->getTable() as $entry) { $entryName = $entry[0]; $this->response = $this->makeDavRequest($user, 'DELETE', $entryName, []); $this->pushToLastStatusCodesArrays(); } $this->lastUploadDeleteTime = \time(); } /** * @When user :user creates folder :destination using the WebDAV API * * @param string $user * @param string $destination * * @return void * @throws JsonException * @throws GuzzleException */ public function userCreatesFolder(string $user, string $destination):void { $response = $this->createFolder($user, $destination); $this->setResponse($response); } /** * @Given user :user has created folder :destination * * @param string $user * @param string $destination * * @return void * @throws JsonException * @throws GuzzleException */ public function userHasCreatedFolder(string $user, string $destination):void { $user = $this->getActualUsername($user); $response = $this->createFolder($user, $destination, true); $this->theHTTPStatusCodeShouldBe( ["201", "204"], "HTTP status code was not 201 or 204 while trying to create folder '$destination' for user '$user'", $response ); } /** * @Given admin has created folder :destination * * @param string $destination * * @return void * @throws JsonException * @throws GuzzleException */ public function adminHasCreatedFolder(string $destination):void { $admin = $this->getAdminUsername(); Assert::assertEquals( "admin", $admin, __METHOD__ . "The provided user is not admin but '" . $admin . "'" ); $response = $this->createFolder($admin, $destination, true); $this->theHTTPStatusCodeShouldBe( ["201", "204"], "HTTP status code was not 201 or 204 while trying to create folder '$destination' for admin '$admin'", $response ); $this->adminResources[] = $destination; } /** * @Given /^user "([^"]*)" has created the following folders$/ * * @param string $user * @param TableNode $table * * @return void * @throws Exception */ public function userHasCreatedFollowingFolders(string $user, TableNode $table):void { $this->verifyTableNodeColumns($table, ["path"]); $paths = $table->getHash(); foreach ($paths as $path) { $destination = $path["path"]; $user = $this->getActualUsername($user); $response = $this->createFolder($user, $destination, true); $this->theHTTPStatusCodeShouldBe( ["201", "204"], "HTTP status code was not 201 or 204 while trying to create folder '$destination' for user '$user'", $response ); } } /** * @Then user :user should be able to create folder :destination * * @param string $user * @param string $destination * * @return void * @throws Exception */ public function userShouldBeAbleToCreateFolder(string $user, string $destination):void { $user = $this->getActualUsername($user); $response = $this->createFolder($user, $destination, true); $this->theHTTPStatusCodeShouldBe( ["201", "204"], "HTTP status code was not 201 or 204 while trying to create folder '$destination' for user '$user'", $response ); $this->checkFileOrFolderExistsForUser( $user, "folder", $destination ); } /** * @Then user :user should be able to create folder :destination using password :password * * @param string $user * @param string $destination * @param string $password * * @return void * @throws Exception */ public function userShouldBeAbleToCreateFolderUsingPassword(string $user, string $destination, string $password):void { $user = $this->getActualUsername($user); $response = $this->createFolder($user, $destination, true, $password); $this->theHTTPStatusCodeShouldBe( ["201", "204"], "HTTP status code was not 201 or 204 while trying to create folder '$destination' for user '$user'", $response ); $this->checkFileOrFolderExistsForUser( $user, "folder", $destination ); } /** * @Then user :user should not be able to create folder :destination * * @param string $user * @param string $destination * * @return void * @throws Exception */ public function userShouldNotBeAbleToCreateFolder(string $user, string $destination):void { $user = $this->getActualUsername($user); $response = $this->createFolder($user, $destination); $this->theHTTPStatusCodeShouldBeBetween(400, 499, $response); $this->checkFileOrFolderDoesNotExistsForUser( $user, "folder", $destination ); } /** * @Then user :user should not be able to create folder :destination using password :password * * @param string $user * @param string $destination * @param string $password * * @return void * @throws Exception */ public function userShouldNotBeAbleToCreateFolderUsingPassword(string $user, string $destination, string $password):void { $user = $this->getActualUsername($user); $response = $this->createFolder($user, $destination, false, $password); $this->theHTTPStatusCodeShouldBeBetween(400, 499, $response); $this->checkFileOrFolderDoesNotExistsForUser( $user, "folder", $destination ); } /** * Old style chunking upload * * @When user :user uploads the following :total chunks to :file with old chunking and using the WebDAV API * * @param string $user * @param string $total * @param string $file * @param TableNode $chunkDetails table of 2 columns, chunk number and chunk * content with column headings, e.g. * | number | content | * | 1 | first data | * | 2 | followed by second data | * Chunks may be numbered out-of-order if desired. * * @return void * @throws Exception */ public function userUploadsTheFollowingTotalChunksUsingOldChunking( string $user, string $total, string $file, TableNode $chunkDetails ):void { $this->verifyTableNodeColumns($chunkDetails, ['number', 'content']); foreach ($chunkDetails->getHash() as $chunkDetail) { $chunkNumber = (int)$chunkDetail['number']; $chunkContent = $chunkDetail['content']; $this->setResponse($this->userUploadChunkedFile($user, $chunkNumber, (int)$total, $chunkContent, $file)); } } /** * Old style chunking upload * * @When user :user uploads the following chunks to :file with old chunking and using the WebDAV API * * @param string $user * @param string $file * @param TableNode $chunkDetails table of 2 columns, chunk number and chunk * content with column headings, e.g. * | number | content | * | 1 | first data | * | 2 | followed by second data | * Chunks may be numbered out-of-order if desired. * * @return void * @throws Exception */ public function userUploadsTheFollowingChunksUsingOldChunking( string $user, string $file, TableNode $chunkDetails ):void { $total = (string) \count($chunkDetails->getHash()); $this->verifyTableNodeColumns($chunkDetails, ['number', 'content']); foreach ($chunkDetails->getHash() as $chunkDetail) { $chunkNumber = (int)$chunkDetail['number']; $chunkContent = $chunkDetail['content']; $this->setResponse($this->userUploadChunkedFile($user, $chunkNumber, (int)$total, $chunkContent, $file)); } } /** * Old style chunking upload * * @param string $user * @param int $num * @param int $total * @param string|null $data * @param string $destination * @param bool|null $isGivenStep * * @return ResponseInterface */ public function userUploadChunkedFile( string $user, int $num, int $total, ?string $data, string $destination, ?bool $isGivenStep = false ):ResponseInterface { $user = $this->getActualUsername($user); $num -= 1; $file = "$destination-chunking-42-$total-$num"; $this->pauseUploadDelete(); $response = $this->makeDavRequest( $user, 'PUT', $file, ['OC-Chunked' => '1'], $data, "uploads", null, false, null, [], null, $isGivenStep ); $this->lastUploadDeleteTime = \time(); return $response; } /** * New style chunking upload * * @param string $user * @param string $type "asynchronously" or empty * @param string $file * @param TableNode $chunkDetails table of 2 columns, chunk number and chunk * content with column headings, e.g. * | number | content | * | 1 | first data | * | 2 | second data | * Chunks may be numbered out-of-order if desired. * @param bool|null $isGivenStep * * @return void * @throws Exception */ public function uploadTheFollowingChunksUsingNewChunking( string $user, string $type, string $file, TableNode $chunkDetails, ?bool $isGivenStep = false ):void { $user = $this->getActualUsername($user); $async = false; if ($type === "asynchronously") { $async = true; } $this->verifyTableNodeColumns($chunkDetails, ["number", "content"]); $this->userUploadsChunksUsingNewChunking( $user, $file, 'chunking-42', $chunkDetails->getHash(), $async, $isGivenStep ); } /** * New style chunking upload * * @param string $user * @param string $file * @param string $chunkingId * @param array $chunkDetails of chunks of the file. Each array entry is * itself an array of 2 items: * [number] the chunk number * [content] data content of the chunk * Chunks may be numbered out-of-order if desired. * @param bool $async use asynchronous MOVE at the end or not * @param bool $isGivenStep * * @return void */ public function userUploadsChunksUsingNewChunking( string $user, string $file, string $chunkingId, array $chunkDetails, bool $async = false, bool $isGivenStep = false ):void { $this->pauseUploadDelete(); if ($isGivenStep) { $response = $this->userCreateANewChunkingUploadWithId($user, $chunkingId, true); $this->theHTTPStatusCodeShouldBeBetween(200, 299, $response); } else { $this->setResponse($this->userCreateANewChunkingUploadWithId($user, $chunkingId)); } foreach ($chunkDetails as $chunkDetail) { $chunkNumber = (int)$chunkDetail['number']; $chunkContent = $chunkDetail['content']; if ($isGivenStep) { $response = $this->userUploadNewChunkFileOfWithToId($user, $chunkNumber, $chunkContent, $chunkingId, true); $this->theHTTPStatusCodeShouldBeBetween(200, 299, $response); } else { $response = $this->userUploadNewChunkFileOfWithToId($user, $chunkNumber, $chunkContent, $chunkingId); $this->setResponse($response); $this->pushToLastStatusCodesArrays(); } } $headers = []; if ($async === true) { $headers = ['OC-LazyOps' => 'true']; } $response = $this->moveNewDavChunkToFinalFile($user, $chunkingId, $file, $headers, $isGivenStep); if ($isGivenStep) { $this->theHTTPStatusCodeShouldBeBetween(200, 299, $response); } else { $this->setResponse($response); } $this->lastUploadDeleteTime = \time(); } /** * * @param string $user * @param string $id * @param bool|null $isGivenStep * * @return ResponseInterface */ public function userCreateANewChunkingUploadWithId( string $user, string $id, ?bool $isGivenStep = false ):ResponseInterface { $user = $this->getActualUsername($user); $destination = "/uploads/$user/$id"; return $this->makeDavRequest( $user, 'MKCOL', $destination, [], null, "uploads", null, false, null, [], null, $isGivenStep ); } /** * @param string $user * @param int $num * @param string|null $data * @param string $id * @param bool|null $isGivenStep * * @return ResponseInterface */ public function userUploadNewChunkFileOfWithToId( string $user, int $num, ?string $data, string $id, ?bool $isGivenStep = false ):ResponseInterface { $user = $this->getActualUsername($user); $destination = "/uploads/$user/$id/$num"; return $this->makeDavRequest( $user, 'PUT', $destination, [], $data, "uploads", null, false, null, [], null, $isGivenStep ); } /** * @param string $user * @param string $id * @param string $type "asynchronously" or empty * @param string $dest * * @return ResponseInterface */ public function userMoveNewChunkFileWithIdToMychunkedfile( string $user, string $id, string $type, string $dest ):ResponseInterface { $headers = []; if ($type === "asynchronously") { $headers = ['OC-LazyOps' => 'true']; } return $this->moveNewDavChunkToFinalFile($user, $id, $dest, $headers); } /** * @When /^user "([^"]*)" moves new chunk file with id "([^"]*)"\s?(asynchronously|) to "([^"]*)" using the WebDAV API$/ * * @param string $user * @param string $id * @param string $type "asynchronously" or empty * @param string $dest * * @return void */ public function userMovesNewChunkFileWithIdToMychunkedfile( string $user, string $id, string $type, string $dest ):void { $this->setResponse($this->userMoveNewChunkFileWithIdToMychunkedfile($user, $id, $type, $dest)); } /** * @param string $user * @param string $id * @param string $type "asynchronously" or empty * @param string $dest * @param int $size * * @return ResponseInterface */ public function userMoveNewChunkFileWithIdToMychunkedfileWithSize( string $user, string $id, string $type, string $dest, int $size ):ResponseInterface { $headers = ['OC-Total-Length' => $size]; if ($type === "asynchronously") { $headers['OC-LazyOps'] = 'true'; } return $this->moveNewDavChunkToFinalFile( $user, $id, $dest, $headers ); } /** * @param string $user * @param string $id * @param string $type "asynchronously" or empty * @param string $dest * @param string $checksum * * @return ResponseInterface */ public function userMoveNewChunkFileWithIdToMychunkedfileWithChecksum( string $user, string $id, string $type, string $dest, string $checksum ):ResponseInterface { $headers = ['OC-Checksum' => $checksum]; if ($type === "asynchronously") { $headers['OC-LazyOps'] = 'true'; } return $this->moveNewDavChunkToFinalFile( $user, $id, $dest, $headers ); } /** * @Given /^user "([^"]*)" has moved new chunk file with id "([^"]*)"\s?(asynchronously|) to "([^"]*)" with checksum "([^"]*)" * * @param string $user * @param string $id * @param string $type "asynchronously" or empty * @param string $dest * @param string $checksum * * @return void */ public function userHasMovedNewChunkFileWithIdToMychunkedfileWithChecksum( string $user, string $id, string $type, string $dest, string $checksum ):void { $response = $this->userMoveNewChunkFileWithIdToMychunkedfileWithChecksum( $user, $id, $type, $dest, $checksum ); $this->theHTTPStatusCodeShouldBe("201", "", $response); } /** * Move chunked new DAV file to final file * * @param string $user user * @param string $id upload id * @param string $destination destination path * @param array $headers extra headers * @param bool|null $isGivenStep * * @return ResponseInterface */ private function moveNewDavChunkToFinalFile( string $user, string $id, string $destination, array $headers, ?bool $isGivenStep = false ):ResponseInterface { $user = $this->getActualUsername($user); $source = "/uploads/$user/$id/.file"; $headers['Destination'] = $this->destinationHeaderValue( $user, $destination ); return $this->makeDavRequest( $user, 'MOVE', $source, $headers, null, "uploads", null, false, null, [], null, $isGivenStep ); } /** * Delete chunked-upload directory * * @param string $user user * @param string $id upload id * @param array $headers extra headers * * @return ResponseInterface */ private function deleteUpload(string $user, string $id, array $headers):ResponseInterface { $source = "/uploads/$user/$id"; return $this->makeDavRequest( $user, 'DELETE', $source, $headers, null, "uploads" ); } /** * URL encodes the given path but keeps the slashes * * @param string $path to encode * * @return string encoded path */ public function encodePath(string $path):string { // slashes need to stay // in ocis even brackets are encoded return \str_replace('%2F', '/', \rawurlencode($path)); } /** * an unauthenticated client connects to the DAV endpoint using the WebDAV API * * @return ResponseInterface */ public function connectToDavEndpoint():ResponseInterface { return $this->makeDavRequest( null, 'PROPFIND', '', [] ); } /** * @When an unauthenticated client connects to the DAV endpoint using the WebDAV API * * @return void */ public function connectingToDavEndpoint():void { $this->setResponse($this->connectToDavEndpoint()); } /** * @Then there should be no duplicate headers * * @return void * @throws Exception */ public function thereAreNoDuplicateHeaders():void { $headers = $this->response->getHeaders(); foreach ($headers as $headerName => $headerValues) { // if a header has multiple values, they must be different if (\count($headerValues) > 1 && \count(\array_unique($headerValues)) < \count($headerValues) ) { throw new Exception("Duplicate header found: $headerName"); } } } /** * @Then the following headers should not be set * * @param TableNode $table * * @return void * @throws Exception */ public function theFollowingHeadersShouldNotBeSet(TableNode $table):void { $this->verifyTableNodeColumns( $table, ['header'] ); foreach ($table->getColumnsHash() as $header) { $headerName = $header['header']; $headerValue = $this->response->getHeader($headerName); //Note: getHeader returns an empty array if the named header does not exist $headerValue0 = $headerValue[0] ?? ''; Assert::assertEmpty( $headerValue, "header $headerName should not exist " . "but does and is set to $headerValue0" ); } } /** * @Then the following headers should match these regular expressions * * @param TableNode $table * * @return void * @throws Exception */ public function theFollowingHeadersShouldMatchTheseRegularExpressions(TableNode $table):void { $this->headersShouldMatchRegularExpressions($table); } /** * @param TableNode $table * * @return void */ public function headersShouldMatchRegularExpressions(TableNode $table):void { $this->verifyTableNodeColumnsCount($table, 2); foreach ($table->getTable() as $header) { $headerName = $header[0]; $expectedHeaderValue = $header[1]; $expectedHeaderValue = $this->substituteInLineCodes( $expectedHeaderValue, null, ['preg_quote' => ['/']] ); $returnedHeaders = $this->response->getHeader($headerName); $returnedHeader = $returnedHeaders[0]; Assert::assertNotFalse( (bool) \preg_match($expectedHeaderValue, $returnedHeader), "'$expectedHeaderValue' does not match '$returnedHeader'" ); } } /** * @Then /^if the HTTP status code was "([^"]*)" then the following headers should match these regular expressions$/ * * @param int $statusCode * @param TableNode $table * * @return void * @throws Exception */ public function statusCodeShouldMatchTheseRegularExpressions(int $statusCode, TableNode $table):void { $actualStatusCode = $this->response->getStatusCode(); if ($actualStatusCode === $statusCode) { $this->headersShouldMatchRegularExpressions($table); } } /** * @Then the following headers should match these regular expressions for user :user * * @param string $user * @param TableNode $table * * @return void * @throws Exception */ public function headersShouldMatchRegularExpressionsForUser(string $user, TableNode $table):void { $this->verifyTableNodeColumnsCount($table, 2); $user = $this->getActualUsername($user); foreach ($table->getTable() as $header) { $headerName = $header[0]; $expectedHeaderValue = $header[1]; $expectedHeaderValue = $this->substituteInLineCodes( $expectedHeaderValue, $user, ['preg_quote' => ['/']] ); $returnedHeaders = $this->response->getHeader($headerName); $returnedHeader = $returnedHeaders[0]; Assert::assertNotFalse( (bool) \preg_match($expectedHeaderValue, $returnedHeader), "'$expectedHeaderValue' does not match '$returnedHeader'" ); } } /** * @When /^user "([^"]*)" deletes everything from folder "([^"]*)" using the WebDAV API$/ * * @param string $user * @param string $folder * * @return void * @throws Exception */ public function userDeletesEverythingInFolder( string $user, string $folder ):void { $this->deleteEverythingInFolder($user, $folder, false); } /** * @param string $user * @param string $folder * @param boolean $checkEachDelete * * @return void */ public function deleteEverythingInFolder( string $user, string $folder, bool $checkEachDelete = false ):void { $user = $this->getActualUsername($user); $responseXmlObject = $this->listFolderAndReturnResponseXml( $user, $folder, '1' ); $elementList = $responseXmlObject->xpath("//d:response/d:href"); if (\is_array($elementList) && \count($elementList)) { \array_shift($elementList); //don't delete the folder itself $davPrefix = "/" . $this->getFullDavFilesPath($user); foreach ($elementList as $element) { $element = \substr((string)$element, \strlen($davPrefix)); if ($checkEachDelete) { $user = $this->getActualUsername($user); $response = $this->deleteFile($user, $element); $this->theHTTPStatusCodeShouldBe( ["204"], "HTTP status code was not 204 while trying to delete resource '$element' for user '$user'", $response ); } else { $this->setResponse($this->deleteFile($user, $element)); $this->pushToLastStatusCodesArrays(); } } } } /** * @When user :user downloads the preview of :path with width :width and height :height using the WebDAV API * @When user :user tries to download the preview of nonexistent file :path with width :width and height :height using the WebDAV API * * @param string $user * @param string $path * @param string $width * @param string $height * * @return void */ public function downloadPreviewOfFiles(string $user, string $path, string $width, string $height):void { $response = $this->downloadPreviews( $user, $path, null, $width, $height ); $this->setResponse($response); } /** * @When user :user downloads the preview of shared resource :path with width :width and height :height using the WebDAV API * * @param string $user * @param string $path * @param string $width * @param string $height * * @return void */ public function userDownloadsThePreviewOfSharedResourceWithWidthAndHeightUsingTheWebdavApi(string $user, string $path, string $width, string $height): void { if ($this->getDavPathVersion() === 3) { $this->setResponse($this->downloadSharedFilePreview($user, $path, $width, $height)); } else { $this->setResponse($this->downloadPreviews($user, $path, null, $width, $height)); } } /** * @Given user :user has downloaded the preview of shared resource :path with width :width and height :height * * @param string $user * @param string $path * @param string $width * @param string $height * * @return void */ public function userHasDownloadedThePreviewOfSharedResourceWithWidthAndHeight(string $user, string $path, string $width, string $height): void { if ($this->getDavPathVersion() === 3) { $response = $this->downloadSharedFilePreview($user, $path, $width, $height); } else { $response = $this->downloadPreviews($user, $path, null, $width, $height); } $this->setResponse($response); $this->theHTTPStatusCodeShouldBe(200, '', $response); $this->checkImageDimensions($width, $height); // save response to user response dictionary for further comparisons $this->userResponseBodyContents[$user] = $this->responseBodyContent; } /** * @Then as user :user the preview of shared resource :path with width :width and height :height should have been changed * * @param string $user * @param string $path * @param string $width * @param string $height * * @return void */ public function asUserThePreviewOfSharedResourceWithWidthAndHeightShouldHaveBeenChanged(string $user, string $path, string $width, string $height):void { if ($this->getDavPathVersion() === 3) { $response = $this->downloadSharedFilePreview($user, $path, $width, $height); } else { $response = $this->downloadPreviews($user, $path, null, $width, $height); } $this->setResponse($response); $this->theHTTPStatusCodeShouldBe(200, '', $response); $newResponseBodyContents = $this->response->getBody()->getContents(); Assert::assertNotEquals( $newResponseBodyContents, // different users can download files before and after an update is made to a file // previous response content is fetched from the user response body content array entry for that user $this->userResponseBodyContents[$user], __METHOD__ . " previous and current previews content is same but expected to be different", ); // update the saved content for the next comparison $this->userResponseBodyContents[$user] = $newResponseBodyContents; } /** * @When user :user uploads file with content :content to shared resource :destination using the WebDAV API * * @param string $user * @param string $content * @param string $destination * * @return void */ public function userUploadsFileWithContentSharedResourceToUsingTheWebdavApi(string $user, string $content, string $destination): void { if ($this->getDavPathVersion() === 3) { $this->setResponse($this->uploadToSharedFolder($user, $destination, $content)); } else { $this->setResponse($this->uploadFileWithContent($user, $content, $destination)); } } /** * @param string $user * @param string $path * * @return string * @throws GuzzleException */ public function getSharesMountPath( string $user, string $path ): string { $user = $this->getActualUsername($user); $path = trim($path, "/"); $pathArray = explode("/", $path); $sharedFolder = $pathArray[0] === "Shares" ? $pathArray[1] : $pathArray[0]; $shareMountId = GraphHelper::getShareMountId( $this->getBaseUrl(), $this->getStepLineRef(), $user, $this->getPasswordForUser($user), $sharedFolder ); $path = \array_slice($pathArray, array_search($sharedFolder, $pathArray) + 1); $path = \implode("/", $path); return "$shareMountId/$path"; } /** * @param string $user * @param string $path * @param string|null $width * @param string|null $height * * @return ResponseInterface * @throws GuzzleException */ public function downloadSharedFilePreview( string $user, string $path, ?string $width = null, ?string $height = null ): ResponseInterface { if ($width !== null && $height !== null) { $urlParameter = [ 'x' => $width, 'y' => $height, 'forceIcon' => '0', 'preview' => '1' ]; $urlParameter = \http_build_query($urlParameter, '', '&'); } else { $urlParameter = null; } $sharesPath = $this->getSharesMountPath($user, $path) . '/?' . $urlParameter; $davPath = WebDavHelper::getDavPath($user, $this->getDavPathVersion()); $fullUrl = $this->getBaseUrl() . "/$davPath" . $sharesPath; return HttpRequestHelper::sendRequest( $fullUrl, $this->getStepLineRef(), 'GET', $user, $this->getPasswordForUser($user) ); } /** * @param string $user * @param string $destination * @param string|null $content * * @return ResponseInterface * @throws GuzzleException */ public function uploadToSharedFolder( string $user, string $destination, ?string $content = null ): ResponseInterface { $sharesPath = $this->getSharesMountPath($user, $destination); $davPath = WebDavHelper::getDavPath($user, $this->getDavPathVersion()); $fullUrl = $this->getBaseUrl() . "/$davPath" . $sharesPath; return HttpRequestHelper::sendRequest( $fullUrl, $this->getStepLineRef(), 'PUT', $user, $this->getPasswordForUser($user), null, $content ); } /** * @When user :user1 downloads the preview of :path of :user2 with width :width and height :height using the WebDAV API * * @param string $user1 * @param string $path * @param string $doDavRequestAsUser * @param string $width * @param string $height * * @return void */ public function downloadPreviewOfOtherUser(string $user1, string $path, string $doDavRequestAsUser, string $width, string $height):void { $response = $this->downloadPreviews( $user1, $path, $doDavRequestAsUser, $width, $height ); $this->setResponse($response); } /** * @Then the downloaded image should be :width pixels wide and :height pixels high * * @param string $width * @param string $height * * @return void */ public function imageDimensionsShouldBe(string $width, string $height): void { $this->checkImageDimensions($width, $height); } /** * @param string $width * @param string $height * @param ResponseInterface|null $response * * @return void */ public function checkImageDimensions(string $width, string $height, ?ResponseInterface $response = null) : void { $response = $response ?? $this->getResponse(); if ($this->responseBodyContent === null) { $this->responseBodyContent = $response->getBody()->getContents(); } $size = \getimagesizefromstring($this->responseBodyContent); Assert::assertNotFalse($size, "could not get size of image"); Assert::assertEquals($width, $size[0], "width not as expected"); Assert::assertEquals($height, $size[1], "height not as expected"); } /** * @Then the downloaded preview content should match with :preview fixtures preview content * * @param string $filename relative path from fixtures directory * * @return void * @throws Exception */ public function theDownloadedPreviewContentShouldMatchWithFixturesPreviewContentFor(string $filename):void { $expectedPreview = \file_get_contents(__DIR__ . "/../fixtures/" . $filename); Assert::assertEquals($expectedPreview, $this->responseBodyContent); } /** * @Given user :user has downloaded the preview of :path with width :width and height :height * * @param string $user * @param string $path * @param string $width * @param string $height * * @return void */ public function userDownloadsThePreviewOfWithWidthAndHeight(string $user, string $path, string $width, string $height):void { $response = $this->downloadPreviews( $user, $path, null, $width, $height ); $this->theHTTPStatusCodeShouldBe(200, "", $response); $this->checkImageDimensions($width, $height, $response); // save response to user response dictionary for further comparisons $this->userResponseBodyContents[$user] = $this->responseBodyContent; } /** * @Then as user :user the preview of :path with width :width and height :height should have been changed * * @param string $user * @param string $path * @param string $width * @param string $height * * @return void */ public function asUserThePreviewOfPathWithHeightAndWidthShouldHaveBeenChanged(string $user, string $path, string $width, string $height):void { $response = $this->downloadPreviews( $user, $path, null, $width, $height ); $this->theHTTPStatusCodeShouldBe(200, "", $response); $newResponseBodyContents = $response->getBody()->getContents(); Assert::assertNotEquals( $newResponseBodyContents, // different users can download files before and after an update is made to a file // previous response content is fetched from the user response body content array entry for that user $this->userResponseBodyContents[$user], __METHOD__ . " previous and current previews content is same but expected to be different", ); // update the saved content for the next comparison $this->userResponseBodyContents[$user] = $newResponseBodyContents; } /** * @param string $user * @param string $path * * @return string|null */ public function getFileIdForPath(string $user, string $path): ?string { $user = $this->getActualUsername($user); try { return WebDavHelper::getFileIdForPath( $this->getBaseUrl(), $user, $this->getPasswordForUser($user), $path, $this->getStepLineRef(), $this->getDavPathVersion() ); } catch (Exception $e) { return null; } } /** * @Given /^user "([^"]*)" has stored id of (?:file|folder) "([^"]*)"$/ * * @param string $user * @param string $path * * @return void */ public function userStoresFileIdForPath(string $user, string $path):void { $this->storedFileID = $this->getFileIdForPath($user, $path); } /** * @Then /^user "([^"]*)" (file|folder) "([^"]*)" should have the previously stored id$/ * * @param string $user * @param string $fileOrFolder * @param string $path * * @return void */ public function userFileShouldHaveStoredId(string $user, string $fileOrFolder, string $path):void { $user = $this->getActualUsername($user); $currentFileID = $this->getFileIdForPath($user, $path); Assert::assertEquals( $currentFileID, $this->storedFileID, __METHOD__ . " User '$user' $fileOrFolder '$path' does not have the previously stored id '$this->storedFileID', but has '$currentFileID'." ); } /** * @Then /^the (?:Cal|Card)?DAV (exception|message|reason) should be "([^"]*)"$/ * * @param string $element exception|message|reason * @param string $message * * @return void * @throws Exception */ public function theDavElementShouldBe(string $element, string $message):void { $resXmlArray = HttpRequestHelper::parseResponseAsXml($this->getResponse()); WebDavAssert::assertDavResponseElementIs( $element, $message, $resXmlArray, __METHOD__ ); } /** * @param string $shouldOrNot (not|) * @param TableNode $expectedFiles * @param string|null $user * @param string|null $method * @param string|null $folderpath * * @return void * @throws GuzzleException */ public function propfindResultShouldContainEntries( string $shouldOrNot, TableNode $expectedFiles, ?string $user = null, ?string $method = 'REPORT', ?string $folderpath = '' ):void { if ($folderpath === "/") { $folderpath = ""; } $this->verifyTableNodeColumnsCount($expectedFiles, 1); $elementRows = $expectedFiles->getRows(); $should = ($shouldOrNot !== "not"); foreach ($elementRows as $expectedFile) { $resource = $expectedFile[0]; $resource = $this->substituteInLineCodes($resource, $user); if ($resource === '') { continue; } if ($method === "REPORT") { $fileFound = $this->findEntryFromSearchResponse( $resource ); if (\is_object($fileFound)) { $fileFound = $fileFound->xpath("d:propstat//oc:name"); } } else { $fileFound = $this->findEntryFromPropfindResponse( $resource, $user, "files", $folderpath ); } if ($should) { Assert::assertNotEmpty( $fileFound, "response does not contain the entry '$resource'" ); } else { Assert::assertFalse( $fileFound, "response does contain the entry '$resource' but should not" ); } } } /** * @Then /^the (?:propfind|search) result of user "([^"]*)" should (not|)\s?contain these (?:files|entries):$/ * * @param string $user * @param string $shouldOrNot (not|) * @param TableNode $expectedFiles * * @return void * @throws Exception */ public function thePropfindResultShouldContainEntries( string $user, string $shouldOrNot, TableNode $expectedFiles ):void { $user = $this->getActualUsername($user); $this->propfindResultShouldContainEntries( $shouldOrNot, $expectedFiles, $user ); } /** * @Then /^the (?:propfind|search) result of user "([^"]*)" should contain only these (?:files|entries):$/ * * @param string $user * @param TableNode $expectedFiles * * @return void * @throws Exception */ public function thePropfindResultShouldContainOnlyEntries( string $user, TableNode $expectedFiles ):void { $user = $this->getActualUsername($user); Assert::assertEquals( \count($expectedFiles->getTable()), $this->getNumberOfEntriesInPropfindResponse( $user ), "The number of elements in the response doesn't matches with expected number of elements" ); $this->propfindResultShouldContainEntries( '', $expectedFiles, $user ); } /** * @Then the propfind/search result should contain :numFiles files/entries * * @param int $numFiles * * @return void */ public function propfindResultShouldContainNumEntries(int $numFiles):void { $this->checkIFResponseContainsNumberEntries($numFiles); } /** * @param integer $numFiles * * @return void */ public function checkIFResponseContainsNumberEntries(int $numFiles):void { //if we are using that step the second time in a scenario e.g. 'But ... should not' //then don't parse the result again, because the result in a ResponseInterface if (empty($this->responseXml)) { $this->setResponseXml( HttpRequestHelper::parseResponseAsXml($this->response) ); } Assert::assertIsArray( $this->responseXml, __METHOD__ . " responseXml is not an array" ); Assert::assertArrayHasKey( "value", $this->responseXml, __METHOD__ . " responseXml does not have key 'value'" ); $multistatusResults = $this->responseXml["value"]; if ($multistatusResults === null) { $multistatusResults = []; } Assert::assertEquals( $numFiles, \count($multistatusResults), __METHOD__ . " Expected result to contain '" . $numFiles . "' files/entries, but got '" . \count($multistatusResults) . "' files/entries." ); } /** * @Then the propfind/search result should contain any :expectedNumber of these files/entries: * * @param integer $expectedNumber * @param TableNode $expectedFiles * * @return void * @throws Exception */ public function theSearchResultShouldContainAnyOfTheseEntries( int $expectedNumber, TableNode $expectedFiles ):void { $this->checkIfSearchResultContainsFiles( $this->getCurrentUser(), $expectedNumber, $expectedFiles ); } /** * @Then the propfind/search result of user :user should contain any :expectedNumber of these files/entries: * * @param string $user * @param integer $expectedNumber * @param TableNode $expectedFiles * * @return void * @throws Exception */ public function theSearchResultOfUserShouldContainAnyOfTheseEntries( string $user, int $expectedNumber, TableNode $expectedFiles ):void { $this->checkIfSearchResultContainsFiles( $user, $expectedNumber, $expectedFiles ); } /** * @param string $user * @param integer $expectedNumber * @param TableNode $expectedFiles * * @return void */ public function checkIfSearchResultContainsFiles( string $user, int $expectedNumber, TableNode $expectedFiles ):void { $user = $this->getActualUsername($user); $this->verifyTableNodeColumnsCount($expectedFiles, 1); $this->checkIFResponseContainsNumberEntries($expectedNumber); $elementRows = $expectedFiles->getColumn(0); // Remove any "/" from the front (or back) of the expected values passed // into the step. findEntryFromPropfindResponse returns entries without // any leading (or trailing) slash $expectedEntries = \array_map( function ($value) { return \trim($value, "/"); }, $elementRows ); $resultEntries = $this->findEntryFromSearchResponse(); foreach ($resultEntries as $resultEntry) { Assert::assertContains($resultEntry, $expectedEntries); } } /** * @When user :arg1 lists the resources in :path with depth :depth using the WebDAV API * * @param string $user * @param string $path * @param string $depth * * @return void * @throws Exception */ public function userListsTheResourcesInPathWithDepthUsingTheWebdavApi(string $user, string $path, string $depth):void { $response = $this->listFolder( $user, $path, $depth ); $this->setResponse($response); $this->setResponseXml(HttpRequestHelper::parseResponseAsXml($this->response)); } /** * @Then the last DAV response for user :user should contain these nodes/elements * * @param string $user * @param TableNode $table * * @return void * @throws Exception */ public function theLastDavResponseShouldContainTheseNodes(string $user, TableNode $table):void { $this->verifyTableNodeColumns($table, ["name"]); foreach ($table->getHash() as $row) { $path = $this->substituteInLineCodes($row['name']); $res = $this->findEntryFromPropfindResponse($path, $user); Assert::assertNotFalse($res, "expected $path to be in DAV response but was not found"); } } /** * @Then the last DAV response for user :user should not contain these nodes/elements * * @param string $user * @param TableNode $table * * @return void * @throws Exception */ public function theLastDavResponseShouldNotContainTheseNodes(string $user, TableNode $table):void { $this->verifyTableNodeColumns($table, ["name"]); foreach ($table->getHash() as $row) { $path = $this->substituteInLineCodes($row['name']); $res = $this->findEntryFromPropfindResponse($path, $user); Assert::assertFalse($res, "expected $path to not be in DAV response but was found"); } } /** * @Then the last public link DAV response should contain these nodes/elements * * @param TableNode $table * * @return void * @throws Exception */ public function theLastPublicDavResponseShouldContainTheseNodes(TableNode $table):void { $token = ($this->isUsingSharingNG()) ? $this->shareNgGetLastCreatedLinkShareToken() : $this->getLastCreatedPublicShareToken(); $this->verifyTableNodeColumns($table, ["name"]); $type = $this->usingOldDavPath ? "public-files" : "public-files-new"; foreach ($table->getHash() as $row) { $path = $this->substituteInLineCodes($row['name']); $res = $this->findEntryFromPropfindResponse($path, $token, $type); Assert::assertNotFalse($res, "expected $path to be in DAV response but was not found"); } } /** * @Then the last public link DAV response should not contain these nodes/elements * * @param TableNode $table * * @return void * @throws Exception */ public function theLastPublicDavResponseShouldNotContainTheseNodes(TableNode $table):void { $token = ($this->isUsingSharingNG()) ? $this->shareNgGetLastCreatedLinkShareToken() : $this->getLastCreatedPublicShareToken(); $this->verifyTableNodeColumns($table, ["name"]); $type = $this->usingOldDavPath ? "public-files" : "public-files-new"; foreach ($table->getHash() as $row) { $path = $this->substituteInLineCodes($row['name']); $res = $this->findEntryFromPropfindResponse($path, $token, $type); Assert::assertFalse($res, "expected $path to not be in DAV response but was found"); } } /** * @When the public lists the resources in the last created public link with depth :depth using the WebDAV API * * @param string $depth * * @return void * @throws Exception */ public function thePublicListsTheResourcesInTheLastCreatedPublicLinkWithDepthUsingTheWebdavApi(string $depth):void { $token = ($this->isUsingSharingNG()) ? $this->shareNgGetLastCreatedLinkShareToken() : $this->getLastCreatedPublicShareToken(); $response = $this->listFolder( $token, '/', $depth, null, $this->usingOldDavPath ? "public-files" : "public-files-new" ); $this->setResponse($response); $this->setResponseXml(HttpRequestHelper::parseResponseAsXml($this->response)); } /** * @param string|null $user * * @return array */ public function findEntryFromReportResponse(?string $user):array { $responseXmlObj = $this->getResponseXmlObject(); $responseResources = []; $hrefs = $responseXmlObj->xpath('//d:href'); foreach ($hrefs as $href) { $hrefParts = \explode("/", (string)$href[0]); if (\in_array($user, $hrefParts)) { $entry = \urldecode(\end($hrefParts)); $responseResources[] = $entry; } else { throw new Error("Expected user: $hrefParts[5] but found: $user"); } } return $responseResources; } /** * parses a PROPFIND response from $this->response into xml * and returns found search results if found else returns false * * @param string|null $user * * @return int */ public function getNumberOfEntriesInPropfindResponse( ?string $user = null ):int { $multistatusResults = $this->getMultiStatusResultFromPropfindResult($user); return \count($multistatusResults); } /** * parses a PROPFIND response from $this->response * and returns multistatus data from the response * * @param string|null $user * * @return array */ public function getMultiStatusResultFromPropfindResult( ?string $user = null ):array { //if we are using that step the second time in a scenario e.g. 'But ... should not' //then don't parse the result again, because the result in a ResponseInterface if (empty($this->responseXml)) { $this->setResponseXml( HttpRequestHelper::parseResponseAsXml($this->response) ); } Assert::assertNotEmpty($this->responseXml, __METHOD__ . ' Response is empty'); if ($user === null) { $user = $this->getCurrentUser(); } Assert::assertIsArray( $this->responseXml, __METHOD__ . " responseXml for user $user is not an array" ); Assert::assertArrayHasKey( "value", $this->responseXml, __METHOD__ . " responseXml for user $user does not have key 'value'" ); $multistatus = $this->responseXml["value"]; if ($multistatus == null) { $multistatus = []; } return $multistatus; } /** * Escapes the given string for * 1. Space --> %20 * 2. Opening Small Bracket --> %28 * 3. Closing Small Bracket --> %29 * * @param string $path - File path to parse * * @return string */ public function escapePath(string $path): string { return \str_replace([" ", "(", ")"], ["%20", "%28", "%29"], $path); } /** * parses a PROPFIND response from $this->response into xml * and returns found search results if found else returns false * * @param string|null $entryNameToSearch * @param string|null $user * @param string $type * @param string $folderPath * * @return string|array|boolean * * string if $entryNameToSearch is given and is found * array if $entryNameToSearch is not given * boolean false if $entryNameToSearch is given and is not found * * @throws GuzzleException */ public function findEntryFromPropfindResponse( ?string $entryNameToSearch = null, ?string $user = null, string $type = "files", string $folderPath = '' ) { $trimmedEntryNameToSearch = ''; // trim any leading "/" passed by the caller, we can just match the "raw" name if ($entryNameToSearch != null) { $trimmedEntryNameToSearch = \trim($entryNameToSearch, "/"); } // url encode for spaces and brackets that may appear in the filePath $folderPath = $this->escapePath($folderPath); // topWebDavPath should be something like /remote.php/webdav/ or // /remote.php/dav/files/alice/ $topWebDavPath = "/" . $this->getFullDavFilesPath($user) . "/" . $folderPath; switch ($type) { case "files": break; case "public-files": case "public-files-old": case "public-files-new": $topWebDavPath = "/" . $this->getPublicLinkDavPath($user, $type) . "/"; break; default: throw new Exception("error"); } $multistatusResults = $this->getMultiStatusResultFromPropfindResult($user); $results = []; foreach ($multistatusResults as $multistatusResult) { $entryPath = $multistatusResult['value'][0]['value']; $entryName = \str_replace($topWebDavPath, "", $entryPath); $entryName = \rawurldecode($entryName); $entryName = \trim($entryName, "/"); if ($trimmedEntryNameToSearch === $entryName) { return $multistatusResult; } $results[] = $entryName; } if ($entryNameToSearch === null) { return $results; } return false; } /** * parses a REPORT response from $this->response into xml * and returns found search results if found else returns false * * @param string|null $entryNameToSearch * @param bool|null $searchForHighlightString * * @return string|array|boolean * * string if $entryNameToSearch is given and is found * array if $entryNameToSearch is not given * boolean false if $entryNameToSearch is given and is not found * * @throws Exception */ public function findEntryFromSearchResponse( ?string $entryNameToSearch = null, ?bool $searchForHighlightString = false ) { // trim any leading "/" passed by the caller, we can just match the "raw" name if ($entryNameToSearch !== null) { $entryNameToSearch = \trim($entryNameToSearch, "/"); } $spacesBaseUrl = "/" . webDavHelper::getDavPath(null, webDavHelper::DAV_VERSION_SPACES); $searchResults = $this->getResponseXml()->xpath("//d:multistatus/d:response"); $results = []; foreach ($searchResults as $item) { $href = (string)$item->xpath("d:href")[0]; $shareRootXml = $item->xpath("d:propstat//oc:shareroot"); $href = \str_replace($spacesBaseUrl, "", $href); $resourcePath = $href; // do not try to parse the resource path // if the item to search is space itself if (!GraphHelper::isSpaceId($entryNameToSearch ?? '')) { $resourcePath = \substr($href, \strpos($href, '/') + 1); } if (\count($shareRootXml)) { $shareroot = \trim((string)$shareRootXml[0], "/"); $resourcePath = $shareroot . "/" . $resourcePath; } $resourcePath = \rawurldecode($resourcePath); if ($entryNameToSearch === $resourcePath) { // If searching for a single entry, // we return a SimpleXmlElement of found item return $item; } if ($searchForHighlightString) { // If searching for highlighted string, // we return an array of entries with highlighted content as value // Example: // [ // "" => "" // "" => "" // ] $actualHighlightString = $item->xpath("d:propstat//oc:highlights"); $results[$resourcePath] = (string)$actualHighlightString[0]; } else { // If list all the entries i.e. $entryNameToSearch=null, // we return an array of entries in the response // Example: // ["", ""] $results[] = $resourcePath; } } if ($entryNameToSearch === null) { return $results; } return false; } /** * Prevent creating two uploads and/or deletes with the same "stime" * That is based on seconds in some implementations. * This prevents duplication of etags or other problems with * trashbin/versions save/restore. * * Set env var UPLOAD_DELETE_WAIT_TIME to 1 to activate a 1-second pause. * By default, there is no pause. That allows testing of implementations * which should be able to cope with multiple upload/delete actions in the * same second. * * @return void */ public function pauseUploadDelete():void { $time = \time(); $uploadWaitTime = \getenv("UPLOAD_DELETE_WAIT_TIME"); $uploadWaitTime = $uploadWaitTime ? (int)$uploadWaitTime : 0; if (($this->lastUploadDeleteTime !== null) && ($uploadWaitTime > 0) && (($time - $this->lastUploadDeleteTime) < $uploadWaitTime) ) { \sleep($uploadWaitTime); } } /** * @param string $index * @param string $expectedUsername * * @return void */ public function checkAuthorOfAVersionOfFile(string $index, string $expectedUsername):void { $expectedUserDisplayName = $this->getUserDisplayName($expectedUsername); $resXml = $this->getResponseXmlObject(); if ($resXml === null) { $resXml = HttpRequestHelper::getResponseXml( $this->getResponse(), __METHOD__ ); $this->setResponseXmlObject($resXml); } // the username should be in oc:meta-version-edited-by $xmlPart = $resXml->xpath("//oc:meta-version-edited-by"); $authors = []; foreach ($xmlPart as $idx => $author) { // The first element is the root path element which is not a version // So skipping it if ($idx !== 0) { $authors[] = $author->__toString(); } } if (!isset($authors[$index - 1])) { Assert::fail( 'could not find version with index "' . $index . '" for oc:meta-version-edited-by property in response to user "' . $this->responseUser . '"' ); } $actualUser = $authors[$index - 1]; Assert::assertEquals( $expectedUsername, $actualUser, "Expected user of version with index $index in response to user '$this->responseUser' was '$expectedUsername', but got '$actualUser'" ); // the user's display name should be in oc:meta-version-edited-by-name $xmlPart = $resXml->xpath("//oc:meta-version-edited-by-name"); $displaynames = []; foreach ($xmlPart as $idx => $displayname) { // The first element is the root path element which is not a version // So skipping it if ($idx !== 0) { $displaynames[] = $displayname->__toString(); } } if (!isset($displaynames[$index - 1])) { Assert::fail( 'could not find version with index "' . $index . '" for oc:meta-version-edited-by-name property in response to user "' . $this->responseUser . '"' ); } $actualUserDisplayName = $displaynames[$index - 1]; Assert::assertEquals( $expectedUserDisplayName, $actualUserDisplayName, "Expected display name of version with index $index in response to user '$this->responseUser' was '$expectedUserDisplayName', but got '$actualUserDisplayName'" ); } /** * @When user :user downloads the content of GDPR report :pathToFile * * @param string $user * @param string $pathToFile * * @return void * @throws Exception */ public function userGetsTheContentOfGeneratedJsonReport(string $user, string $pathToFile): void { $password = $this->getPasswordForUser($user); $response = $this->downloadFileAsUserUsingPassword($user, $pathToFile, $password); $this->setResponse($response); $this->pushToLastStatusCodesArrays(); } }