diff options
Diffstat (limited to 'build/integration/features/bootstrap')
40 files changed, 1132 insertions, 289 deletions
diff --git a/build/integration/features/bootstrap/Activity.php b/build/integration/features/bootstrap/Activity.php new file mode 100644 index 00000000000..4172776304d --- /dev/null +++ b/build/integration/features/bootstrap/Activity.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +use Behat\Gherkin\Node\TableNode; +use PHPUnit\Framework\Assert; + +trait Activity { + use BasicStructure; + + /** + * @Then last activity should be + * @param TableNode $activity + */ + public function lastActivityIs(TableNode $activity): void { + $this->sendRequestForJSON('GET', '/apps/activity/api/v2/activity'); + $this->theHTTPStatusCodeShouldBe('200'); + $data = json_decode($this->response->getBody()->getContents(), true); + $activities = $data['ocs']['data']; + /* Sort by id */ + uasort($activities, fn ($a, $b) => $a['activity_id'] <=> $b['activity_id']); + $lastActivity = array_pop($activities); + foreach ($activity->getRowsHash() as $key => $value) { + Assert::assertEquals($value, $lastActivity[$key]); + } + } +} diff --git a/build/integration/features/bootstrap/AppConfiguration.php b/build/integration/features/bootstrap/AppConfiguration.php index 5f39c58ffeb..e8580ed537b 100644 --- a/build/integration/features/bootstrap/AppConfiguration.php +++ b/build/integration/features/bootstrap/AppConfiguration.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/Auth.php b/build/integration/features/bootstrap/Auth.php index e620af4d530..aeaade85383 100644 --- a/build/integration/features/bootstrap/Auth.php +++ b/build/integration/features/bootstrap/Auth.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -203,7 +204,8 @@ trait Auth { * @param bool $remember */ public function aNewBrowserSessionIsStarted($remember = false) { - $loginUrl = substr($this->baseUrl, 0, -5) . '/login'; + $baseUrl = substr($this->baseUrl, 0, -5); + $loginUrl = $baseUrl . '/login'; // Request a new session and extract CSRF token $client = new Client(); $response = $client->get($loginUrl, [ @@ -222,6 +224,9 @@ trait Auth { 'requesttoken' => $this->requestToken, ], 'cookies' => $this->cookieJar, + 'headers' => [ + 'Origin' => $baseUrl, + ], ] ); $this->extracRequestTokenFromResponse($response); diff --git a/build/integration/features/bootstrap/Avatar.php b/build/integration/features/bootstrap/Avatar.php index f05d0372687..beebf1c024a 100644 --- a/build/integration/features/bootstrap/Avatar.php +++ b/build/integration/features/bootstrap/Avatar.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -9,7 +10,7 @@ use PHPUnit\Framework\Assert; require __DIR__ . '/../../vendor/autoload.php'; trait Avatar { - /** @var string **/ + /** @var string * */ private $lastAvatar; /** @AfterScenario **/ @@ -240,10 +241,10 @@ trait Avatar { } private function isSameColor(array $firstColor, array $secondColor, int $allowedDelta = 1) { - if ($this->isSameColorComponent($firstColor['red'], $secondColor['red'], $allowedDelta) && - $this->isSameColorComponent($firstColor['green'], $secondColor['green'], $allowedDelta) && - $this->isSameColorComponent($firstColor['blue'], $secondColor['blue'], $allowedDelta) && - $this->isSameColorComponent($firstColor['alpha'], $secondColor['alpha'], $allowedDelta)) { + if ($this->isSameColorComponent($firstColor['red'], $secondColor['red'], $allowedDelta) + && $this->isSameColorComponent($firstColor['green'], $secondColor['green'], $allowedDelta) + && $this->isSameColorComponent($firstColor['blue'], $secondColor['blue'], $allowedDelta) + && $this->isSameColorComponent($firstColor['alpha'], $secondColor['alpha'], $allowedDelta)) { return true; } @@ -251,8 +252,8 @@ trait Avatar { } private function isSameColorComponent(int $firstColorComponent, int $secondColorComponent, int $allowedDelta) { - if ($firstColorComponent >= ($secondColorComponent - $allowedDelta) && - $firstColorComponent <= ($secondColorComponent + $allowedDelta)) { + if ($firstColorComponent >= ($secondColorComponent - $allowedDelta) + && $firstColorComponent <= ($secondColorComponent + $allowedDelta)) { return true; } diff --git a/build/integration/features/bootstrap/BasicStructure.php b/build/integration/features/bootstrap/BasicStructure.php index 6a1cc7e1ada..59a4312913e 100644 --- a/build/integration/features/bootstrap/BasicStructure.php +++ b/build/integration/features/bootstrap/BasicStructure.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -8,6 +9,7 @@ use Behat\Gherkin\Node\TableNode; use GuzzleHttp\Client; use GuzzleHttp\Cookie\CookieJar; use GuzzleHttp\Exception\ClientException; +use GuzzleHttp\Exception\ServerException; use PHPUnit\Framework\Assert; use Psr\Http\Message\ResponseInterface; @@ -18,6 +20,7 @@ trait BasicStructure { use Avatar; use Download; use Mail; + use Theming; /** @var string */ private $currentUser = ''; @@ -120,7 +123,11 @@ trait BasicStructure { * @return string */ public function getOCSResponse($response) { - return simplexml_load_string($response->getBody())->meta[0]->statuscode; + $body = simplexml_load_string((string)$response->getBody()); + if ($body === false) { + throw new \RuntimeException('Could not parse OCS response, body is not valid XML'); + } + return $body->meta[0]->statuscode; } /** @@ -170,6 +177,8 @@ trait BasicStructure { $this->response = $client->request($verb, $fullUrl, $options); } catch (ClientException $ex) { $this->response = $ex->getResponse(); + } catch (ServerException $ex) { + $this->response = $ex->getResponse(); } } @@ -185,8 +194,8 @@ trait BasicStructure { $options = []; if ($this->currentUser === 'admin') { $options['auth'] = ['admin', 'admin']; - } elseif (strpos($this->currentUser, 'guest') !== 0) { - $options['auth'] = [$this->currentUser, self::TEST_PASSWORD]; + } elseif (strpos($this->currentUser, 'anonymous') !== 0) { + $options['auth'] = [$this->currentUser, $this->regularUser]; } if ($body instanceof TableNode) { $fd = $body->getRowsHash(); @@ -279,7 +288,8 @@ trait BasicStructure { * @param string $user */ public function loggingInUsingWebAs($user) { - $loginUrl = substr($this->baseUrl, 0, -5) . '/index.php/login'; + $baseUrl = substr($this->baseUrl, 0, -5); + $loginUrl = $baseUrl . '/index.php/login'; // Request a new session and extract CSRF token $client = new Client(); $response = $client->get( @@ -302,6 +312,9 @@ trait BasicStructure { 'requesttoken' => $this->requestToken, ], 'cookies' => $this->cookieJar, + 'headers' => [ + 'Origin' => $baseUrl, + ], ] ); $this->extracRequestTokenFromResponse($response); @@ -327,7 +340,7 @@ trait BasicStructure { $fd = $body->getRowsHash(); $options['form_params'] = $fd; } elseif ($body) { - $options = array_merge($options, $body); + $options = array_merge_recursive($options, $body); } $client = new Client(); @@ -415,14 +428,14 @@ trait BasicStructure { } public function createFileSpecificSize($name, $size) { - $file = fopen("work/" . "$name", 'w'); + $file = fopen('work/' . "$name", 'w'); fseek($file, $size - 1, SEEK_CUR); fwrite($file, 'a'); // write a dummy char at SIZE position fclose($file); } public function createFileWithText($name, $text) { - $file = fopen("work/" . "$name", 'w'); + $file = fopen('work/' . "$name", 'w'); fwrite($file, $text); fclose($file); } @@ -458,19 +471,19 @@ trait BasicStructure { */ public static function addFilesToSkeleton() { for ($i = 0; $i < 5; $i++) { - file_put_contents("../../core/skeleton/" . "textfile" . "$i" . ".txt", "Nextcloud test text file\n"); + file_put_contents('../../core/skeleton/' . 'textfile' . "$i" . '.txt', "Nextcloud test text file\n"); } - if (!file_exists("../../core/skeleton/FOLDER")) { - mkdir("../../core/skeleton/FOLDER", 0777, true); + if (!file_exists('../../core/skeleton/FOLDER')) { + mkdir('../../core/skeleton/FOLDER', 0777, true); } - if (!file_exists("../../core/skeleton/PARENT")) { - mkdir("../../core/skeleton/PARENT", 0777, true); + if (!file_exists('../../core/skeleton/PARENT')) { + mkdir('../../core/skeleton/PARENT', 0777, true); } - file_put_contents("../../core/skeleton/PARENT/" . "parent.txt", "Nextcloud test text file\n"); - if (!file_exists("../../core/skeleton/PARENT/CHILD")) { - mkdir("../../core/skeleton/PARENT/CHILD", 0777, true); + file_put_contents('../../core/skeleton/PARENT/' . 'parent.txt', "Nextcloud test text file\n"); + if (!file_exists('../../core/skeleton/PARENT/CHILD')) { + mkdir('../../core/skeleton/PARENT/CHILD', 0777, true); } - file_put_contents("../../core/skeleton/PARENT/CHILD/" . "child.txt", "Nextcloud test text file\n"); + file_put_contents('../../core/skeleton/PARENT/CHILD/' . 'child.txt', "Nextcloud test text file\n"); } /** @@ -478,18 +491,18 @@ trait BasicStructure { */ public static function removeFilesFromSkeleton() { for ($i = 0; $i < 5; $i++) { - self::removeFile("../../core/skeleton/", "textfile" . "$i" . ".txt"); + self::removeFile('../../core/skeleton/', 'textfile' . "$i" . '.txt'); } - if (is_dir("../../core/skeleton/FOLDER")) { - rmdir("../../core/skeleton/FOLDER"); + if (is_dir('../../core/skeleton/FOLDER')) { + rmdir('../../core/skeleton/FOLDER'); } - self::removeFile("../../core/skeleton/PARENT/CHILD/", "child.txt"); - if (is_dir("../../core/skeleton/PARENT/CHILD")) { - rmdir("../../core/skeleton/PARENT/CHILD"); + self::removeFile('../../core/skeleton/PARENT/CHILD/', 'child.txt'); + if (is_dir('../../core/skeleton/PARENT/CHILD')) { + rmdir('../../core/skeleton/PARENT/CHILD'); } - self::removeFile("../../core/skeleton/PARENT/", "parent.txt"); - if (is_dir("../../core/skeleton/PARENT")) { - rmdir("../../core/skeleton/PARENT"); + self::removeFile('../../core/skeleton/PARENT/', 'parent.txt'); + if (is_dir('../../core/skeleton/PARENT')) { + rmdir('../../core/skeleton/PARENT'); } } @@ -497,7 +510,7 @@ trait BasicStructure { * @BeforeScenario @local_storage */ public static function removeFilesFromLocalStorageBefore() { - $dir = "./work/local_storage/"; + $dir = './work/local_storage/'; $di = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS); $ri = new RecursiveIteratorIterator($di, RecursiveIteratorIterator::CHILD_FIRST); foreach ($ri as $file) { @@ -509,7 +522,7 @@ trait BasicStructure { * @AfterScenario @local_storage */ public static function removeFilesFromLocalStorageAfter() { - $dir = "./work/local_storage/"; + $dir = './work/local_storage/'; $di = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS); $ri = new RecursiveIteratorIterator($di, RecursiveIteratorIterator::CHILD_FIRST); foreach ($ri as $file) { diff --git a/build/integration/features/bootstrap/CalDavContext.php b/build/integration/features/bootstrap/CalDavContext.php index a9db5c89d4d..459c35089fa 100644 --- a/build/integration/features/bootstrap/CalDavContext.php +++ b/build/integration/features/bootstrap/CalDavContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -11,7 +12,7 @@ use GuzzleHttp\Exception\GuzzleException; use Psr\Http\Message\ResponseInterface; class CalDavContext implements \Behat\Behat\Context\Context { - /** @var string */ + /** @var string */ private $baseUrl; /** @var Client */ private $client; @@ -41,7 +42,7 @@ class CalDavContext implements \Behat\Behat\Context\Context { /** @AfterScenario */ public function afterScenario() { - $davUrl = $this->baseUrl. '/remote.php/dav/calendars/admin/MyCalendar'; + $davUrl = $this->baseUrl . '/remote.php/dav/calendars/admin/MyCalendar'; try { $this->client->delete( $davUrl, @@ -266,7 +267,7 @@ class CalDavContext implements \Behat\Behat\Context\Context { * @param string $name */ public function createsACalendarNamed($user, $name) { - $davUrl = $this->baseUrl . '/remote.php/dav/calendars/'.$user.'/'.$name; + $davUrl = $this->baseUrl . '/remote.php/dav/calendars/' . $user . '/' . $name; $password = ($user === 'admin') ? 'admin' : '123456'; $this->response = $this->client->request( @@ -289,7 +290,7 @@ class CalDavContext implements \Behat\Behat\Context\Context { * @param string $name */ public function publiclySharesTheCalendarNamed($user, $name) { - $davUrl = $this->baseUrl . '/remote.php/dav/calendars/'.$user.'/'.$name; + $davUrl = $this->baseUrl . '/remote.php/dav/calendars/' . $user . '/' . $name; $password = ($user === 'admin') ? 'admin' : '123456'; $this->response = $this->client->request( diff --git a/build/integration/features/bootstrap/CapabilitiesContext.php b/build/integration/features/bootstrap/CapabilitiesContext.php index 79ede6ac8ba..7d09ab6ddcf 100644 --- a/build/integration/features/bootstrap/CapabilitiesContext.php +++ b/build/integration/features/bootstrap/CapabilitiesContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -22,7 +23,9 @@ class CapabilitiesContext implements Context, SnippetAcceptingContext { * @param \Behat\Gherkin\Node\TableNode|null $formData */ public function checkCapabilitiesResponse(\Behat\Gherkin\Node\TableNode $formData) { - $capabilitiesXML = simplexml_load_string($this->response->getBody())->data->capabilities; + $capabilitiesXML = simplexml_load_string($this->response->getBody()); + Assert::assertNotFalse($capabilitiesXML, 'Failed to fetch capabilities'); + $capabilitiesXML = $capabilitiesXML->data->capabilities; foreach ($formData->getHash() as $row) { $path_to_element = explode('@@@', $row['path_to_element']); @@ -32,9 +35,9 @@ class CapabilitiesContext implements Context, SnippetAcceptingContext { } $answeredValue = (string)$answeredValue; Assert::assertEquals( - $row['value'] === "EMPTY" ? '' : $row['value'], + $row['value'] === 'EMPTY' ? '' : $row['value'], $answeredValue, - "Failed field " . $row['capability'] . " " . $row['path_to_element'] + 'Failed field ' . $row['capability'] . ' ' . $row['path_to_element'] ); } } diff --git a/build/integration/features/bootstrap/CardDavContext.php b/build/integration/features/bootstrap/CardDavContext.php index d5694ec619b..733c98dca02 100644 --- a/build/integration/features/bootstrap/CardDavContext.php +++ b/build/integration/features/bootstrap/CardDavContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -11,7 +12,7 @@ use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Message\ResponseInterface; class CardDavContext implements \Behat\Behat\Context\Context { - /** @var string */ + /** @var string */ private $baseUrl; /** @var Client */ private $client; @@ -110,7 +111,7 @@ class CardDavContext implements \Behat\Behat\Context\Context { * @throws \Exception */ public function createsAnAddressbookNamedWithStatuscode($user, $addressBook, $statusCode) { - $davUrl = $this->baseUrl . '/remote.php/dav/addressbooks/users/'.$user.'/'.$addressBook; + $davUrl = $this->baseUrl . '/remote.php/dav/addressbooks/users/' . $user . '/' . $addressBook; $password = ($user === 'admin') ? 'admin' : '123456'; $this->response = $this->client->request( @@ -123,7 +124,7 @@ class CardDavContext implements \Behat\Behat\Context\Context { <d:prop> <d:resourcetype> <d:collection />,<card:addressbook /> - </d:resourcetype>,<d:displayname>'.$addressBook.'</d:displayname> + </d:resourcetype>,<d:displayname>' . $addressBook . '</d:displayname> </d:prop> </d:set> </d:mkcol>', @@ -190,7 +191,7 @@ class CardDavContext implements \Behat\Behat\Context\Context { * @Given :user uploads the contact :fileName to the addressbook :addressbook */ public function uploadsTheContactToTheAddressbook($user, $fileName, $addressBook) { - $davUrl = $this->baseUrl . '/remote.php/dav/addressbooks/users/'.$user.'/'.$addressBook . '/' . $fileName; + $davUrl = $this->baseUrl . '/remote.php/dav/addressbooks/users/' . $user . '/' . $addressBook . '/' . $fileName; $password = ($user === 'admin') ? 'admin' : '123456'; $this->response = $this->client->request( @@ -223,7 +224,7 @@ class CardDavContext implements \Behat\Behat\Context\Context { * @When Exporting the picture of contact :fileName from addressbook :addressBook as user :user */ public function whenExportingThePictureOfContactFromAddressbookAsUser($fileName, $addressBook, $user) { - $davUrl = $this->baseUrl . '/remote.php/dav/addressbooks/users/'.$user.'/'.$addressBook . '/' . $fileName . '?photo=true'; + $davUrl = $this->baseUrl . '/remote.php/dav/addressbooks/users/' . $user . '/' . $addressBook . '/' . $fileName . '?photo=true'; $password = ($user === 'admin') ? 'admin' : '123456'; try { @@ -249,7 +250,7 @@ class CardDavContext implements \Behat\Behat\Context\Context { * @When Downloading the contact :fileName from addressbook :addressBook as user :user */ public function whenDownloadingTheContactFromAddressbookAsUser($fileName, $addressBook, $user) { - $davUrl = $this->baseUrl . '/remote.php/dav/addressbooks/users/'.$user.'/'.$addressBook . '/' . $fileName; + $davUrl = $this->baseUrl . '/remote.php/dav/addressbooks/users/' . $user . '/' . $addressBook . '/' . $fileName; $password = ($user === 'admin') ? 'admin' : '123456'; try { diff --git a/build/integration/features/bootstrap/ChecksumsContext.php b/build/integration/features/bootstrap/ChecksumsContext.php index 5661015e714..c8abf91127e 100644 --- a/build/integration/features/bootstrap/ChecksumsContext.php +++ b/build/integration/features/bootstrap/ChecksumsContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -10,7 +11,7 @@ use GuzzleHttp\Client; use GuzzleHttp\Message\ResponseInterface; class ChecksumsContext implements \Behat\Behat\Context\Context { - /** @var string */ + /** @var string */ private $baseUrl; /** @var Client */ private $client; @@ -87,7 +88,7 @@ class ChecksumsContext implements \Behat\Behat\Context\Context { */ public function theWebdavResponseShouldHaveAStatusCode($statusCode) { if ((int)$statusCode !== $this->response->getStatusCode()) { - throw new \Exception("Expected $statusCode, got ".$this->response->getStatusCode()); + throw new \Exception("Expected $statusCode, got " . $this->response->getStatusCode()); } } @@ -131,7 +132,7 @@ class ChecksumsContext implements \Behat\Behat\Context\Context { $checksums = $parsed[0]['value'][1]['value'][0]['value'][0]; if ($checksums['value'][0]['value'] !== $checksum) { - throw new \Exception("Expected $checksum, got ".$checksums['value'][0]['value']); + throw new \Exception("Expected $checksum, got " . $checksums['value'][0]['value']); } } @@ -159,7 +160,7 @@ class ChecksumsContext implements \Behat\Behat\Context\Context { */ public function theHeaderChecksumShouldMatch($checksum) { if ($this->response->getHeader('OC-Checksum')[0] !== $checksum) { - throw new \Exception("Expected $checksum, got ".$this->response->getHeader('OC-Checksum')[0]); + throw new \Exception("Expected $checksum, got " . $this->response->getHeader('OC-Checksum')[0]); } } @@ -199,7 +200,7 @@ class ChecksumsContext implements \Behat\Behat\Context\Context { $status = $parsed[0]['value'][1]['value'][1]['value']; if ($status !== 'HTTP/1.1 404 Not Found') { - throw new \Exception("Expected 'HTTP/1.1 404 Not Found', got ".$status); + throw new \Exception("Expected 'HTTP/1.1 404 Not Found', got " . $status); } } @@ -208,34 +209,7 @@ class ChecksumsContext implements \Behat\Behat\Context\Context { */ public function theOcChecksumHeaderShouldNotBeThere() { if ($this->response->hasHeader('OC-Checksum')) { - throw new \Exception("Expected no checksum header but got ".$this->response->getHeader('OC-Checksum')[0]); + throw new \Exception('Expected no checksum header but got ' . $this->response->getHeader('OC-Checksum')[0]); } } - - /** - * @Given user :user uploads chunk file :num of :total with :data to :destination with checksum :checksum - * @param string $user - * @param int $num - * @param int $total - * @param string $data - * @param string $destination - * @param string $checksum - */ - public function userUploadsChunkFileOfWithToWithChecksum($user, $num, $total, $data, $destination, $checksum) { - $num -= 1; - $this->response = $this->client->put( - $this->baseUrl . '/remote.php/webdav' . $destination . '-chunking-42-'.$total.'-'.$num, - [ - 'auth' => [ - $user, - $this->getPasswordForUser($user) - ], - 'body' => $data, - 'headers' => [ - 'OC-Checksum' => $checksum, - 'OC-Chunked' => '1', - ] - ] - ); - } } diff --git a/build/integration/features/bootstrap/CollaborationContext.php b/build/integration/features/bootstrap/CollaborationContext.php index 854abe98b2f..27fa1795c5d 100644 --- a/build/integration/features/bootstrap/CollaborationContext.php +++ b/build/integration/features/bootstrap/CollaborationContext.php @@ -71,7 +71,7 @@ class CollaborationContext implements Context { try { $destination = '/users/admin/myaddressbook'; $data = '<x0:mkcol xmlns:x0="DAV:"><x0:set><x0:prop><x0:resourcetype><x0:collection/><x4:addressbook xmlns:x4="urn:ietf:params:xml:ns:carddav"/></x0:resourcetype><x0:displayname>myaddressbook</x0:displayname></x0:prop></x0:set></x0:mkcol>'; - $this->response = $this->makeDavRequest($this->currentUser, "MKCOL", $destination, ['Content-Type' => 'application/xml'], $data, "addressbooks"); + $this->response = $this->makeDavRequest($this->currentUser, 'MKCOL', $destination, ['Content-Type' => 'application/xml'], $data, 'addressbooks'); } catch (\GuzzleHttp\Exception\ServerException $e) { // 5xx responses cause a server exception $this->response = $e->getResponse(); @@ -93,7 +93,7 @@ EMAIL;TYPE=HOME:user@example.com REV;VALUE=DATE-AND-OR-TIME:20211130T140111Z END:VCARD EOF; - $this->response = $this->makeDavRequest($this->currentUser, "PUT", $destination, [], $data, "addressbooks"); + $this->response = $this->makeDavRequest($this->currentUser, 'PUT', $destination, [], $data, 'addressbooks'); } catch (\GuzzleHttp\Exception\ServerException $e) { // 5xx responses cause a server exception $this->response = $e->getResponse(); diff --git a/build/integration/features/bootstrap/CommandLine.php b/build/integration/features/bootstrap/CommandLine.php index 84b3dfd447f..924d723daa6 100644 --- a/build/integration/features/bootstrap/CommandLine.php +++ b/build/integration/features/bootstrap/CommandLine.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/CommandLineContext.php b/build/integration/features/bootstrap/CommandLineContext.php index 47a85885ce4..e7764356270 100644 --- a/build/integration/features/bootstrap/CommandLineContext.php +++ b/build/integration/features/bootstrap/CommandLineContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -6,6 +7,7 @@ */ require __DIR__ . '/../../vendor/autoload.php'; +use Behat\Behat\Context\Exception\ContextNotFoundException; use Behat\Behat\Hook\Scope\BeforeScenarioScope; use PHPUnit\Framework\Assert; @@ -41,8 +43,12 @@ class CommandLineContext implements \Behat\Behat\Context\Context { /** @BeforeScenario */ public function gatherContexts(BeforeScenarioScope $scope) { $environment = $scope->getEnvironment(); - // this should really be "WebDavContext" ... - $this->featureContext = $environment->getContext('FeatureContext'); + // this should really be "WebDavContext" + try { + $this->featureContext = $environment->getContext('FeatureContext'); + } catch (ContextNotFoundException) { + $this->featureContext = $environment->getContext('DavFeatureContext'); + } } private function findLastTransferFolderForUser($sourceUser, $targetUser) { @@ -104,19 +110,6 @@ class CommandLineContext implements \Behat\Behat\Context\Context { } /** - * @When /^transferring ownership of path "([^"]+)" from "([^"]+)" to "([^"]+)" with received shares$/ - */ - public function transferringOwnershipPathWithIncomingShares($path, $user1, $user2) { - $path = '--path=' . $path; - if ($this->runOcc(['files:transfer-ownership', $path, $user1, $user2, '--transfer-incoming-shares=1']) === 0) { - $this->lastTransferPath = $this->findLastTransferFolderForUser($user1, $user2); - } else { - // failure - $this->lastTransferPath = null; - } - } - - /** * @When /^using received transfer folder of "([^"]+)" as dav path$/ */ public function usingTransferFolderAsDavPath($user) { diff --git a/build/integration/features/bootstrap/CommentsContext.php b/build/integration/features/bootstrap/CommentsContext.php index 8d7b0fe0c2f..53001b1c204 100644 --- a/build/integration/features/bootstrap/CommentsContext.php +++ b/build/integration/features/bootstrap/CommentsContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -29,8 +30,6 @@ class CommentsContext implements \Behat\Behat\Context\Context { } } - - /** * get a named entry from response instead of picking a random entry from values * @@ -138,7 +137,7 @@ class CommentsContext implements \Behat\Behat\Context\Context { } if ($res->getStatusCode() !== (int)$statusCode) { - throw new \Exception("Response status code was not $statusCode (" . $res->getStatusCode() . ")"); + throw new \Exception("Response status code was not $statusCode (" . $res->getStatusCode() . ')'); } } @@ -180,13 +179,13 @@ class CommentsContext implements \Behat\Behat\Context\Context { } if ($res->getStatusCode() !== (int)$statusCode) { - throw new \Exception("Response status code was not $statusCode (" . $res->getStatusCode() . ")"); + throw new \Exception("Response status code was not $statusCode (" . $res->getStatusCode() . ')'); } if ($res->getStatusCode() === 207) { $service = new Sabre\Xml\Service(); $this->response = $service->parse($res->getBody()->getContents()); - $this->commentId = (int) ($this->getValueFromNamedEntries('{DAV:}response {DAV:}propstat {DAV:}prop {http://owncloud.org/ns}id', $this->response ?? []) ?? 0); + $this->commentId = (int)($this->getValueFromNamedEntries('{DAV:}response {DAV:}propstat {DAV:}prop {http://owncloud.org/ns}id', $this->response ?? []) ?? 0); } } @@ -238,7 +237,7 @@ class CommentsContext implements \Behat\Behat\Context\Context { } if ($res->getStatusCode() !== (int)$statusCode) { - throw new \Exception("Response status code was not $statusCode (" . $res->getStatusCode() . ")"); + throw new \Exception("Response status code was not $statusCode (" . $res->getStatusCode() . ')'); } } @@ -275,7 +274,7 @@ class CommentsContext implements \Behat\Behat\Context\Context { $count = count($this->response); } if ($count !== (int)$number) { - throw new \Exception("Found more comments than $number (" . $count . ")"); + throw new \Exception("Found more comments than $number (" . $count . ')'); } } @@ -305,7 +304,7 @@ class CommentsContext implements \Behat\Behat\Context\Context { } if ($res->getStatusCode() !== (int)$statusCode) { - throw new \Exception("Response status code was not $statusCode (" . $res->getStatusCode() . ")"); + throw new \Exception("Response status code was not $statusCode (" . $res->getStatusCode() . ')'); } } } diff --git a/build/integration/features/bootstrap/ContactsMenu.php b/build/integration/features/bootstrap/ContactsMenu.php index 4fc3c03c5e9..f6bf6b9422b 100644 --- a/build/integration/features/bootstrap/ContactsMenu.php +++ b/build/integration/features/bootstrap/ContactsMenu.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/ConversionsContext.php b/build/integration/features/bootstrap/ConversionsContext.php new file mode 100644 index 00000000000..ccd14c460f8 --- /dev/null +++ b/build/integration/features/bootstrap/ConversionsContext.php @@ -0,0 +1,60 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +require __DIR__ . '/../../vendor/autoload.php'; + +use Behat\Behat\Context\Context; +use Behat\Behat\Context\SnippetAcceptingContext; +use Behat\Gherkin\Node\TableNode; + +class ConversionsContext implements Context, SnippetAcceptingContext { + use AppConfiguration; + use BasicStructure; + use WebDav; + + /** @BeforeScenario */ + public function setUpScenario() { + $this->asAn('admin'); + $this->setStatusTestingApp(true); + } + + /** @AfterScenario */ + public function tearDownScenario() { + $this->asAn('admin'); + $this->setStatusTestingApp(false); + } + + protected function resetAppConfigs() { + } + + /** + * @When /^user "([^"]*)" converts file "([^"]*)" to "([^"]*)"$/ + */ + public function userConvertsTheSavedFileId(string $user, string $path, string $mime) { + $this->userConvertsTheSavedFileIdTo($user, $path, $mime, null); + } + + /** + * @When /^user "([^"]*)" converts file "([^"]*)" to "([^"]*)" and saves it to "([^"]*)"$/ + */ + public function userConvertsTheSavedFileIdTo(string $user, string $path, string $mime, ?string $destination) { + try { + $fileId = $this->getFileIdForPath($user, $path); + } catch (Exception $e) { + // return a fake value to keep going and be able to test the error + $fileId = 0; + } + + $data = [['fileId', $fileId], ['targetMimeType', $mime]]; + if ($destination !== null) { + $data[] = ['destination', $destination]; + } + + $this->asAn($user); + $this->sendingToWith('post', '/apps/files/api/v1/convert', new TableNode($data)); + } +} diff --git a/build/integration/features/bootstrap/DavFeatureContext.php b/build/integration/features/bootstrap/DavFeatureContext.php new file mode 100644 index 00000000000..ec6085cff98 --- /dev/null +++ b/build/integration/features/bootstrap/DavFeatureContext.php @@ -0,0 +1,24 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +use Behat\Behat\Context\Context; +use Behat\Behat\Context\SnippetAcceptingContext; + +require __DIR__ . '/../../vendor/autoload.php'; + +class DavFeatureContext implements Context, SnippetAcceptingContext { + use AppConfiguration; + use ContactsMenu; + use ExternalStorage; + use Search; + use WebDav; + use Trashbin; + + protected function resetAppConfigs() { + $this->deleteServerConfig('files_sharing', 'outgoing_server2server_share_enabled'); + } +} diff --git a/build/integration/features/bootstrap/Download.php b/build/integration/features/bootstrap/Download.php index aa10830427a..549a033346e 100644 --- a/build/integration/features/bootstrap/Download.php +++ b/build/integration/features/bootstrap/Download.php @@ -1,14 +1,16 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ use PHPUnit\Framework\Assert; +use Psr\Http\Message\StreamInterface; require __DIR__ . '/../../vendor/autoload.php'; trait Download { - /** @var string **/ + /** @var string * */ private $downloadedFile; /** @AfterScenario **/ @@ -20,16 +22,16 @@ trait Download { * @When user :user downloads zip file for entries :entries in folder :folder */ public function userDownloadsZipFileForEntriesInFolder($user, $entries, $folder) { + $folder = trim($folder, '/'); $this->asAn($user); - $this->sendingToDirectUrl('GET', "/index.php/apps/files/ajax/download.php?dir=" . $folder . "&files=[" . $entries . "]"); + $this->sendingToDirectUrl('GET', "/remote.php/dav/files/$user/$folder?accept=zip&files=[" . $entries . ']'); $this->theHTTPStatusCodeShouldBe('200'); - - $this->getDownloadedFile(); } private function getDownloadedFile() { $this->downloadedFile = ''; + /** @var StreamInterface */ $body = $this->response->getBody(); while (!$body->eof()) { $this->downloadedFile .= $body->read(8192); @@ -38,14 +40,28 @@ trait Download { } /** + * @Then the downloaded file is a zip file + */ + public function theDownloadedFileIsAZipFile() { + $this->getDownloadedFile(); + + Assert::assertTrue( + strpos($this->downloadedFile, "\x50\x4B\x01\x02") !== false, + 'File does not contain the central directory file header' + ); + } + + /** * @Then the downloaded zip file is a zip32 file */ public function theDownloadedZipFileIsAZip32File() { + $this->theDownloadedFileIsAZipFile(); + // assertNotContains is not used to prevent the whole file from being // printed in case of error. Assert::assertTrue( strpos($this->downloadedFile, "\x50\x4B\x06\x06") === false, - "File contains the zip64 end of central dir signature" + 'File contains the zip64 end of central dir signature' ); } @@ -53,11 +69,13 @@ trait Download { * @Then the downloaded zip file is a zip64 file */ public function theDownloadedZipFileIsAZip64File() { + $this->theDownloadedFileIsAZipFile(); + // assertNotContains is not used to prevent the whole file from being // printed in case of error. Assert::assertTrue( strpos($this->downloadedFile, "\x50\x4B\x06\x06") !== false, - "File does not contain the zip64 end of central dir signature" + 'File does not contain the zip64 end of central dir signature' ); } @@ -77,7 +95,7 @@ trait Download { // in case of error and to be able to get the extra field length. Assert::assertEquals( 1, preg_match($fileHeaderRegExp, $this->downloadedFile, $matches), - "Local header for file did not appear once in zip file" + 'Local header for file did not appear once in zip file' ); $extraFieldLength = unpack('vextraFieldLength', $matches[1])['extraFieldLength']; @@ -97,7 +115,7 @@ trait Download { // in case of error. Assert::assertEquals( 1, preg_match($fileHeaderAndContentRegExp, $this->downloadedFile), - "Local header and contents for file did not appear once in zip file" + 'Local header and contents for file did not appear once in zip file' ); } @@ -117,7 +135,21 @@ trait Download { // in case of error. Assert::assertEquals( 1, preg_match($folderHeaderRegExp, $this->downloadedFile), - "Local header for folder did not appear once in zip file" + 'Local header for folder did not appear once in zip file' + ); + } + + /** + * @Then the downloaded file has the content of :sourceFilename from :user data + */ + public function theDownloadedFileHasContentOfUserFile($sourceFilename, $user) { + $this->getDownloadedFile(); + $expectedFileContents = file_get_contents($this->getDataDirectory() . "/$user/files" . $sourceFilename); + + // prevent the whole file from being printed in case of error. + Assert::assertEquals( + 0, strcmp($expectedFileContents, $this->downloadedFile), + 'Downloaded file content does not match local file content' ); } } diff --git a/build/integration/features/bootstrap/ExternalStorage.php b/build/integration/features/bootstrap/ExternalStorage.php new file mode 100644 index 00000000000..8fe2653a026 --- /dev/null +++ b/build/integration/features/bootstrap/ExternalStorage.php @@ -0,0 +1,123 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +use Behat\Gherkin\Node\TableNode; +use PHPUnit\Framework\Assert; + +require __DIR__ . '/../../vendor/autoload.php'; + +trait ExternalStorage { + private array $storageIds = []; + + private array $lastExternalStorageData; + + /** + * @AfterScenario + **/ + public function deleteCreatedStorages(): void { + foreach ($this->storageIds as $storageId) { + $this->deleteStorage($storageId); + } + $this->storageIds = []; + } + + private function deleteStorage(string $storageId): void { + // Based on "runOcc" from CommandLine trait + $args = ['files_external:delete', '--yes', $storageId]; + $args = array_map(function ($arg) { + return escapeshellarg($arg); + }, $args); + $args[] = '--no-ansi --no-warnings'; + $args = implode(' ', $args); + + $descriptor = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + $process = proc_open('php console.php ' . $args, $descriptor, $pipes, $ocPath = '../..'); + $lastStdOut = stream_get_contents($pipes[1]); + proc_close($process); + } + + /** + * @When logged in user creates external global storage + * + * @param TableNode $fields + */ + public function loggedInUserCreatesExternalGlobalStorage(TableNode $fields): void { + $this->sendJsonWithRequestTokenAndBasicAuth('POST', '/index.php/apps/files_external/globalstorages', $fields); + $this->theHTTPStatusCodeShouldBe('201'); + + $this->lastExternalStorageData = json_decode($this->response->getBody(), $asAssociativeArray = true); + + $this->storageIds[] = $this->lastExternalStorageData['id']; + } + + /** + * @When logged in user updates last external userglobal storage + * + * @param TableNode $fields + */ + public function loggedInUserUpdatesLastExternalUserglobalStorage(TableNode $fields): void { + $this->sendJsonWithRequestTokenAndBasicAuth('PUT', '/index.php/apps/files_external/userglobalstorages/' . $this->lastExternalStorageData['id'], $fields); + $this->theHTTPStatusCodeShouldBe('200'); + + $this->lastExternalStorageData = json_decode($this->response->getBody(), $asAssociativeArray = true); + } + + /** + * @Then fields of last external storage match with + * + * @param TableNode $fields + */ + public function fieldsOfLastExternalStorageMatchWith(TableNode $fields): void { + foreach ($fields->getRowsHash() as $expectedField => $expectedValue) { + if (!array_key_exists($expectedField, $this->lastExternalStorageData)) { + Assert::fail("$expectedField was not found in response"); + } + + Assert::assertEquals($expectedValue, $this->lastExternalStorageData[$expectedField], "Field '$expectedField' does not match ({$this->lastExternalStorageData[$expectedField]})"); + } + } + + private function sendJsonWithRequestToken(string $method, string $url, TableNode $fields): void { + $isFirstField = true; + $fieldsAsJsonString = '{'; + foreach ($fields->getRowsHash() as $key => $value) { + $fieldsAsJsonString .= ($isFirstField ? '' : ',') . '"' . $key . '":' . $value; + $isFirstField = false; + } + $fieldsAsJsonString .= '}'; + + $body = [ + 'headers' => [ + 'Content-Type' => 'application/json', + ], + 'body' => $fieldsAsJsonString, + ]; + $this->sendingAToWithRequesttoken($method, $url, $body); + } + + private function sendJsonWithRequestTokenAndBasicAuth(string $method, string $url, TableNode $fields): void { + $isFirstField = true; + $fieldsAsJsonString = '{'; + foreach ($fields->getRowsHash() as $key => $value) { + $fieldsAsJsonString .= ($isFirstField ? '' : ',') . '"' . $key . '":' . $value; + $isFirstField = false; + } + $fieldsAsJsonString .= '}'; + + $body = [ + 'headers' => [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Basic ' . base64_encode('admin:admin'), + ], + 'body' => $fieldsAsJsonString, + ]; + $this->sendingAToWithRequesttoken($method, $url, $body); + } +} diff --git a/build/integration/features/bootstrap/FakeSMTPHelper.php b/build/integration/features/bootstrap/FakeSMTPHelper.php index 2d90494b82c..32387869edd 100644 --- a/build/integration/features/bootstrap/FakeSMTPHelper.php +++ b/build/integration/features/bootstrap/FakeSMTPHelper.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -34,7 +35,7 @@ class fakeSMTP { $hasValidTo = false; $receivingData = false; $header = true; - $this->reply('220 '.$this->serverHello); + $this->reply('220 ' . $this->serverHello); $this->mail['ipaddress'] = $this->detectIP(); while ($data = fgets($this->fd)) { $data = preg_replace('@\r\n@', "\n", $data); @@ -60,7 +61,7 @@ class fakeSMTP { $this->reply('250 2.1.5 Ok'); $hasValidTo = true; } else { - $this->reply('501 5.1.3 Bad recipient address syntax '.$match[1]); + $this->reply('501 5.1.3 Bad recipient address syntax ' . $match[1]); } } } elseif (!$receivingData && preg_match('/^RSET$/i', trim($data))) { @@ -70,7 +71,7 @@ class fakeSMTP { } elseif (!$receivingData && preg_match('/^NOOP$/i', trim($data))) { $this->reply('250 2.0.0 Ok'); } elseif (!$receivingData && preg_match('/^VRFY (.*)/i', trim($data), $match)) { - $this->reply('250 2.0.0 '.$match[1]); + $this->reply('250 2.0.0 ' . $match[1]); } elseif (!$receivingData && preg_match('/^DATA/i', trim($data))) { if (!$hasValidTo) { $this->reply('503 5.5.1 Error: need RCPT command'); @@ -79,7 +80,7 @@ class fakeSMTP { $receivingData = true; } } elseif (!$receivingData && preg_match('/^(HELO|EHLO)/i', $data)) { - $this->reply('250 HELO '.$this->mail['ipaddress']); + $this->reply('250 HELO ' . $this->mail['ipaddress']); } elseif (!$receivingData && preg_match('/^QUIT/i', trim($data))) { break; } elseif (!$receivingData) { @@ -88,7 +89,7 @@ class fakeSMTP { } elseif ($receivingData && $data == ".\n") { /* Email Received, now let's look at it */ $receivingData = false; - $this->reply('250 2.0.0 Ok: queued as '.$this->generateRandom(10)); + $this->reply('250 2.0.0 Ok: queued as ' . $this->generateRandom(10)); $splitmail = explode("\n\n", $this->mail['rawEmail'], 2); if (count($splitmail) == 2) { $this->mail['emailHeaders'] = $splitmail[0]; @@ -109,14 +110,14 @@ class fakeSMTP { } } /* Say good bye */ - $this->reply('221 2.0.0 Bye '.$this->mail['ipaddress']); + $this->reply('221 2.0.0 Bye ' . $this->mail['ipaddress']); fclose($this->fd); } public function log($s) { if ($this->logFile) { - file_put_contents($this->logFile, trim($s)."\n", FILE_APPEND); + file_put_contents($this->logFile, trim($s) . "\n", FILE_APPEND); } } diff --git a/build/integration/features/bootstrap/FeatureContext.php b/build/integration/features/bootstrap/FeatureContext.php index 638772da0b7..ab37556f931 100644 --- a/build/integration/features/bootstrap/FeatureContext.php +++ b/build/integration/features/bootstrap/FeatureContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -9,13 +10,20 @@ use Behat\Behat\Context\SnippetAcceptingContext; require __DIR__ . '/../../vendor/autoload.php'; - /** * Features context. */ class FeatureContext implements Context, SnippetAcceptingContext { + use AppConfiguration; use ContactsMenu; + use ExternalStorage; use Search; use WebDav; use Trashbin; + + protected function resetAppConfigs(): void { + $this->deleteServerConfig('bruteForce', 'whitelist_0'); + $this->deleteServerConfig('bruteForce', 'whitelist_1'); + $this->deleteServerConfig('bruteforcesettings', 'apply_allowlist_to_ratelimit'); + } } diff --git a/build/integration/features/bootstrap/FederationContext.php b/build/integration/features/bootstrap/FederationContext.php index 03a03ef2fd6..95dc8119ad6 100644 --- a/build/integration/features/bootstrap/FederationContext.php +++ b/build/integration/features/bootstrap/FederationContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -7,6 +8,7 @@ use Behat\Behat\Context\Context; use Behat\Behat\Context\SnippetAcceptingContext; use Behat\Gherkin\Node\TableNode; +use PHPUnit\Framework\Assert; require __DIR__ . '/../../vendor/autoload.php'; @@ -38,7 +40,7 @@ class FederationContext implements Context, SnippetAcceptingContext { $port = getenv('PORT_FED'); - self::$phpFederatedServerPid = exec('php -S localhost:' . $port . ' -t ../../ >/dev/null & echo $!'); + self::$phpFederatedServerPid = exec('PHP_CLI_SERVER_WORKERS=2 php -S localhost:' . $port . ' -t ../../ >/dev/null & echo $!'); } /** @@ -64,7 +66,7 @@ class FederationContext implements Context, SnippetAcceptingContext { * @param string $shareeServer "LOCAL" or "REMOTE" */ public function federateSharing($sharerUser, $sharerServer, $sharerPath, $shareeUser, $shareeServer) { - if ($shareeServer == "REMOTE") { + if ($shareeServer == 'REMOTE') { $shareWith = "$shareeUser@" . substr($this->remoteBaseUrl, 0, -4); } else { $shareWith = "$shareeUser@" . substr($this->localBaseUrl, 0, -4); @@ -85,7 +87,7 @@ class FederationContext implements Context, SnippetAcceptingContext { * @param string $shareeServer "LOCAL" or "REMOTE" */ public function federateGroupSharing($sharerUser, $sharerServer, $sharerPath, $shareeGroup, $shareeServer) { - if ($shareeServer == "REMOTE") { + if ($shareeServer == 'REMOTE') { $shareWith = "$shareeGroup@" . substr($this->remoteBaseUrl, 0, -4); } else { $shareWith = "$shareeGroup@" . substr($this->localBaseUrl, 0, -4); @@ -134,7 +136,7 @@ class FederationContext implements Context, SnippetAcceptingContext { public function acceptLastPendingShare($user, $server) { $previous = $this->usingServer($server); $this->asAn($user); - $this->sendingToWith('GET', "/apps/files_sharing/api/v1/remote_shares/pending", null); + $this->sendingToWith('GET', '/apps/files_sharing/api/v1/remote_shares/pending', null); $this->theHTTPStatusCodeShouldBe('200'); $this->theOCSStatusCodeShouldBe('100'); $share_id = simplexml_load_string($this->response->getBody())->data[0]->element[0]->id; @@ -152,7 +154,7 @@ class FederationContext implements Context, SnippetAcceptingContext { */ public function deleteLastAcceptedRemoteShare($user) { $this->asAn($user); - $this->sendingToWith('DELETE', "/apps/files_sharing/api/v1/remote_shares/" . $this->lastAcceptedRemoteShareId, null); + $this->sendingToWith('DELETE', '/apps/files_sharing/api/v1/remote_shares/' . $this->lastAcceptedRemoteShareId, null); } /** @@ -168,8 +170,52 @@ class FederationContext implements Context, SnippetAcceptingContext { self::$phpFederatedServerPid = ''; } + /** + * @BeforeScenario @TrustedFederation + */ + public function theServersAreTrustingEachOther() { + $this->asAn('admin'); + // Trust the remote server on the local server + $this->usingServer('LOCAL'); + $this->sendRequestForJSON('POST', '/apps/federation/trusted-servers', ['url' => 'http://localhost:' . getenv('PORT')]); + Assert::assertTrue(($this->response->getStatusCode() === 200 || $this->response->getStatusCode() === 409)); + + // Trust the local server on the remote server + $this->usingServer('REMOTE'); + $this->sendRequestForJSON('POST', '/apps/federation/trusted-servers', ['url' => 'http://localhost:' . getenv('PORT_FED')]); + // If the server is already trusted, we expect a 409 + Assert::assertTrue(($this->response->getStatusCode() === 200 || $this->response->getStatusCode() === 409)); + } + + /** + * @AfterScenario @TrustedFederation + */ + public function theServersAreNoLongerTrustingEachOther() { + $this->asAn('admin'); + // Untrust the remote servers on the local server + $this->usingServer('LOCAL'); + $this->sendRequestForJSON('GET', '/apps/federation/trusted-servers'); + $this->theHTTPStatusCodeShouldBe('200'); + $trustedServersIDs = array_map(fn ($server) => $server->id, json_decode($this->response->getBody())->ocs->data); + foreach ($trustedServersIDs as $id) { + $this->sendRequestForJSON('DELETE', '/apps/federation/trusted-servers/' . $id); + $this->theHTTPStatusCodeShouldBe('200'); + } + + // Untrust the local server on the remote server + $this->usingServer('REMOTE'); + $this->sendRequestForJSON('GET', '/apps/federation/trusted-servers'); + $this->theHTTPStatusCodeShouldBe('200'); + $trustedServersIDs = array_map(fn ($server) => $server->id, json_decode($this->response->getBody())->ocs->data); + foreach ($trustedServersIDs as $id) { + $this->sendRequestForJSON('DELETE', '/apps/federation/trusted-servers/' . $id); + $this->theHTTPStatusCodeShouldBe('200'); + } + } + protected function resetAppConfigs() { $this->deleteServerConfig('files_sharing', 'incoming_server2server_group_share_enabled'); $this->deleteServerConfig('files_sharing', 'outgoing_server2server_group_share_enabled'); + $this->deleteServerConfig('files_sharing', 'federated_trusted_share_auto_accept'); } } diff --git a/build/integration/features/bootstrap/FilesDropContext.php b/build/integration/features/bootstrap/FilesDropContext.php index b611e55ea0f..0c437f28a72 100644 --- a/build/integration/features/bootstrap/FilesDropContext.php +++ b/build/integration/features/bootstrap/FilesDropContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -15,7 +16,7 @@ class FilesDropContext implements Context, SnippetAcceptingContext { /** * @When Dropping file :path with :content */ - public function droppingFileWith($path, $content) { + public function droppingFileWith($path, $content, $nickname = null) { $client = new Client(); $options = []; if (count($this->lastShareData->data->element) > 0) { @@ -25,11 +26,16 @@ class FilesDropContext implements Context, SnippetAcceptingContext { } $base = substr($this->baseUrl, 0, -4); - $fullUrl = $base . "/public.php/dav/files/$token/$path"; + $fullUrl = str_replace('//', '/', $base . "/public.php/dav/files/$token/$path"); $options['headers'] = [ - 'X-REQUESTED-WITH' => 'XMLHttpRequest' + 'X-REQUESTED-WITH' => 'XMLHttpRequest', ]; + + if ($nickname) { + $options['headers']['X-NC-NICKNAME'] = $nickname; + } + $options['body'] = \GuzzleHttp\Psr7\Utils::streamFor($content); try { @@ -39,10 +45,19 @@ class FilesDropContext implements Context, SnippetAcceptingContext { } } + + /** + * @When Dropping file :path with :content as :nickName + */ + public function droppingFileWithAs($path, $content, $nickname) { + $this->droppingFileWith($path, $content, $nickname); + } + + /** * @When Creating folder :folder in drop */ - public function creatingFolderInDrop($folder) { + public function creatingFolderInDrop($folder, $nickname = null) { $client = new Client(); $options = []; if (count($this->lastShareData->data->element) > 0) { @@ -52,16 +67,28 @@ class FilesDropContext implements Context, SnippetAcceptingContext { } $base = substr($this->baseUrl, 0, -4); - $fullUrl = $base . "/public.php/dav/files/$token/$folder"; + $fullUrl = str_replace('//', '/', $base . "/public.php/dav/files/$token/$folder"); $options['headers'] = [ - 'X-REQUESTED-WITH' => 'XMLHttpRequest' + 'X-REQUESTED-WITH' => 'XMLHttpRequest', ]; + if ($nickname) { + $options['headers']['X-NC-NICKNAME'] = $nickname; + } + try { $this->response = $client->request('MKCOL', $fullUrl, $options); } catch (\GuzzleHttp\Exception\ClientException $e) { $this->response = $e->getResponse(); } } + + + /** + * @When Creating folder :folder in drop as :nickName + */ + public function creatingFolderInDropWithNickname($folder, $nickname) { + return $this->creatingFolderInDrop($folder, $nickname); + } } diff --git a/build/integration/features/bootstrap/LDAPContext.php b/build/integration/features/bootstrap/LDAPContext.php index f0181b36c71..986dced77a1 100644 --- a/build/integration/features/bootstrap/LDAPContext.php +++ b/build/integration/features/bootstrap/LDAPContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/Mail.php b/build/integration/features/bootstrap/Mail.php index 5e5a75fdc26..d48ed6399c5 100644 --- a/build/integration/features/bootstrap/Mail.php +++ b/build/integration/features/bootstrap/Mail.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -20,7 +21,7 @@ trait Mail { return; } - exec("kill " . $this->fakeSmtpServerPid); + exec('kill ' . $this->fakeSmtpServerPid); $this->invokingTheCommand('config:system:delete mail_smtpport'); } @@ -33,6 +34,6 @@ trait Mail { // FakeSMTP uses 2525 instead. $this->invokingTheCommand('config:system:set mail_smtpport --value=2525 --type integer'); - $this->fakeSmtpServerPid = exec("php features/bootstrap/FakeSMTPHelper.php >/dev/null 2>&1 & echo $!"); + $this->fakeSmtpServerPid = exec('php features/bootstrap/FakeSMTPHelper.php >/dev/null 2>&1 & echo $!'); } } diff --git a/build/integration/features/bootstrap/MetadataContext.php b/build/integration/features/bootstrap/MetadataContext.php new file mode 100644 index 00000000000..32042590c86 --- /dev/null +++ b/build/integration/features/bootstrap/MetadataContext.php @@ -0,0 +1,124 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +use Behat\Behat\Context\Context; +use Behat\Step\Then; +use Behat\Step\When; +use PHPUnit\Framework\Assert; +use Sabre\DAV\Client as SClient; + +require __DIR__ . '/../../vendor/autoload.php'; + +class MetadataContext implements Context { + private string $davPath = '/remote.php/dav'; + + public function __construct( + private string $baseUrl, + private array $admin, + private string $regular_user_password, + ) { + // in case of ci deployment we take the server url from the environment + $testServerUrl = getenv('TEST_SERVER_URL'); + if ($testServerUrl !== false) { + $this->baseUrl = substr($testServerUrl, 0, -5); + } + } + + #[When('User :user sets the :metadataKey prop with value :metadataValue on :fileName')] + public function userSetsProp(string $user, string $metadataKey, string $metadataValue, string $fileName) { + $client = new SClient([ + 'baseUri' => $this->baseUrl, + 'userName' => $user, + 'password' => '123456', + 'authType' => SClient::AUTH_BASIC, + ]); + + $body = '<?xml version="1.0"?> +<d:propertyupdate xmlns:d="DAV:" xmlns:nc="http://nextcloud.com/ns"> + <d:set> + <d:prop> + <nc:' . $metadataKey . '>' . $metadataValue . '</nc:' . $metadataKey . '> + </d:prop> + </d:set> +</d:propertyupdate>'; + + $davUrl = $this->getDavUrl($user, $fileName); + $client->request('PROPPATCH', $this->baseUrl . $davUrl, $body); + } + + #[When('User :user deletes the :metadataKey prop on :fileName')] + public function userDeletesProp(string $user, string $metadataKey, string $fileName) { + $client = new SClient([ + 'baseUri' => $this->baseUrl, + 'userName' => $user, + 'password' => '123456', + 'authType' => SClient::AUTH_BASIC, + ]); + + $body = '<?xml version="1.0"?> +<d:propertyupdate xmlns:d="DAV:" xmlns:nc="http://nextcloud.com/ns"> + <d:remove> + <d:prop> + <nc:' . $metadataKey . '></nc:' . $metadataKey . '> + </d:prop> + </d:remove> +</d:propertyupdate>'; + + $davUrl = $this->getDavUrl($user, $fileName); + $client->request('PROPPATCH', $this->baseUrl . $davUrl, $body); + } + + #[Then('User :user should see the prop :metadataKey equal to :metadataValue for file :fileName')] + public function checkPropForFile(string $user, string $metadataKey, string $metadataValue, string $fileName) { + $client = new SClient([ + 'baseUri' => $this->baseUrl, + 'userName' => $user, + 'password' => '123456', + 'authType' => SClient::AUTH_BASIC, + ]); + + $body = '<?xml version="1.0"?> +<d:propfind xmlns:d="DAV:" xmlns:nc="http://nextcloud.com/ns"> + <d:prop> + <nc:' . $metadataKey . '></nc:' . $metadataKey . '> + </d:prop> +</d:propfind>'; + + $davUrl = $this->getDavUrl($user, $fileName); + $response = $client->request('PROPFIND', $this->baseUrl . $davUrl, $body); + $parsedResponse = $client->parseMultistatus($response['body']); + + Assert::assertEquals($parsedResponse[$davUrl]['200']['{http://nextcloud.com/ns}' . $metadataKey], $metadataValue); + } + + #[Then('User :user should not see the prop :metadataKey for file :fileName')] + public function checkPropDoesNotExistsForFile(string $user, string $metadataKey, string $fileName) { + $client = new SClient([ + 'baseUri' => $this->baseUrl, + 'userName' => $user, + 'password' => '123456', + 'authType' => SClient::AUTH_BASIC, + ]); + + $body = '<?xml version="1.0"?> +<d:propfind xmlns:d="DAV:" xmlns:nc="http://nextcloud.com/ns"> + <d:prop> + <nc:' . $metadataKey . '></nc:' . $metadataKey . '> + </d:prop> +</d:propfind>'; + + $davUrl = $this->getDavUrl($user, $fileName); + $response = $client->request('PROPFIND', $this->baseUrl . $davUrl, $body); + $parsedResponse = $client->parseMultistatus($response['body']); + + Assert::assertEquals($parsedResponse[$davUrl]['404']['{http://nextcloud.com/ns}' . $metadataKey], null); + } + + private function getDavUrl(string $user, string $fileName) { + return $this->davPath . '/files/' . $user . $fileName; + } +} diff --git a/build/integration/features/bootstrap/PrincipalPropertySearchContext.php b/build/integration/features/bootstrap/PrincipalPropertySearchContext.php new file mode 100644 index 00000000000..9dfd9379240 --- /dev/null +++ b/build/integration/features/bootstrap/PrincipalPropertySearchContext.php @@ -0,0 +1,141 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +require __DIR__ . '/../../vendor/autoload.php'; + +use Behat\Behat\Context\Context; +use GuzzleHttp\BodySummarizer; +use GuzzleHttp\Client; +use GuzzleHttp\HandlerStack; +use GuzzleHttp\Middleware; +use GuzzleHttp\Utils; +use Psr\Http\Message\ResponseInterface; + +class PrincipalPropertySearchContext implements Context { + private string $baseUrl; + private Client $client; + private ResponseInterface $response; + + public function __construct(string $baseUrl) { + $this->baseUrl = $baseUrl; + + // in case of ci deployment we take the server url from the environment + $testServerUrl = getenv('TEST_SERVER_URL'); + if ($testServerUrl !== false) { + $this->baseUrl = substr($testServerUrl, 0, -5); + } + } + + /** @BeforeScenario */ + public function setUpScenario(): void { + $this->client = $this->createGuzzleInstance(); + } + + /** + * Create a Guzzle client with a higher truncateAt value to read full error responses. + */ + private function createGuzzleInstance(): Client { + $bodySummarizer = new BodySummarizer(2048); + + $stack = new HandlerStack(Utils::chooseHandler()); + $stack->push(Middleware::httpErrors($bodySummarizer), 'http_errors'); + $stack->push(Middleware::redirect(), 'allow_redirects'); + $stack->push(Middleware::cookies(), 'cookies'); + $stack->push(Middleware::prepareBody(), 'prepare_body'); + + return new Client(['handler' => $stack]); + } + + /** + * @When searching for a principal matching :match + * @param string $match + * @throws \Exception + */ + public function principalPropertySearch(string $match) { + $davUrl = $this->baseUrl . '/remote.php/dav/'; + $user = 'admin'; + $password = 'admin'; + + $this->response = $this->client->request( + 'REPORT', + $davUrl, + [ + 'body' => '<x0:principal-property-search xmlns:x0="DAV:" test="anyof"> + <x0:property-search> + <x0:prop> + <x0:displayname/> + <x2:email-address xmlns:x2="http://sabredav.org/ns"/> + </x0:prop> + <x0:match>' . $match . '</x0:match> + </x0:property-search> + <x0:prop> + <x0:displayname/> + <x1:calendar-user-type xmlns:x1="urn:ietf:params:xml:ns:caldav"/> + <x1:calendar-user-address-set xmlns:x1="urn:ietf:params:xml:ns:caldav"/> + <x0:principal-URL/> + <x0:alternate-URI-set/> + <x2:email-address xmlns:x2="http://sabredav.org/ns"/> + <x3:language xmlns:x3="http://nextcloud.com/ns"/> + <x1:calendar-home-set xmlns:x1="urn:ietf:params:xml:ns:caldav"/> + <x1:schedule-inbox-URL xmlns:x1="urn:ietf:params:xml:ns:caldav"/> + <x1:schedule-outbox-URL xmlns:x1="urn:ietf:params:xml:ns:caldav"/> + <x1:schedule-default-calendar-URL xmlns:x1="urn:ietf:params:xml:ns:caldav"/> + <x3:resource-type xmlns:x3="http://nextcloud.com/ns"/> + <x3:resource-vehicle-type xmlns:x3="http://nextcloud.com/ns"/> + <x3:resource-vehicle-make xmlns:x3="http://nextcloud.com/ns"/> + <x3:resource-vehicle-model xmlns:x3="http://nextcloud.com/ns"/> + <x3:resource-vehicle-is-electric xmlns:x3="http://nextcloud.com/ns"/> + <x3:resource-vehicle-range xmlns:x3="http://nextcloud.com/ns"/> + <x3:resource-vehicle-seating-capacity xmlns:x3="http://nextcloud.com/ns"/> + <x3:resource-contact-person xmlns:x3="http://nextcloud.com/ns"/> + <x3:resource-contact-person-vcard xmlns:x3="http://nextcloud.com/ns"/> + <x3:room-type xmlns:x3="http://nextcloud.com/ns"/> + <x3:room-seating-capacity xmlns:x3="http://nextcloud.com/ns"/> + <x3:room-building-address xmlns:x3="http://nextcloud.com/ns"/> + <x3:room-building-story xmlns:x3="http://nextcloud.com/ns"/> + <x3:room-building-room-number xmlns:x3="http://nextcloud.com/ns"/> + <x3:room-features xmlns:x3="http://nextcloud.com/ns"/> + </x0:prop> + <x0:apply-to-principal-collection-set/> +</x0:principal-property-search> +', + 'auth' => [ + $user, + $password, + ], + 'headers' => [ + 'Content-Type' => 'application/xml; charset=UTF-8', + 'Depth' => '0', + ], + ] + ); + } + + /** + * @Then The search HTTP status code should be :code + * @param string $code + * @throws \Exception + */ + public function theHttpStatusCodeShouldBe(string $code): void { + if ((int)$code !== $this->response->getStatusCode()) { + throw new \Exception('Expected ' . (int)$code . ' got ' . $this->response->getStatusCode()); + } + } + + /** + * @Then The search response should contain :needle + * @param string $needle + * @throws \Exception + */ + public function theResponseShouldContain(string $needle): void { + $body = $this->response->getBody()->getContents(); + + if (str_contains($body, $needle) === false) { + throw new \Exception('Response does not contain "' . $needle . '"'); + } + } +} diff --git a/build/integration/features/bootstrap/Provisioning.php b/build/integration/features/bootstrap/Provisioning.php index 2fb1c807cc5..935ad2a4a1d 100644 --- a/build/integration/features/bootstrap/Provisioning.php +++ b/build/integration/features/bootstrap/Provisioning.php @@ -1,9 +1,12 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. * SPDX-License-Identifier: AGPL-3.0-or-later */ + +use Behat\Gherkin\Node\TableNode; use GuzzleHttp\Client; use GuzzleHttp\Message\ResponseInterface; use PHPUnit\Framework\Assert; @@ -34,7 +37,7 @@ trait Provisioning { $this->userExists($user); } catch (\GuzzleHttp\Exception\ClientException $ex) { $previous_user = $this->currentUser; - $this->currentUser = "admin"; + $this->currentUser = 'admin'; $this->creatingTheUser($user); $this->currentUser = $previous_user; } @@ -51,7 +54,7 @@ trait Provisioning { $this->userExists($user); } catch (\GuzzleHttp\Exception\ClientException $ex) { $previous_user = $this->currentUser; - $this->currentUser = "admin"; + $this->currentUser = 'admin'; $this->creatingTheUser($user, $displayname); $this->currentUser = $previous_user; } @@ -72,7 +75,7 @@ trait Provisioning { return; } $previous_user = $this->currentUser; - $this->currentUser = "admin"; + $this->currentUser = 'admin'; $this->deletingTheUser($user); $this->currentUser = $previous_user; try { @@ -124,7 +127,7 @@ trait Provisioning { * @Then /^user "([^"]*)" has$/ * * @param string $user - * @param \Behat\Gherkin\Node\TableNode|null $settings + * @param TableNode|null $settings */ public function userHasSetting($user, $settings) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user"; @@ -145,12 +148,43 @@ trait Provisioning { if (isset($value['element']) && in_array($setting[0], ['additional_mail', 'additional_mailScope'], true)) { $expectedValues = explode(';', $setting[1]); foreach ($expectedValues as $expected) { - Assert::assertTrue(in_array($expected, $value['element'], true)); + Assert::assertTrue(in_array($expected, $value['element'], true), 'Data wrong for field: ' . $setting[0]); } } elseif (isset($value[0])) { - Assert::assertEqualsCanonicalizing($setting[1], $value[0]); + Assert::assertEqualsCanonicalizing($setting[1], $value[0], 'Data wrong for field: ' . $setting[0]); } else { - Assert::assertEquals('', $setting[1]); + Assert::assertEquals('', $setting[1], 'Data wrong for field: ' . $setting[0]); + } + } + } + + /** + * @Then /^user "([^"]*)" has the following profile data$/ + */ + public function userHasProfileData(string $user, ?TableNode $settings): void { + $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/profile/$user"; + $client = new Client(); + $options = []; + if ($this->currentUser === 'admin') { + $options['auth'] = $this->adminUser; + } else { + $options['auth'] = [$this->currentUser, $this->regularUser]; + } + $options['headers'] = [ + 'OCS-APIREQUEST' => 'true', + 'Accept' => 'application/json', + ]; + + $response = $client->get($fullUrl, $options); + $body = $response->getBody()->getContents(); + $data = json_decode($body, true); + $data = $data['ocs']['data']; + foreach ($settings->getRows() as $setting) { + Assert::assertArrayHasKey($setting[0], $data, 'Profile data field missing: ' . $setting[0]); + if ($setting[1] === 'NULL') { + Assert::assertNull($data[$setting[0]], 'Profile data wrong for field: ' . $setting[0]); + } else { + Assert::assertEquals($setting[1], $data[$setting[0]], 'Profile data wrong for field: ' . $setting[0]); } } } @@ -159,7 +193,7 @@ trait Provisioning { * @Then /^group "([^"]*)" has$/ * * @param string $user - * @param \Behat\Gherkin\Node\TableNode|null $settings + * @param TableNode|null $settings */ public function groupHasSetting($group, $settings) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/groups/details?search=$group"; @@ -191,7 +225,7 @@ trait Provisioning { * @Then /^user "([^"]*)" has editable fields$/ * * @param string $user - * @param \Behat\Gherkin\Node\TableNode|null $fields + * @param TableNode|null $fields */ public function userHasEditableFields($user, $fields) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/user/fields"; @@ -221,9 +255,9 @@ trait Provisioning { * @Then /^search users by phone for region "([^"]*)" with$/ * * @param string $user - * @param \Behat\Gherkin\Node\TableNode|null $settings + * @param TableNode|null $settings */ - public function searchUserByPhone($region, \Behat\Gherkin\Node\TableNode $searchTable) { + public function searchUserByPhone($region, TableNode $searchTable) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/search/by-phone"; $client = new Client(); $options = []; @@ -250,7 +284,7 @@ trait Provisioning { public function createUser($user) { $previous_user = $this->currentUser; - $this->currentUser = "admin"; + $this->currentUser = 'admin'; $this->creatingTheUser($user); $this->userExists($user); $this->currentUser = $previous_user; @@ -258,7 +292,7 @@ trait Provisioning { public function deleteUser($user) { $previous_user = $this->currentUser; - $this->currentUser = "admin"; + $this->currentUser = 'admin'; $this->deletingTheUser($user); $this->userDoesNotExist($user); $this->currentUser = $previous_user; @@ -266,7 +300,7 @@ trait Provisioning { public function createGroup($group) { $previous_user = $this->currentUser; - $this->currentUser = "admin"; + $this->currentUser = 'admin'; $this->creatingTheGroup($group); $this->groupExists($group); $this->currentUser = $previous_user; @@ -274,7 +308,7 @@ trait Provisioning { public function deleteGroup($group) { $previous_user = $this->currentUser; - $this->currentUser = "admin"; + $this->currentUser = 'admin'; $this->deletingTheGroup($group); $this->groupDoesNotExist($group); $this->currentUser = $previous_user; @@ -343,7 +377,7 @@ trait Provisioning { */ public function assureUserBelongsToGroup($user, $group) { $previous_user = $this->currentUser; - $this->currentUser = "admin"; + $this->currentUser = 'admin'; if (!$this->userBelongsToGroup($user, $group)) { $this->addingUserToGroup($user, $group); @@ -522,7 +556,7 @@ trait Provisioning { $this->groupExists($group); } catch (\GuzzleHttp\Exception\ClientException $ex) { $previous_user = $this->currentUser; - $this->currentUser = "admin"; + $this->currentUser = 'admin'; $this->creatingTheGroup($group); $this->currentUser = $previous_user; } @@ -543,7 +577,7 @@ trait Provisioning { return; } $previous_user = $this->currentUser; - $this->currentUser = "admin"; + $this->currentUser = 'admin'; $this->deletingTheGroup($group); $this->currentUser = $previous_user; try { @@ -624,10 +658,10 @@ trait Provisioning { /** * @Then /^users returned are$/ - * @param \Behat\Gherkin\Node\TableNode|null $usersList + * @param TableNode|null $usersList */ public function theUsersShouldBe($usersList) { - if ($usersList instanceof \Behat\Gherkin\Node\TableNode) { + if ($usersList instanceof TableNode) { $users = $usersList->getRows(); $usersSimplified = $this->simplifyArray($users); $respondedArray = $this->getArrayOfUsersResponded($this->response); @@ -637,10 +671,10 @@ trait Provisioning { /** * @Then /^phone matches returned are$/ - * @param \Behat\Gherkin\Node\TableNode|null $usersList + * @param TableNode|null $usersList */ public function thePhoneUsersShouldBe($usersList) { - if ($usersList instanceof \Behat\Gherkin\Node\TableNode) { + if ($usersList instanceof TableNode) { $users = $usersList->getRowsHash(); $listCheckedElements = simplexml_load_string($this->response->getBody())->data; $respondedArray = json_decode(json_encode($listCheckedElements), true); @@ -650,10 +684,10 @@ trait Provisioning { /** * @Then /^detailed users returned are$/ - * @param \Behat\Gherkin\Node\TableNode|null $usersList + * @param TableNode|null $usersList */ public function theDetailedUsersShouldBe($usersList) { - if ($usersList instanceof \Behat\Gherkin\Node\TableNode) { + if ($usersList instanceof TableNode) { $users = $usersList->getRows(); $usersSimplified = $this->simplifyArray($users); $respondedArray = $this->getArrayOfDetailedUsersResponded($this->response); @@ -664,10 +698,10 @@ trait Provisioning { /** * @Then /^groups returned are$/ - * @param \Behat\Gherkin\Node\TableNode|null $groupsList + * @param TableNode|null $groupsList */ public function theGroupsShouldBe($groupsList) { - if ($groupsList instanceof \Behat\Gherkin\Node\TableNode) { + if ($groupsList instanceof TableNode) { $groups = $groupsList->getRows(); $groupsSimplified = $this->simplifyArray($groups); $respondedArray = $this->getArrayOfGroupsResponded($this->response); @@ -677,10 +711,10 @@ trait Provisioning { /** * @Then /^subadmin groups returned are$/ - * @param \Behat\Gherkin\Node\TableNode|null $groupsList + * @param TableNode|null $groupsList */ public function theSubadminGroupsShouldBe($groupsList) { - if ($groupsList instanceof \Behat\Gherkin\Node\TableNode) { + if ($groupsList instanceof TableNode) { $groups = $groupsList->getRows(); $groupsSimplified = $this->simplifyArray($groups); $respondedArray = $this->getArrayOfSubadminsResponded($this->response); @@ -690,10 +724,10 @@ trait Provisioning { /** * @Then /^apps returned are$/ - * @param \Behat\Gherkin\Node\TableNode|null $appList + * @param TableNode|null $appList */ public function theAppsShouldBe($appList) { - if ($appList instanceof \Behat\Gherkin\Node\TableNode) { + if ($appList instanceof TableNode) { $apps = $appList->getRows(); $appsSimplified = $this->simplifyArray($apps); $respondedArray = $this->getArrayOfAppsResponded($this->response); @@ -703,7 +737,7 @@ trait Provisioning { /** * @Then /^subadmin users returned are$/ - * @param \Behat\Gherkin\Node\TableNode|null $groupsList + * @param TableNode|null $groupsList */ public function theSubadminUsersShouldBe($groupsList) { $this->theSubadminGroupsShouldBe($groupsList); @@ -775,7 +809,7 @@ trait Provisioning { * @param string $app */ public function appIsDisabled($app) { - $fullUrl = $this->baseUrl . "v2.php/cloud/apps?filter=disabled"; + $fullUrl = $this->baseUrl . 'v2.php/cloud/apps?filter=disabled'; $client = new Client(); $options = []; if ($this->currentUser === 'admin') { @@ -796,7 +830,7 @@ trait Provisioning { * @param string $app */ public function appIsEnabled($app) { - $fullUrl = $this->baseUrl . "v2.php/cloud/apps?filter=enabled"; + $fullUrl = $this->baseUrl . 'v2.php/cloud/apps?filter=enabled'; $client = new Client(); $options = []; if ($this->currentUser === 'admin') { @@ -820,7 +854,7 @@ trait Provisioning { * @param string $app */ public function appIsNotEnabled($app) { - $fullUrl = $this->baseUrl . "v2.php/cloud/apps?filter=enabled"; + $fullUrl = $this->baseUrl . 'v2.php/cloud/apps?filter=enabled'; $client = new Client(); $options = []; if ($this->currentUser === 'admin') { @@ -873,7 +907,7 @@ trait Provisioning { $this->response = $client->get($fullUrl, $options); // boolean to string is integer - Assert::assertEquals("1", simplexml_load_string($this->response->getBody())->data[0]->enabled); + Assert::assertEquals('1', simplexml_load_string($this->response->getBody())->data[0]->enabled); } /** @@ -882,13 +916,13 @@ trait Provisioning { * @param string $quota */ public function userHasAQuotaOf($user, $quota) { - $body = new \Behat\Gherkin\Node\TableNode([ + $body = new TableNode([ 0 => ['key', 'quota'], 1 => ['value', $quota], ]); // method used from BasicStructure trait - $this->sendingToWith("PUT", "/cloud/users/" . $user, $body); + $this->sendingToWith('PUT', '/cloud/users/' . $user, $body); } /** @@ -950,7 +984,7 @@ trait Provisioning { /** * @Then /^user "([^"]*)" has not$/ */ - public function userHasNotSetting($user, \Behat\Gherkin\Node\TableNode $settings) { + public function userHasNotSetting($user, TableNode $settings) { $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user"; $client = new Client(); $options = []; diff --git a/build/integration/features/bootstrap/RateLimitingContext.php b/build/integration/features/bootstrap/RateLimitingContext.php new file mode 100644 index 00000000000..15c8c5c8379 --- /dev/null +++ b/build/integration/features/bootstrap/RateLimitingContext.php @@ -0,0 +1,31 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +use Behat\Behat\Context\Context; + +class RateLimitingContext implements Context { + use BasicStructure; + use CommandLine; + use Provisioning; + + /** + * @BeforeScenario @RateLimiting + */ + public function enableRateLimiting() { + // Enable rate limiting for the tests. + // Ratelimiting is disabled by default, so we need to enable it + $this->runOcc(['config:system:set', 'ratelimit.protection.enabled', '--value', 'true', '--type', 'bool']); + } + + /** + * @AfterScenario @RateLimiting + */ + public function disableRateLimiting() { + // Restore the default rate limiting configuration. + // Ratelimiting is disabled by default, so we need to disable it + $this->runOcc(['config:system:set', 'ratelimit.protection.enabled', '--value', 'false', '--type', 'bool']); + } +} diff --git a/build/integration/features/bootstrap/RemoteContext.php b/build/integration/features/bootstrap/RemoteContext.php index b1605e4da25..6102f686ea7 100644 --- a/build/integration/features/bootstrap/RemoteContext.php +++ b/build/integration/features/bootstrap/RemoteContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -41,7 +42,7 @@ class RemoteContext implements Context { * @param string $remoteServer "NON_EXISTING" or "REMOTE" */ public function selectRemoteInstance($remoteServer) { - if ($remoteServer == "REMOTE") { + if ($remoteServer == 'REMOTE') { $baseUri = $this->remoteUrl; } else { $baseUri = 'nonexistingnextcloudserver.local'; diff --git a/build/integration/features/bootstrap/RoutingContext.php b/build/integration/features/bootstrap/RoutingContext.php new file mode 100644 index 00000000000..762570547e0 --- /dev/null +++ b/build/integration/features/bootstrap/RoutingContext.php @@ -0,0 +1,19 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +use Behat\Behat\Context\Context; +use Behat\Behat\Context\SnippetAcceptingContext; + +require __DIR__ . '/../../vendor/autoload.php'; + +class RoutingContext implements Context, SnippetAcceptingContext { + use Provisioning; + use AppConfiguration; + use CommandLine; + + protected function resetAppConfigs(): void { + } +} diff --git a/build/integration/features/bootstrap/Search.php b/build/integration/features/bootstrap/Search.php index 47259be769c..49a4fe92822 100644 --- a/build/integration/features/bootstrap/Search.php +++ b/build/integration/features/bootstrap/Search.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/SetupContext.php b/build/integration/features/bootstrap/SetupContext.php index 96cb00d8601..aa131cec597 100644 --- a/build/integration/features/bootstrap/SetupContext.php +++ b/build/integration/features/bootstrap/SetupContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/ShareesContext.php b/build/integration/features/bootstrap/ShareesContext.php index e152a749bfa..37e0e63e547 100644 --- a/build/integration/features/bootstrap/ShareesContext.php +++ b/build/integration/features/bootstrap/ShareesContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/build/integration/features/bootstrap/Sharing.php b/build/integration/features/bootstrap/Sharing.php index c0affcb9cda..0cc490ff110 100644 --- a/build/integration/features/bootstrap/Sharing.php +++ b/build/integration/features/bootstrap/Sharing.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -54,15 +55,19 @@ trait Sharing { $fd = $body->getRowsHash(); if (array_key_exists('expireDate', $fd)) { $dateModification = $fd['expireDate']; - if (!empty($dateModification)) { + if ($dateModification === 'null') { + $fd['expireDate'] = null; + } elseif (!empty($dateModification)) { $fd['expireDate'] = date('Y-m-d', strtotime($dateModification)); + } else { + $fd['expireDate'] = ''; } } $options['form_params'] = $fd; } try { - $this->response = $client->request("POST", $fullUrl, $options); + $this->response = $client->request('POST', $fullUrl, $options); } catch (\GuzzleHttp\Exception\ClientException $ex) { $this->response = $ex->getResponse(); } @@ -98,7 +103,7 @@ trait Sharing { public function acceptingLastShare() { $share_id = $this->lastShareData->data[0]->id; $url = "/apps/files_sharing/api/v{$this->sharingApiVersion}/shares/pending/$share_id"; - $this->sendingToWith("POST", $url, null); + $this->sendingToWith('POST', $url, null); $this->theHTTPStatusCodeShouldBe('200'); } @@ -118,7 +123,7 @@ trait Sharing { $share_id = $this->lastShareData->data[0]->id; $url = "/apps/files_sharing/api/v{$this->sharingApiVersion}/shares/pending/$share_id"; - $this->sendingToWith("POST", $url, null); + $this->sendingToWith('POST', $url, null); $this->currentUser = $previousUser; @@ -134,7 +139,7 @@ trait Sharing { } else { $url = $this->lastShareData->data->url; } - $fullUrl = $url . "/download"; + $fullUrl = $url . '/download'; $this->checkDownload($fullUrl, null, 'text/plain'); } @@ -148,7 +153,7 @@ trait Sharing { $token = $this->lastShareData->data->token; } - $fullUrl = substr($this->baseUrl, 0, -4) . "index.php/s/" . $token . "/download"; + $fullUrl = substr($this->baseUrl, 0, -4) . 'index.php/s/' . $token . '/download'; $this->checkDownload($fullUrl, null, 'text/plain'); } @@ -194,7 +199,7 @@ trait Sharing { * @When /^Adding expiration date to last share$/ */ public function addingExpirationDate() { - $share_id = (string) $this->lastShareData->data[0]->id; + $share_id = (string)$this->lastShareData->data[0]->id; $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/apps/files_sharing/api/v{$this->sharingApiVersion}/shares/$share_id"; $client = new Client(); $options = []; @@ -203,9 +208,9 @@ trait Sharing { } else { $options['auth'] = [$this->currentUser, $this->regularUser]; } - $date = date('Y-m-d', strtotime("+3 days")); + $date = date('Y-m-d', strtotime('+3 days')); $options['form_params'] = ['expireDate' => $date]; - $this->response = $this->response = $client->request("PUT", $fullUrl, $options); + $this->response = $this->response = $client->request('PUT', $fullUrl, $options); Assert::assertEquals(200, $this->response->getStatusCode()); } @@ -214,7 +219,7 @@ trait Sharing { * @param TableNode|null $body */ public function updatingLastShare($body) { - $share_id = (string) $this->lastShareData->data[0]->id; + $share_id = (string)$this->lastShareData->data[0]->id; $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/apps/files_sharing/api/v{$this->sharingApiVersion}/shares/$share_id"; $client = new Client(); $options = [ @@ -238,7 +243,7 @@ trait Sharing { } try { - $this->response = $client->request("PUT", $fullUrl, $options); + $this->response = $client->request('PUT', $fullUrl, $options); } catch (\GuzzleHttp\Exception\ClientException $ex) { $this->response = $ex->getResponse(); } @@ -286,13 +291,13 @@ trait Sharing { } if ($viewOnly === true) { - $body['attributes'] = json_encode([['scope' => 'permissions', 'key' => 'download', 'enabled' => false]]); + $body['attributes'] = json_encode([['scope' => 'permissions', 'key' => 'download', 'value' => false]]); } $options['form_params'] = $body; try { - $this->response = $client->request("POST", $fullUrl, $options); + $this->response = $client->request('POST', $fullUrl, $options); $this->lastShareData = simplexml_load_string($this->response->getBody()); } catch (\GuzzleHttp\Exception\ClientException $ex) { $this->response = $ex->getResponse(); @@ -303,18 +308,18 @@ trait Sharing { public function isFieldInResponse($field, $contentExpected) { $data = simplexml_load_string($this->response->getBody())->data[0]; if ((string)$field == 'expiration') { - if(!empty($contentExpected)) { - $contentExpected = date('Y-m-d', strtotime($contentExpected)) . " 00:00:00"; + if (!empty($contentExpected)) { + $contentExpected = date('Y-m-d', strtotime($contentExpected)) . ' 00:00:00'; } } if (count($data->element) > 0) { foreach ($data as $element) { - if ($contentExpected == "A_TOKEN") { + if ($contentExpected == 'A_TOKEN') { return (strlen((string)$element->$field) == 15); - } elseif ($contentExpected == "A_NUMBER") { + } elseif ($contentExpected == 'A_NUMBER') { return is_numeric((string)$element->$field); - } elseif ($contentExpected == "AN_URL") { - return $this->isExpectedUrl((string)$element->$field, "index.php/s/"); + } elseif ($contentExpected == 'AN_URL') { + return $this->isExpectedUrl((string)$element->$field, 'index.php/s/'); } elseif ((string)$element->$field == $contentExpected) { return true; } else { @@ -324,14 +329,16 @@ trait Sharing { return false; } else { - if ($contentExpected == "A_TOKEN") { + if ($contentExpected == 'A_TOKEN') { return (strlen((string)$data->$field) == 15); - } elseif ($contentExpected == "A_NUMBER") { + } elseif ($contentExpected == 'A_NUMBER') { return is_numeric((string)$data->$field); - } elseif ($contentExpected == "AN_URL") { - return $this->isExpectedUrl((string)$data->$field, "index.php/s/"); + } elseif ($contentExpected == 'AN_URL') { + return $this->isExpectedUrl((string)$data->$field, 'index.php/s/'); } elseif ($contentExpected == $data->$field) { return true; + } else { + print($data->$field); } return false; } @@ -455,7 +462,7 @@ trait Sharing { public function deletingLastShare() { $share_id = $this->lastShareData->data[0]->id; $url = "/apps/files_sharing/api/v{$this->sharingApiVersion}/shares/$share_id"; - $this->sendingToWith("DELETE", $url, null); + $this->sendingToWith('DELETE', $url, null); } /** @@ -464,7 +471,7 @@ trait Sharing { public function gettingInfoOfLastShare() { $share_id = $this->lastShareData->data[0]->id; $url = "/apps/files_sharing/api/v{$this->sharingApiVersion}/shares/$share_id"; - $this->sendingToWith("GET", $url, null); + $this->sendingToWith('GET', $url, null); } /** @@ -496,13 +503,13 @@ trait Sharing { $fd = $body->getRowsHash(); foreach ($fd as $field => $value) { - if (substr($field, 0, 10) === "share_with") { - $value = str_replace("REMOTE", substr($this->remoteBaseUrl, 0, -5), $value); - $value = str_replace("LOCAL", substr($this->localBaseUrl, 0, -5), $value); + if (substr($field, 0, 10) === 'share_with') { + $value = str_replace('REMOTE', substr($this->remoteBaseUrl, 0, -5), $value); + $value = str_replace('LOCAL', substr($this->localBaseUrl, 0, -5), $value); } - if (substr($field, 0, 6) === "remote") { - $value = str_replace("REMOTE", substr($this->remoteBaseUrl, 0, -4), $value); - $value = str_replace("LOCAL", substr($this->localBaseUrl, 0, -4), $value); + if (substr($field, 0, 6) === 'remote') { + $value = str_replace('REMOTE', substr($this->remoteBaseUrl, 0, -4), $value); + $value = str_replace('LOCAL', substr($this->localBaseUrl, 0, -4), $value); } if (!$this->isFieldInResponse($field, $value)) { Assert::fail("$field" . " doesn't have value " . "$value"); @@ -557,18 +564,18 @@ trait Sharing { ]; $expectedFields = array_merge($defaultExpectedFields, $body->getRowsHash()); - if (!array_key_exists('uid_file_owner', $expectedFields) && - array_key_exists('uid_owner', $expectedFields)) { + if (!array_key_exists('uid_file_owner', $expectedFields) + && array_key_exists('uid_owner', $expectedFields)) { $expectedFields['uid_file_owner'] = $expectedFields['uid_owner']; } - if (!array_key_exists('displayname_file_owner', $expectedFields) && - array_key_exists('displayname_owner', $expectedFields)) { + if (!array_key_exists('displayname_file_owner', $expectedFields) + && array_key_exists('displayname_owner', $expectedFields)) { $expectedFields['displayname_file_owner'] = $expectedFields['displayname_owner']; } - if (array_key_exists('share_type', $expectedFields) && - $expectedFields['share_type'] == 10 /* IShare::TYPE_ROOM */ && - array_key_exists('share_with', $expectedFields)) { + if (array_key_exists('share_type', $expectedFields) + && $expectedFields['share_type'] == 10 /* IShare::TYPE_ROOM */ + && array_key_exists('share_with', $expectedFields)) { if ($expectedFields['share_with'] === 'private_conversation') { $expectedFields['share_with'] = 'REGEXP /^private_conversation_[0-9a-f]{6}$/'; } else { @@ -603,7 +610,7 @@ trait Sharing { } if ($field === 'expiration' && !empty($contentExpected)) { - $contentExpected = date('Y-m-d', strtotime($contentExpected)) . " 00:00:00"; + $contentExpected = date('Y-m-d', strtotime($contentExpected)) . ' 00:00:00'; } if ($contentExpected === 'A_NUMBER') { diff --git a/build/integration/features/bootstrap/SharingContext.php b/build/integration/features/bootstrap/SharingContext.php index 97c2a35ad84..a9dd99108a9 100644 --- a/build/integration/features/bootstrap/SharingContext.php +++ b/build/integration/features/bootstrap/SharingContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -17,6 +18,7 @@ class SharingContext implements Context, SnippetAcceptingContext { use Trashbin; use AppConfiguration; use CommandLine; + use Activity; protected function resetAppConfigs() { $this->deleteServerConfig('core', 'shareapi_default_permissions'); @@ -27,6 +29,9 @@ class SharingContext implements Context, SnippetAcceptingContext { $this->deleteServerConfig('core', 'shareapi_default_expire_date'); $this->deleteServerConfig('core', 'shareapi_expire_after_n_days'); $this->deleteServerConfig('core', 'link_defaultExpDays'); + $this->deleteServerConfig('core', 'shareapi_allow_federation_on_public_shares'); + $this->deleteServerConfig('files_sharing', 'outgoing_server2server_share_enabled'); + $this->deleteServerConfig('core', 'shareapi_allow_view_without_download'); $this->runOcc(['config:system:delete', 'share_folder']); } diff --git a/build/integration/features/bootstrap/TagsContext.php b/build/integration/features/bootstrap/TagsContext.php index 3a2c0e41744..c64626de68d 100644 --- a/build/integration/features/bootstrap/TagsContext.php +++ b/build/integration/features/bootstrap/TagsContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -245,7 +246,7 @@ class TagsContext implements \Behat\Behat\Context\Context { if (count($table->getRows()) !== count($tags)) { throw new \Exception( sprintf( - "Expected %s tags, got %s.", + 'Expected %s tags, got %s.', count($table->getRows()), count($tags) ) @@ -255,9 +256,9 @@ class TagsContext implements \Behat\Behat\Context\Context { foreach ($table->getRowsHash() as $rowDisplayName => $row) { foreach ($tags as $key => $tag) { if ( - $tag['display-name'] === $rowDisplayName && - $tag['user-visible'] === $row[0] && - $tag['user-assignable'] === $row[1] + $tag['display-name'] === $rowDisplayName + && $tag['user-visible'] === $row[0] + && $tag['user-assignable'] === $row[1] ) { unset($tags[$key]); } diff --git a/build/integration/features/bootstrap/TalkContext.php b/build/integration/features/bootstrap/TalkContext.php index fe248e1af7c..6f351c30ccf 100644 --- a/build/integration/features/bootstrap/TalkContext.php +++ b/build/integration/features/bootstrap/TalkContext.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/build/integration/features/bootstrap/Theming.php b/build/integration/features/bootstrap/Theming.php new file mode 100644 index 00000000000..f44a6533a1b --- /dev/null +++ b/build/integration/features/bootstrap/Theming.php @@ -0,0 +1,49 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +require __DIR__ . '/../../vendor/autoload.php'; + +trait Theming { + + private bool $undoAllThemingChangesAfterScenario = false; + + /** + * @AfterScenario + */ + public function undoAllThemingChanges() { + if (!$this->undoAllThemingChangesAfterScenario) { + return; + } + + $this->loggingInUsingWebAs('admin'); + $this->sendingAToWithRequesttoken('POST', '/index.php/apps/theming/ajax/undoAllChanges'); + + $this->undoAllThemingChangesAfterScenario = false; + } + + /** + * @When logged in admin uploads theming image for :key from file :source + * + * @param string $key + * @param string $source + */ + public function loggedInAdminUploadsThemingImageForFromFile(string $key, string $source) { + $this->undoAllThemingChangesAfterScenario = true; + + $file = \GuzzleHttp\Psr7\Utils::streamFor(fopen($source, 'r')); + + $this->sendingAToWithRequesttoken('POST', '/index.php/apps/theming/ajax/uploadImage?key=' . $key, + [ + 'multipart' => [ + [ + 'name' => 'image', + 'contents' => $file + ] + ] + ]); + $this->theHTTPStatusCodeShouldBe('200'); + } +} diff --git a/build/integration/features/bootstrap/Trashbin.php b/build/integration/features/bootstrap/Trashbin.php index 1b1be71bace..dfcc23289a7 100644 --- a/build/integration/features/bootstrap/Trashbin.php +++ b/build/integration/features/bootstrap/Trashbin.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2017 ownCloud GmbH @@ -97,7 +98,7 @@ trait Trashbin { foreach ($elementsSimplified as $expectedElement) { $expectedElement = ltrim($expectedElement, '/'); if (array_search($expectedElement, $trashContent) === false) { - Assert::fail("$expectedElement" . " is not in trash listing"); + Assert::fail("$expectedElement" . ' is not in trash listing'); } } } diff --git a/build/integration/features/bootstrap/WebDav.php b/build/integration/features/bootstrap/WebDav.php index c2de207284d..2cb37002ac0 100644 --- a/build/integration/features/bootstrap/WebDav.php +++ b/build/integration/features/bootstrap/WebDav.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -17,7 +18,7 @@ require __DIR__ . '/../../vendor/autoload.php'; trait WebDav { use Sharing; - private string $davPath = "remote.php/webdav"; + private string $davPath = 'remote.php/webdav'; private bool $usingOldDavPath = true; private ?array $storedETAG = null; // map with user as key and another map as value, which has path as key and etag as value private ?int $storedFileID = null; @@ -40,7 +41,7 @@ trait WebDav { * @Given /^using old dav path$/ */ public function usingOldDavPath() { - $this->davPath = "remote.php/webdav"; + $this->davPath = 'remote.php/webdav'; $this->usingOldDavPath = true; } @@ -48,7 +49,15 @@ trait WebDav { * @Given /^using new dav path$/ */ public function usingNewDavPath() { - $this->davPath = "remote.php/dav"; + $this->davPath = 'remote.php/dav'; + $this->usingOldDavPath = false; + } + + /** + * @Given /^using new public dav path$/ + */ + public function usingNewPublicDavPath() { + $this->davPath = 'public.php/dav'; $this->usingOldDavPath = false; } @@ -60,13 +69,13 @@ trait WebDav { } } - public function makeDavRequest($user, $method, $path, $headers, $body = null, $type = "files") { - if ($type === "files") { + public function makeDavRequest($user, $method, $path, $headers, $body = null, $type = 'files') { + if ($type === 'files') { $fullUrl = substr($this->baseUrl, 0, -4) . $this->getDavFilesPath($user) . "$path"; - } elseif ($type === "uploads") { + } elseif ($type === 'uploads') { $fullUrl = substr($this->baseUrl, 0, -4) . $this->davPath . "$path"; } else { - $fullUrl = substr($this->baseUrl, 0, -4) . $this->davPath . '/' . $type . "$path"; + $fullUrl = substr($this->baseUrl, 0, -4) . $this->davPath . '/' . $type . "$path"; } $client = new GClient(); $options = [ @@ -75,7 +84,7 @@ trait WebDav { ]; if ($user === 'admin') { $options['auth'] = $this->adminUser; - } else { + } elseif ($user !== '') { $options['auth'] = [$user, $this->regularUser]; } return $client->request($method, $fullUrl, $options); @@ -90,7 +99,7 @@ trait WebDav { public function userMovedFile($user, $entry, $fileSource, $fileDestination) { $fullUrl = substr($this->baseUrl, 0, -4) . $this->getDavFilesPath($user); $headers['Destination'] = $fullUrl . $fileDestination; - $this->response = $this->makeDavRequest($user, "MOVE", $fileSource, $headers); + $this->response = $this->makeDavRequest($user, 'MOVE', $fileSource, $headers); Assert::assertEquals(201, $this->response->getStatusCode()); } @@ -104,7 +113,7 @@ trait WebDav { $fullUrl = substr($this->baseUrl, 0, -4) . $this->getDavFilesPath($user); $headers['Destination'] = $fullUrl . $fileDestination; try { - $this->response = $this->makeDavRequest($user, "MOVE", $fileSource, $headers); + $this->response = $this->makeDavRequest($user, 'MOVE', $fileSource, $headers); } catch (\GuzzleHttp\Exception\ClientException $e) { $this->response = $e->getResponse(); } @@ -134,7 +143,7 @@ trait WebDav { */ public function downloadFileWithRange($fileSource, $range) { $headers['Range'] = $range; - $this->response = $this->makeDavRequest($this->currentUser, "GET", $fileSource, $headers); + $this->response = $this->makeDavRequest($this->currentUser, 'GET', $fileSource, $headers); } /** @@ -151,7 +160,7 @@ trait WebDav { 'Range' => $range ]; - $this->response = $client->request("GET", $fullUrl, $options); + $this->response = $client->request('GET', $fullUrl, $options); } /** @@ -169,7 +178,7 @@ trait WebDav { ] ]; - $this->response = $client->request("GET", $fullUrl, $options); + $this->response = $client->request('GET', $fullUrl, $options); } /** @@ -189,7 +198,7 @@ trait WebDav { */ public function checkPropForFile($file, $prefix, $prop, $value) { $elementList = $this->propfindFile($this->currentUser, $file, "<$prefix:$prop/>"); - $property = $elementList['/'.$this->getDavFilesPath($this->currentUser).$file][200]["{DAV:}$prop"]; + $property = $elementList['/' . $this->getDavFilesPath($this->currentUser) . $file][200]["{DAV:}$prop"]; Assert::assertEquals($property, $value); } @@ -231,6 +240,37 @@ trait WebDav { } /** + * @When Downloading folder :folderName + */ + public function downloadingFolder(string $folderName) { + try { + $this->response = $this->makeDavRequest($this->currentUser, 'GET', $folderName, ['Accept' => 'application/zip']); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $this->response = $e->getResponse(); + } + } + + /** + * @When Downloading public folder :folderName + */ + public function downloadPublicFolder(string $folderName) { + $token = $this->lastShareData->data->token; + $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/dav/files/$token/$folderName"; + + $client = new GClient(); + $options = []; + $options['headers'] = [ + 'Accept' => 'application/zip' + ]; + + try { + $this->response = $client->request('GET', $fullUrl, $options); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $this->response = $e->getResponse(); + } + } + + /** * @When Downloading file :fileName * @param string $fileName */ @@ -243,6 +283,42 @@ trait WebDav { } /** + * @When Downloading public file :filename + */ + public function downloadingPublicFile(string $filename) { + $token = $this->lastShareData->data->token; + $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/dav/files/$token/$filename"; + + $client = new GClient(); + $options = [ + 'headers' => [ + 'X-Requested-With' => 'XMLHttpRequest', + ] + ]; + + try { + $this->response = $client->request('GET', $fullUrl, $options); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $this->response = $e->getResponse(); + } + } + + /** + * @When Downloading public file :filename without ajax header + */ + public function downloadingPublicFileWithoutHeader(string $filename) { + $token = $this->lastShareData->data->token; + $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/dav/files/$token/$filename"; + + $client = new GClient(); + try { + $this->response = $client->request('GET', $fullUrl); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $this->response = $e->getResponse(); + } + } + + /** * @Then Downloaded content should start with :start * @param int $start * @throws \Exception @@ -348,7 +424,7 @@ trait WebDav { public function theResponseShouldContainAShareTypesPropertyWith($table) { $keys = $this->response; if (!array_key_exists('{http://owncloud.org/ns}share-types', $keys)) { - throw new \Exception("Cannot find property \"{http://owncloud.org/ns}share-types\""); + throw new \Exception('Cannot find property "{http://owncloud.org/ns}share-types"'); } $foundTypes = []; @@ -513,7 +589,7 @@ trait WebDav { </d:searchrequest>'; try { - $this->response = $this->makeDavRequest($user, "SEARCH", '', [ + $this->response = $this->makeDavRequest($user, 'SEARCH', '', [ 'Content-Type' => 'text/xml' ], $body, ''); @@ -555,7 +631,7 @@ trait WebDav { if ($type === 'files') { return $this->encodePath($this->getDavFilesPath($user) . $path); } else { - return $this->encodePath($this->davPath . '/' . $type . '/' . $user . '/' . $path); + return $this->encodePath($this->davPath . '/' . $type . '/' . $user . '/' . $path); } } @@ -588,9 +664,9 @@ trait WebDav { $elementRows = $expectedElements->getRows(); $elementsSimplified = $this->simplifyArray($elementRows); foreach ($elementsSimplified as $expectedElement) { - $webdavPath = "/" . $this->getDavFilesPath($user) . $expectedElement; + $webdavPath = '/' . $this->getDavFilesPath($user) . $expectedElement; if (!array_key_exists($webdavPath, $elementList)) { - Assert::fail("$webdavPath" . " is not in propfind answer"); + Assert::fail("$webdavPath" . ' is not in propfind answer'); } } } @@ -605,7 +681,7 @@ trait WebDav { public function userUploadsAFileTo($user, $source, $destination) { $file = \GuzzleHttp\Psr7\Utils::streamFor(fopen($source, 'r')); try { - $this->response = $this->makeDavRequest($user, "PUT", $destination, [], $file); + $this->response = $this->makeDavRequest($user, 'PUT', $destination, [], $file); } catch (\GuzzleHttp\Exception\ServerException $e) { // 5xx responses cause a server exception $this->response = $e->getResponse(); @@ -622,11 +698,11 @@ trait WebDav { * @param string $destination */ public function userAddsAFileTo($user, $bytes, $destination) { - $filename = "filespecificSize.txt"; + $filename = 'filespecificSize.txt'; $this->createFileSpecificSize($filename, $bytes); Assert::assertEquals(1, file_exists("work/$filename")); $this->userUploadsAFileTo($user, "work/$filename", $destination); - $this->removeFile("work/", $filename); + $this->removeFile('work/', $filename); $expectedElements = new \Behat\Gherkin\Node\TableNode([["$destination"]]); $this->checkElementList($user, $expectedElements); } @@ -637,7 +713,7 @@ trait WebDav { public function userUploadsAFileWithContentTo($user, $content, $destination) { $file = \GuzzleHttp\Psr7\Utils::streamFor($content); try { - $this->response = $this->makeDavRequest($user, "PUT", $destination, [], $file); + $this->response = $this->makeDavRequest($user, 'PUT', $destination, [], $file); } catch (\GuzzleHttp\Exception\ServerException $e) { // 5xx responses cause a server exception $this->response = $e->getResponse(); @@ -673,7 +749,7 @@ trait WebDav { public function userCreatedAFolder($user, $destination) { try { $destination = '/' . ltrim($destination, '/'); - $this->response = $this->makeDavRequest($user, "MKCOL", $destination, []); + $this->response = $this->makeDavRequest($user, 'MKCOL', $destination, []); } catch (\GuzzleHttp\Exception\ServerException $e) { // 5xx responses cause a server exception $this->response = $e->getResponse(); @@ -684,21 +760,6 @@ trait WebDav { } /** - * @Given user :user uploads chunk file :num of :total with :data to :destination - * @param string $user - * @param int $num - * @param int $total - * @param string $data - * @param string $destination - */ - public function userUploadsChunkFileOfWithToWithChecksum($user, $num, $total, $data, $destination) { - $num -= 1; - $data = \GuzzleHttp\Psr7\Utils::streamFor($data); - $file = $destination . '-chunking-42-' . $total . '-' . $num; - $this->makeDavRequest($user, 'PUT', $file, ['OC-Chunked' => '1'], $data, "uploads"); - } - - /** * @Given user :user uploads bulked files :name1 with :content1 and :name2 with :content2 and :name3 with :content3 * @param string $user * @param string $name1 @@ -709,31 +770,31 @@ trait WebDav { * @param string $content3 */ public function userUploadsBulkedFiles($user, $name1, $content1, $name2, $content2, $name3, $content3) { - $boundary = "boundary_azertyuiop"; + $boundary = 'boundary_azertyuiop'; - $body = ""; - $body .= '--'.$boundary."\r\n"; - $body .= "X-File-Path: ".$name1."\r\n"; + $body = ''; + $body .= '--' . $boundary . "\r\n"; + $body .= 'X-File-Path: ' . $name1 . "\r\n"; $body .= "X-File-MD5: f6a6263167c92de8644ac998b3c4e4d1\r\n"; $body .= "X-OC-Mtime: 1111111111\r\n"; - $body .= "Content-Length: ".strlen($content1)."\r\n"; + $body .= 'Content-Length: ' . strlen($content1) . "\r\n"; $body .= "\r\n"; - $body .= $content1."\r\n"; - $body .= '--'.$boundary."\r\n"; - $body .= "X-File-Path: ".$name2."\r\n"; + $body .= $content1 . "\r\n"; + $body .= '--' . $boundary . "\r\n"; + $body .= 'X-File-Path: ' . $name2 . "\r\n"; $body .= "X-File-MD5: 87c7d4068be07d390a1fffd21bf1e944\r\n"; $body .= "X-OC-Mtime: 2222222222\r\n"; - $body .= "Content-Length: ".strlen($content2)."\r\n"; + $body .= 'Content-Length: ' . strlen($content2) . "\r\n"; $body .= "\r\n"; - $body .= $content2."\r\n"; - $body .= '--'.$boundary."\r\n"; - $body .= "X-File-Path: ".$name3."\r\n"; + $body .= $content2 . "\r\n"; + $body .= '--' . $boundary . "\r\n"; + $body .= 'X-File-Path: ' . $name3 . "\r\n"; $body .= "X-File-MD5: e86a1cf0678099986a901c79086f5617\r\n"; $body .= "X-File-Mtime: 3333333333\r\n"; - $body .= "Content-Length: ".strlen($content3)."\r\n"; + $body .= 'Content-Length: ' . strlen($content3) . "\r\n"; $body .= "\r\n"; - $body .= $content3."\r\n"; - $body .= '--'.$boundary."--\r\n"; + $body .= $content3 . "\r\n"; + $body .= '--' . $boundary . "--\r\n"; $stream = fopen('php://temp', 'r+'); fwrite($stream, $body); @@ -743,13 +804,13 @@ trait WebDav { $options = [ 'auth' => [$user, $this->regularUser], 'headers' => [ - 'Content-Type' => 'multipart/related; boundary='.$boundary, + 'Content-Type' => 'multipart/related; boundary=' . $boundary, 'Content-Length' => (string)strlen($body), ], 'body' => $body ]; - return $client->request("POST", substr($this->baseUrl, 0, -4) . "remote.php/dav/bulk", $options); + return $client->request('POST', substr($this->baseUrl, 0, -4) . 'remote.php/dav/bulk', $options); } /** @@ -758,7 +819,7 @@ trait WebDav { public function userCreatesANewChunkingUploadWithId($user, $id) { $this->parts = []; $destination = '/uploads/' . $user . '/' . $id; - $this->makeDavRequest($user, 'MKCOL', $destination, [], null, "uploads"); + $this->makeDavRequest($user, 'MKCOL', $destination, [], null, 'uploads'); } /** @@ -767,7 +828,7 @@ trait WebDav { public function userUploadsNewChunkFileOfWithToId($user, $num, $data, $id) { $data = \GuzzleHttp\Psr7\Utils::streamFor($data); $destination = '/uploads/' . $user . '/' . $id . '/' . $num; - $this->makeDavRequest($user, 'PUT', $destination, [], $data, "uploads"); + $this->makeDavRequest($user, 'PUT', $destination, [], $data, 'uploads'); } /** @@ -778,7 +839,7 @@ trait WebDav { $destination = substr($this->baseUrl, 0, -4) . $this->getDavFilesPath($user) . $dest; $this->makeDavRequest($user, 'MOVE', $source, [ 'Destination' => $destination - ], null, "uploads"); + ], null, 'uploads'); } /** @@ -792,7 +853,7 @@ trait WebDav { $this->response = $this->makeDavRequest($user, 'MOVE', $source, [ 'Destination' => $destination, 'OC-Total-Length' => $size - ], null, "uploads"); + ], null, 'uploads'); } catch (\GuzzleHttp\Exception\BadResponseException $ex) { $this->response = $ex->getResponse(); } @@ -808,7 +869,7 @@ trait WebDav { $destination = '/uploads/' . $user . '/' . $this->getUploadId($id); $this->response = $this->makeDavRequest($user, 'MKCOL', $destination, [ 'Destination' => $this->s3MultipartDestination, - ], null, "uploads"); + ], null, 'uploads'); } /** @@ -819,7 +880,7 @@ trait WebDav { $destination = '/uploads/' . $user . '/' . $this->getUploadId($id) . '/' . $num; $this->response = $this->makeDavRequest($user, 'PUT', $destination, [ 'Destination' => $this->s3MultipartDestination - ], $data, "uploads"); + ], $data, 'uploads'); } /** @@ -830,7 +891,7 @@ trait WebDav { try { $this->response = $this->makeDavRequest($user, 'MOVE', $source, [ 'Destination' => $this->s3MultipartDestination, - ], null, "uploads"); + ], null, 'uploads'); } catch (\GuzzleHttp\Exception\ServerException $e) { // 5xx responses cause a server exception $this->response = $e->getResponse(); @@ -957,6 +1018,23 @@ trait WebDav { } /** + * @When Requesting share note on dav endpoint + */ + public function requestingShareNote() { + $propfind = '<d:propfind xmlns:d="DAV:" xmlns:nc="http://nextcloud.org/ns"><d:prop><nc:note /></d:prop></d:propfind>'; + if (count($this->lastShareData->data->element) > 0) { + $token = $this->lastShareData->data[0]->token; + } else { + $token = $this->lastShareData->data->token; + } + try { + $this->response = $this->makeDavRequest('', 'PROPFIND', $token, [], $propfind); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $this->response = $e->getResponse(); + } + } + + /** * @Then there are no duplicate headers */ public function thereAreNoDuplicateHeaders() { @@ -984,9 +1062,9 @@ trait WebDav { $elementRows = $expectedElements->getRows(); $elementsSimplified = $this->simplifyArray($elementRows); foreach ($elementsSimplified as $expectedElement) { - $webdavPath = "/" . $this->getDavFilesPath($user) . $expectedElement; + $webdavPath = '/' . $this->getDavFilesPath($user) . $expectedElement; if (!array_key_exists($webdavPath, $elementList)) { - Assert::fail("$webdavPath" . " is not in report answer"); + Assert::fail("$webdavPath" . ' is not in report answer'); } } } @@ -1001,12 +1079,12 @@ trait WebDav { $elementList = $this->listFolder($user, $folder, 1); $elementListKeys = array_keys($elementList); array_shift($elementListKeys); - $davPrefix = "/" . $this->getDavFilesPath($user); + $davPrefix = '/' . $this->getDavFilesPath($user); foreach ($elementListKeys as $element) { if (substr($element, 0, strlen($davPrefix)) == $davPrefix) { $element = substr($element, strlen($davPrefix)); } - $this->userDeletesFile($user, "element", $element); + $this->userDeletesFile($user, 'element', $element); } } @@ -1017,7 +1095,7 @@ trait WebDav { * @return int */ private function getFileIdForPath($user, $path) { - $propertiesTable = new \Behat\Gherkin\Node\TableNode([["{http://owncloud.org/ns}fileid"]]); + $propertiesTable = new \Behat\Gherkin\Node\TableNode([['{http://owncloud.org/ns}fileid']]); $this->asGetsPropertiesOfFolderWith($user, 'file', $path, $propertiesTable); return (int)$this->response['{http://owncloud.org/ns}fileid']; } @@ -1047,7 +1125,7 @@ trait WebDav { public function userCreatesAFileLocallyWithChunks($arg1, $chunks) { $this->parts = []; for ($i = 1;$i <= (int)$chunks;$i++) { - $randomletter = substr(str_shuffle("abcdefghijklmnopqrstuvwxyz"), 0, 1); + $randomletter = substr(str_shuffle('abcdefghijklmnopqrstuvwxyz'), 0, 1); file_put_contents('/tmp/part-upload-' . $i, str_repeat($randomletter, 5 * 1024 * 1024)); $this->parts[] = '/tmp/part-upload-' . $i; } @@ -1057,7 +1135,7 @@ trait WebDav { * @Given user :user creates the chunk :id with a size of :size MB */ public function userCreatesAChunk($user, $id, $size) { - $randomletter = substr(str_shuffle("abcdefghijklmnopqrstuvwxyz"), 0, 1); + $randomletter = substr(str_shuffle('abcdefghijklmnopqrstuvwxyz'), 0, 1); file_put_contents('/tmp/part-upload-' . $id, str_repeat($randomletter, (int)$size * 1024 * 1024)); $this->parts[] = '/tmp/part-upload-' . $id; } |